mmall商城用户模块开发总结

1、需要实现的功能介绍

注册 登录 用户名校验
忘记密码 提交问题答案 重置密码
获取用户信息 更新用户信息 退出登录

目标

  • 避免横向越权,纵向越权的安全漏洞
  • MD5明文加密级增加的salt值
  • Guava缓存的使用
  • 高复用服务响应对象的设计思想级抽象封装
  • session的使用

横向越权:攻击者尝试访问与他人拥有相同权限的用户。
纵向越权:低级别攻击者尝试访问高级别用户的资源

当用户在未登录的状态下修改密码时,用户回答了忘记密码的答案然后生成一个具有时间限制的token,这里用UUID表示,之后再跳转到重置密码界面输入新的密码提交,这里设置token的作用就是避免横向越权,如果一个网络攻击者直接进入重置密码页面,这里会进行检测他提交时是不是有正确的token,并且我们可以设置token的生效时间,这样就提高了安全性,当然密码的存储是经过MD5加密的,所以数据库看到的是加密后的密码。如果用户在登陆成功后,进行修改的话,我们不需要这么做。

用户重置密码阶段

这个阶段是在用户正确回答自己设置的问题后,拿到了服务器生成的token后进行密码重置,这时该进行怎样的处理呢?
首先前台表单中我们可以获取到用户名,提交表单携带的token,和新的密码。其次,我们先要验证的是传过来的token,如果他为空或者是个空串,或者包含空格,那就不合法,返回参数错误信息;然后,校验用户名,这时候为什么要再次校验username呢?因为在本地保存token时,我把username当做key,生成的token当做了value,所以在后面获取本地储存的token时,要保证username的正确,才能获取本地的token,校验完username后。下一步,我们开始从本地获取token,获取后需要校验一下本地的是不是为空或者是空串,这时才把两个token进行比较,当前台传过来的token与本地服务器端的token一样时,我们才开始更新密码,记得在更新前对新的密码进行加密,HDMC或者MD5方式,加密后才根据username更新密码字段,记得要带上更新时间,也就是一起把update_time也更新了。如果两个token不一样,那就设置错误的状态吗,提示token错误,请重新回答密保问题后再次进行更改。

核心代码如下


public ServerResponse<String> forgetResetPassword(String username, String passwordNew, String forgetToken) {
        if (StringUtils.isBlank(forgetToken)) {
            return ServerResponse.createByErrorMessage("参数错误,token需要传递");
        }
        //需要校验用户名,因为tokenCache的key包含username
        ServerResponse<String> checkValid = this.checkValid(username, Const.USERNAME);
        if (checkValid.isSuccess()) {
            //用户名不存在
            return ServerResponse.createByErrorMessage("用户不存在");
        }
        String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX + username);
        if (StringUtils.isBlank(token)) {
            return ServerResponse.createByErrorMessage("token无效或过期");
        }
        if (StringUtils.equals(forgetToken,token)){
            //这时开始更新password,先加密在保存
            String md5Password = MD5Util.MD5EncodeUtf8(passwordNew);
            int rowCount = userMapper.updatePasswordByUsername(username, md5Password);
            if (rowCount>0){
                return ServerResponse.createBySuccessMessage("修改密码成功");
            }
        }else{
            //如果两个token不一样,那就重新获取新的token
            return ServerResponse.createByErrorMessage("token错误,请重新回答问题后获取重置密码的token");
        }
        return ServerResponse.createByErrorMessage("修改密码失败");
    }

当用户回答完忘记密码的问题后,提交的用户名,问题,答案要在数据库中进行查询如果查到的记录数为1,代表问题与答案相匹配,然后生成token,这里也就是一个UUID,把这个token放在本地的token缓存中,以键值对的形式存储,token_username代表key,生成的UUID代表value,最后返回前端的数据是生成的token。代码如下

public ServerResponse<String> checkAnswer(String username, String question, String answer) {

        int resultCount = userMapper.checkAnswer(username, question, answer);
        if (resultCount > 0) {
            //问题答案匹配成功
            String forgetToken = UUID.randomUUID().toString();
            TokenCache.setKey(TokenCache.TOKEN_PREFIX + username, forgetToken);
            return ServerResponse.createBySuccessData(forgetToken);
        }
        return ServerResponse.createByErrorMessage("问题的答案错误");
    }

TokenCache类:

 private static Logger logger = LoggerFactory.getLogger(TokenCache.class);
    public static final String TOKEN_PREFIX = "token_";

    //LRU算法
    private static LoadingCache<String, String> localCache =
            CacheBuilder.newBuilder()
                    .initialCapacity(1000)
                    .maximumSize(10000)
                    .expireAfterAccess(12, TimeUnit.HOURS)
                    .build(new CacheLoader<String, String>() {
                        //默认的数据加载实现,当key没有对应的值时,就调用这个方法进行加载
                        @Override
                        public String load(String s) throws Exception {
                            return "null";
                        }
                    });

    public static void setKey(String key, String value) {
        localCache.put(key, value);
    }

    public static String getKey(String key){
        String value = null;
        try {
            value =  localCache.get(key);
            if ("null".equals(value)){
                return null;
            }
            return value;
        } catch (ExecutionException e) {
            logger.error("localCache get error",e);
        }
        return null;
    }

现在纯粹的MD5加密已经不安全了,因为有专门解密的相关网站,只要将加密后的密文粘贴上去就可以看到解密的密码。所以为了保存数据传入的安全性,我们需要加上盐值,再用MD5生成密文,只要对方获取不到盐值,他解密是很难的。来看一下怎样使用加盐的方式进行加密。
例如在注册的接口中:

注册过程

首先前台提交过来了一大堆内容,例如用户名,密码,邮箱等等,这时我们防止用户名重复,首先校验用户名不能重复,然后校验邮箱也不能重复,方法和校验用户名一样,再然后就开始对用户的密码进行加密。

public ServerResponse<String> register(User user) {
        //校验用户名
        ServerResponse<String> checkValid = this.checkValid(user.getUsername(), Const.USERNAME);
        if (!checkValid.isSuccess()) {
            return checkValid;
        }
        //校验email
        checkValid = this.checkValid(user.getEmail(), Const.EMAIL);
        if (!checkValid.isSuccess()) {
            return checkValid;
        }
        user.setRole(Const.Role.ROLE_CUSTOMER);
        //MD5加密
        user.setPassword(MD5Util.MD5EncodeUtf8(user.getPassword()));

        int i = userMapper.insert(user);
        if (i == 0) {
            return ServerResponse.createByErrorMessage("注册失败");
        }
        return ServerResponse.createBySuccessMessage("注册成功");
    }

这里开始查看MD5Util类的实现;

 public static String MD5EncodeUtf8(String origin) {
        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

这里就是加盐的过程了,如果在配置文件中没有设置盐默认就是一个空的字符串。password+salt的字符串在进行加密。

private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname)) {
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            } else {
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
            }
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

先将字符串转化为字节数组,然后整体对字节数组进行处理,对与字节数组中的每一个字节将他自身对16进行取余,整除运算,得到的两个结果,再从预先定义的private static final

数组中获取值,最终返回。

 private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++) {
            resultSb.append(byteToHexString(b[i]));
        }

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0) {
            n += 256;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }
 private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
原文地址:https://www.cnblogs.com/itjiangpo/p/14181419.html