【redis实战篇】第二天

摘要:

        传统的Session机制在用户数量增加时面临扩展性差、负载均衡困难、安全隐患、依赖客户端Cookie和状态管理复杂等问题。为解决这些问题,采用Redis替代Session实现用户登录。

        Redis具有优异的读写速度,提升了项目性能,并解决了多服务器部署时的互通问题。具体实现包括:发送验证码时检查手机号格式、生成并保存验证码到Redis;登录校验时查询或创建用户,保存用户信息到Redis并设置有效期;编写拦截器实现用户信息刷新和登录状态保持。

        通过优化拦截器,确保用户在访问未被拦截的功能时仍保持登录状态。Redis的使用显著提高了系统的性能和安全性。

传统的session存在以下问题:

  1. 扩展性问题:随着用户数量增加,服务器负载加重,可能导致性能下降。
  2. 难以处理负载均衡:在分布式系统中,不同服务器间的Session共享和管理困难。
  3. 安全隐患:Session ID容易被盗,可能导致账号安全问题。
  4. 依赖客户端Cookie:如果客户端禁用Cookie,功能会受限。
  5. 状态管理复杂:Session是有状态的,影响服务器扩展性和维护。

所以采用redi代替session来实现用户登录,得益于redis优异的读写速度,一方面提高了项目的性能(登录和访问都是大数量级的操作),另一方面解决了多台服务器部署时无法互通的问题,提高了扩展性

一,发送验证码

(1)检查手机号格式,其实就是使用正则表达式来进行匹配,如果这里一直出现手机号格式错误的问题,可以考虑更换正则表达式,网上随意搜索即可。

(2)利用hutool生成随机6位数字作为验证码

(3)保存验证码到redis,并设置过期时间

    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到redis,并设置过期时间
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone, code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5,发送验证码
        log.info("发送短信验证码成功,验证码:{}", code);
        return Result.ok();
    }

二,登录校验

(1)通过手机号在数据库中查询用户,如果不存在则创建新用户保存

(2)保存userDTO到redis,key为业务名拼接随机UUID(token)生成,采用Hash类型存储对象。

(3)使用hutool工具包的BeanUtil.beanToMap()方法将对象转换为map,因为对象中id类型为Long会出现类型转换异常,所以必须指定值的转换规则

Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),
        CopyOptions.create() //指定装换规则
                .setIgnoreNullValue(true)//忽略空值
                //将值转换为字符串
                .setFieldValueEditor((fieldName, fieldValue)-> fieldValue.toString()));

(4)设置有效期,同时引出问题:只有当用户在30分钟内什么都没有做,我们才在redis中剔除这个信息,但是如何判断用户持续活跃?解决方法:如果用户访问被拦截说明用户是活跃的,我们要重新更新过期时间

public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
            //2.不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.校验验证码
        if (RegexUtils.isCodeInvalid(loginForm.getCode())){
            //4.不符合,返回错误信息
            return Result.fail("验证码格式错误!");
        }
        //redis中获取code并且校验
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone());
        if (code==null||!code.equals(loginForm.getCode())){
            return Result.fail("验证码错误!");
        }
        //5.符合,根据手机号查询用户
        User user = query().eq("phone", loginForm.getPhone()).one();
        //6.判断用户是否存在
        if(user == null){
            //7.不存在,创建新用户并保存
            user = new User();
            user.setPhone(loginForm.getPhone());
            user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
            save(user);
        }

        //8.保存用户信息到redis并返回
        //拼接redis中的key
        String token = UUID.randomUUID().toString(true);
        String key = LOGIN_USER_KEY+token;
        //隐藏用户敏感信息
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue)-> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll(key, map);
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return Result.ok(token);
    }
}

 三,编写拦截器

(1)实现HandlerInterceptor接口,并实现pre和after方法

(2)取出请求头中的token,然后拼接常量作为key从redis中取出用户信息(map),通过万能hutool包中的BeanUtil.fillBeanWitMap将map转换为userDTO对象,最后保存到本地线程

(3)重置redis中用户信息的过期时间为session的30分钟

(4)注册拦截器并设置拦截排除路径,如:"/user/login".....等等

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        //判断token是否存在
        if(StrUtil.isBlank(token)){
            response.setStatus(401);
            return false;
        }
        //redis中获取用户信息
        String key = LOGIN_USER_KEY+token;
        Map<Object, Object> userDTOMap = redisTemplate.opsForHash().entries(key);
        //判断用户是否存在
        if(userDTOMap.isEmpty()){
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userDTOMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

四,拦截器优化

问题:我们通过拦截器刷新redis用户信息来保证用户登录状态,但是拦截器是没有拦截所有路径的,这就会导致用户在访问未被拦截的功能,如:浏览点评等,达到过期时间也会自动登出

解决方法:众嗦粥滋,没有什么是加一层中间层解决不了的,如果有就再加一层 ,所以我们在原拦截器前再加一个拦截器,作用仅用于刷新用户token不执行拦截,原来的拦截器只需要检查本地线程是否有用户信息即可。

小tips:拦截器注册时可通过order(整数)指定拦截器执行优先级,整数值越小,优先级越高

@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        //判断token是否存在
        if(StrUtil.isBlank(token)){
            return true;
        }
        //redis中获取用户信息
        String key = LOGIN_USER_KEY+token;
        Map<Object, Object> userDTOMap = redisTemplate.opsForHash().entries(key);
        //判断用户是否存在
        if(userDTOMap.isEmpty()){
            return true;
        }
        //将userDTOMap中的userDTO键值对填充到Bean中
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userDTOMap, new UserDTO(), false);
        //保存信息到本地线程
        UserHolder.saveUser(userDTO);
        //用户访问说明活跃,刷新有效期
        redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

        @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值