Redis实战篇–短信登录

Redis实战篇–短信登录

短信登录

基于session实现登录

主要流程
在这里插入图片描述
验证登录功能
在这里插入图片描述

session共享问题: 多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题session的替代方案应该满足:

  • 数据共享
  • 内存存储
  • key、value结构

基于Redis实现共享session登录

主要流程
在这里插入图片描述

在这里插入图片描述
发送短信验证码

 public Result sendCodeByPhone(String phone, HttpSession session) {
        //1.校验手机号
        if (!RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机格式错误");
        }
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码到redis
        //session.setAttribute("code", code);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //4.发送验证码
        log.debug("发送短信成功,验证码 :{}" ,code);
        return Result.ok();
    }

短信验证码登录、注册

public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (!RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机格式错误");
        }
        //2.校验验证码
        String code = loginForm.getCode();
        //从redis中获取验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        //3.验证码不一致
        if (cacheCode == null || !cacheCode.equals(code)) {
            return Result.fail("验证码错误");
        }
        //4.验证码一致
        //5.查询用户是否存在
        User user = query().eq("phone", phone).one();
        //6.不存在 创建新用户
        if (user == null) {
            user = userService.createUser(phone);
        }
        //7.将用户信息保存至redis
        //7.1 随机生成token 作为主键
        String token = UUID.randomUUID().toString(true);
        //7.2 将User对象转为Hash存储
        //将user转为UserDTO是为了保护数据
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fileldValue) -> fileldValue.toString()));
        //7.3存储
        //将UserDTO转为Map
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //7.4设置 token 有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.SECONDS);
        //8.返回token
        return Result.ok(token);
    }

    @Override
    public User createUser(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        save(user);
        return user;
    }

保存数据至Redis数据结构的选择
在这里插入图片描述

登录拦截器的优化

在原本的有选着性的拦截器前再加上一个拦截任何请求的拦截器,将获取和刷新token的操作移至前一个拦截器中。
在这里插入图片描述

/**
第一个拦截器 
*/
public class RefeshTokenTimeInterceptor implements HandlerInterceptor {

    //注入stringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;

    public RefeshTokenTimeInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //controller之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        if(StringUtil.isNullOrEmpty(token)){
            return  true;
        }
        //2.基于token获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            return  true;
        }
        //将查询到的数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //6.存在保存用户信息至ThreadLocal
        UserHolder.saveUser(userDTO);
        // 刷新token有效期
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //8.放行


        return HandlerInterceptor.super.preHandle(request, response, handler);
    }


    //controller之后
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    //视图渲染之后返回用户之前  销毁对应用户信息 避免内存泄露
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

/**
第二个拦截器 
*/
public class LoginInterceptor implements HandlerInterceptor {

    //controller之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDTO user = UserHolder.getUser();
        //判断是否需要拦截 即ThreadLocal中是否有用户
        if (user == null) {
            //没有 需要拦截
            response.setStatus(401);
            //拦截
            return false;
        }
        //8.放行
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }


    //controller之后
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    //视图渲染之后返回用户之前  销毁对应用户信息 避免内存泄露
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);

    }
}

对拦截器进行配置

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
        .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
        ).order(1);
        registry.addInterceptor(new RefeshTokenTimeInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

总结

Redis代替session需要考虑的问题

  • 选择合适的数据结构
  • 选择合适的key
  • 选择合适的存储粒度
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值