• 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"};
    
  • 相关阅读:
    uip源码剖析【三】——【网络层】ICMP解读
    uip源码剖析【五】——【传输层】TCP解读
    WebGame方案汇总
    终于,我生命中第一次编译并运行了手机程序
    使用R7版NDK搭建Android开发环境[不使用Cgywin]
    拷问Unity:开发U3D游戏要思考的问题
    浏览器缓存导致FLASH资源更新问题的解决方案
    山寨版的《KingdomRush(皇城突袭)》
    在Unity3D的网络游戏中实现资源动态加载
    Unity3d之无缝地形场景切换–解决方法和代码
  • 原文地址:https://www.cnblogs.com/itjiangpo/p/14181419.html
Copyright © 2020-2023  润新知