• Redis实现短信登入功能(二)Redis实现登入功能


    使用Redis实现短信登入的流程

    发送短信验证码流程

    校验登录流程

    分步实现发送短信登入

    (1)发送短信验证码

    (2)短信登入、注册

    (3)校验登入状态

    完整代码实现


    使用Redis实现短信登入的流程

    发送短信验证码流程

    校验登录流程

    与之前使用Session登入相比,流程发生了比较大的变化。

    总的来说有以下几点:

    (1)之前的生成验证码后,我们会将其存放在Session中,每一个不同的请求就会对应一个Session,它们的SessionID肯定是唯一的;而现在是存在Redis中,它的key肯定不能是简单的指定"code",所以我们使用手机号作为key,验证码作为value,这样保证key的唯一性

    (2)登入注册的时候创建用户(已有用户),用户信息需要保存到Redis中,此时的key建议用一个随机的token(建议不要用手机号码,因为之后这个token会传入前端,有泄露风险!),value存放用户信息。所以这里返回token给客户端(浏览器),为的就是之后在校验的时候可以拿着token去Redis中取数据进行校验!

    (3)最开始的登入校验,之前是Session与Cookie,当使用了Session,它会自动将SessionID传入Cookie中,每次请求都会携带Cookie,就相当于带着SessionID查找用户。而现在是用请求中携带的token去Redis中获取用户数据。

    分步实现发送短信登入

    (1)发送短信验证码

    与之前Session的方式基本一致,只是在保存验证码的时候是保存到Redis中。

    UserService

    1. @Override
    2. public Result sendCode(String phone, HttpSession session) {
    3. //1. 校验手机号
    4. if (RegexUtils.isPhoneInvalid(phone)) {
    5. //2.如果不符合,返回错误信息
    6. return Result.fail("手机号格式错误");
    7. }
    8. //3. 符合,生成验证码
    9. String code = RandomUtil.randomNumbers(6);
    10. //4. 保存验证码到Redis
    11. // 可以加上一个业务前缀来区分
    12. // 设置验证码的有效期 Redis的key有效期
    13. // ("login:code:" + phone,code,2, TimeUnit.MINUTES)
    14. stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
    15. //5. 发送验证码
    16. log.debug("发送短信验证码成功,验证码:{}",code);
    17. //返回ok
    18. return Result.ok();
    19. }

    上述代码使用到了一些常量,我们这里是用一个工具类RedisConstants 去声明一些常量。

    1. public class RedisConstants {
    2. public static final String LOGIN_CODE_KEY = "login:code:";
    3. public static final Long LOGIN_CODE_TTL = 2L;
    4. public static final String LOGIN_USER_KEY = "login:token:";
    5. public static final Long LOGIN_USER_TTL = 36000L;
    6. public static final Long CACHE_NULL_TTL = 2L;
    7. public static final Long CACHE_SHOP_TTL = 30L;
    8. public static final String CACHE_SHOP_KEY = "cache:shop:";
    9. public static final String LOCK_SHOP_KEY = "lock:shop:";
    10. public static final Long LOCK_SHOP_TTL = 10L;
    11. public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    12. }

    (2)短信登入、注册

    需要改动的东西较多。

    • 首先我们需要从Redis中获取验证码,并进行校验;
    • 然后要将用户信息与随机生成的token一起存放到redis中;
    • 最后再设置一下token的有效期。

    UserService

    1. @Override
    2. public Result login(LoginFormDTO loginForm, HttpSession session) {
    3. //1. 校验手机号
    4. String phone = loginForm.getPhone();
    5. if (RegexUtils.isPhoneInvalid(phone)) {
    6. return Result.fail("手机号格式错误");
    7. }
    8. //2. 从redis获取验证码并校验
    9. String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    10. String code = loginForm.getCode();
    11. if (cacheCode == null || !cacheCode.toString().equals(code)){
    12. //3. 不一致,报错
    13. return Result.fail("验证码错误");
    14. }
    15. //4.一致,根据手机号查询用户
    16. User user = query().eq("phone", phone).one();
    17. //5. 判断用户是否存在
    18. if (user == null){
    19. //6. 不存在,创建新用户
    20. user = createUserWithPhone(phone);
    21. }
    22. //7.保存用户信息到redis
    23. //7.1 生成随机token作为登入令牌
    24. String token = UUID.randomUUID().toString(true);
    25. //7.2 将User对象作为HashMap存储
    26. UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    27. // 注意!!!
    28. Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
    29. CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fileName,fileValue) -> fileValue.toString()));
    30. //7.3 存储
    31. String tokenKey = LOGIN_USER_KEY + token;
    32. stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
    33. //7.4设置token的有效期
    34. stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
    35. //8.返回token
    36. return Result.ok(token);
    37. }

    创建用户

    1. private User createUserWithPhone(String phone) {
    2. // 1.创建用户
    3. User user = new User();
    4. user.setPhone(phone);
    5. user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    6. // 2.保存用户
    7. save(user);
    8. return user;
    9. }

    (3)校验登入状态

    MvcConfig 

    1. @Configuration
    2. public class MvcConfig implements WebMvcConfigurer {
    3. @Autowired
    4. private LoginInterceptor loginInterceptor;
    5. @Autowired
    6. private RefreshTokenInterceptor refreshTokenInterceptor;
    7. @Override
    8. public void addInterceptors(InterceptorRegistry registry) {
    9. // 登录拦截器
    10. registry.addInterceptor(loginInterceptor)
    11. .excludePathPatterns(
    12. "/shop/**",
    13. "/voucher/**",
    14. "/shop-type/**",
    15. "/upload/**",
    16. "/blog/hot",
    17. "/user/code",
    18. "/user/login"
    19. ).order(1);
    20. // token刷新的拦截器
    21. registry.addInterceptor(refreshTokenInterceptor)
    22. .addPathPatterns("/**").order(0);
    23. }
    24. }

    LoginInterceptor 

    1. @Component
    2. public class LoginInterceptor implements HandlerInterceptor {
    3. // 基于Redis设置的拦截器
    4. @Override
    5. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    6. //判断是否要拦截
    7. if (UserHolder.getUser() == null) {
    8. response.setStatus(401);
    9. return false;
    10. }
    11. //有用户,放行
    12. return true;
    13. }
    14. @Override
    15. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    16. //移除用户
    17. UserHolder.removeUser();
    18. }
    19. }

    RefreshTokenInterceptor  

    1. @Component
    2. public class RefreshTokenInterceptor implements HandlerInterceptor {
    3. @Autowired
    4. private StringRedisTemplate stringRedisTemplate;
    5. @Override
    6. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    7. // 1.获取请求头中的token
    8. String token = request.getHeader("authorization");
    9. if (StrUtil.isBlank(token)) {
    10. return true;
    11. }
    12. // 2.基于TOKEN获取redis中的用户
    13. String key = LOGIN_USER_KEY + token;
    14. Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    15. // 3.判断用户是否存在
    16. if (userMap.isEmpty()) {
    17. return true;
    18. }
    19. // 5.将查询到的hash数据转为UserDTO
    20. UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    21. // 6.存在,保存用户信息到 ThreadLocal
    22. UserHolder.saveUser(userDTO);
    23. // 7.刷新token有效期
    24. stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
    25. // 8.放行
    26. return true;
    27. }
    28. @Override
    29. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    30. // 移除用户
    31. UserHolder.removeUser();
    32. }
    33. }

    这里使用了两个拦截器的原因如下

    先看这一张图,这是我们之前的方案,只用一个拦截器,拦截的是需要做登入校验的路径。

    但是问题来了!如果用户登入后,访问的都是无需登入就可以看的页面,也就是说这个拦截器不生效!而token有效期就不会刷新!当有效期一到就会自动登出!

    如何解决?

    使用 LoginInterceptor 来拦截所有请求,使用RefreshTokenInterceptor  拦截登入的路径。

    完整代码实现

    链接:https://pan.baidu.com/s/1qeUeCcN3cvBH79p6WJu2tA 
    提取码:95yj 
    --来自百度网盘超级会员V4的分享

  • 相关阅读:
    私有化轻量级持续集成部署方案--03-部署web服务(上)
    外贸谈判过程中,我这样和客户斗智斗勇……
    通达信接口的定义和实现
    IPM 鸟瞰图公式转换与推导
    JAVA微信小程序影视交流评论小程序系统毕业设计 开题报告
    【Python】新手入门学习:详细介绍里氏替换原则(LSP)及其作用、代码示例
    Linux--进程终止
    SeaTunnel 学习笔记
    Docker高级——1 Docker复杂安装详说
    c++函数参数和返回值
  • 原文地址:https://blog.csdn.net/weixin_43715214/article/details/125516361