黑马点评项目-4.Redis实现短信验证登录

Redis实现短信登录

业务流程

短信验证码业务流程图

Session方式实现的问题

为了支持系统整体的性能,会使用负载均衡的相关技术。此时,由于多台Tomcat不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失。

服务架构图

  • 每个Tomcat服务器有自己的session空间。
  • 第一次请求,Nginx分配给Tomcat A;第二次请求Nginx分配给Tomcat B,B无法获取到A中的Session数据。

需要一种方案替代Session,这种方案必须满足:

  1. 数据共享:不同Tomcat能够读取到相同的数据。(改进)
  2. 内存存储:Session存在服务器内存,速度快。(保持)
  3. Key-Value结构:Session本身就是存储键值对,效率高。(保持)

Redis替代Session

  1. Redis是Tomcat服务器方案之外的存储方案,任何一台Tomcat都可以访问Redis。
  2. Redis存在内存中,性能强劲。
  3. Redis是键值对存储,Value支持多种数据结构。

Redis实现短信验证登录

思考

  1. 使用Redis保存数据,Value应该选择何种数据结构?
    验证码用String即可。
  2. Key应该存什么?
    使用Session时,由于Tomcat独占各自内存,保存相同Key值互不干扰。当用户请求到来,自动生成SessionID写入用户浏览器,用户下次访问携带SessionID可以查到服务器之前保存的KEY-VALUE,即不同的请求可以使用相同的KEY,但由于SessionID的存在,仍然能够区分(SessionID-KV)。而Redis是共享空间,对于不同Tomcat处理的请求,由于SessionID不存在,无法区分,需要通过KEY来区分(K-V)。需要为每个不同的手机号,需要保存不同的Key。因此直接将手机号作为Key值。
  3. 通过验证后,从MySQL查到的用户对象(根据查询结果封装)用什么数据结构存到Redis?
    首先,此处将用户对象存入Redis是为了访问资源时携带用户信息,便于进行对应资源的展示(使用Session方式,登陆成功时,也会存入用户对象)。此处Redis应该使用Hash来存储用户对象信息,相较于JSON字符串,占用空间更少(Json会存入一些结构化的字符,例如“{}”)。Key值使用随机Token,并在前端手动写入用户浏览器(替代SessionID)。以后用户请求携带token,从而查找到用户对应的用户对象。

实现

验证码注册/登录

UserServiceImpl实现类中定义sedCode方法,其中phone是前端的传值。

    @Override
    public Result sedCode(String phone, HttpSession session) {
        //1. 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3. 符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4. 保存验证码到session
        //设置一个验证码的有效期,验证码到期即从Redis删除。
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5. 发送验证码(此处模拟,应该使用阿里云等平台提供的服务接口实现)
        log.debug("发送短信验证码成功,验证码:{}", code);
        //返回ok
        return Result.ok();
    }

UserServiceImpl实现类中定义login方法,其中loginForm是前端的表单,包含手机号。

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1. 校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        //2.从Redis获取验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) return Result.fail("验证码错误");
        //4.通过验证码校验,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5. 判断用户是否存在
        if (user == null) {
            //6. 不存在,创建新用户到MySQL数据库
            user = createUserWithPhone(phone);
        }
        //7.保存用户信息到redis中
        //7.1生成随机token作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //7.2将User转为UserDTO(保留基础信息,防止隐私泄露)Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); //简化用户描述信息,避免信息泄露
        //7.3将UserDTO对象转为Map类型。
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions
                .create()
                .setIgnoreNullValue(true)
                .setFieldValueEditor((filedName, filedValue) -> filedValue.toString()));  //将对象转为Map(包含多个KV),
        //7.3 存储用户对象信息到Redis
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        //7.4 设置有效期,防止大量数据占内存
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return Result.ok(token);
    }
访问鉴权:登录/注册完毕后跳转到资源页,资源页面的访问可能需要鉴权!

对于共有数据,例如首页,广告,无需鉴权。但访问这些页面仍然需要刷新token,刷新redis中用户信息的有效期。否则,用户访问公共页面太久会被推出私密数据的访问权限。

RefreshTokenInterceptor全局拦截器中刷新token。在需要登录的路径查询ThreadLocal中是否存在用户信息。

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

为什么要存到ThreadLocal?既然已经存到Redis,那每次用户请求,直接去Redis中鉴权不就好了?Redis是共用数据库,用户量过大、访问量上升时导致服务器负担较重。使用ThreadLocal可以减少io操作,也可以保证访问的稳定性,如果访问过程中redis故障了,ThreadLocal仍然正常使用。更多参考:百度贴吧CSDN

LoginInterceptor登录拦截器中只做鉴权,不做刷新。

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser() == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值