当前位置:   article > 正文

redis实战-项目集成git及redis实现短信验证码登录_验证码流程 redis

验证码流程 redis

 

目录

 

 

IDEA集成git

传统session存在的问题 

redis方案

业务流程

选用的数据结构

整体访问流程

发送短信验证码

 获取校验验证码

配置登录拦截器

拦截器注册配置类

拦截器

用户状态刷新问题

刷新问题解决方案


 

IDEA集成git

远程仓库采用码云,创建好仓库,复制仓库的url

2b81c37fc98443f18a1d0fb2bc188eab.png

 

 在idea中点击,出现git选项,点击ok

f7b350cd46a64df3ae76751c89607036.png

ce290b018ee94e3f8e315fbd3c00e387.png

 之后右击项目,点击remotes654dd9e6be9541b8a4db410eeab3d6d8.png

 填写url即可集成gitee31465e142b4a249aea4bd7fc51d503.png

传统session存在的问题 

传统的登录认证会采用session进行登录认证,将登录的验证码,用户信息都存放到session中,我们通过session来进行操作数据,这有什么问题呢

每个tomcat服务器中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,即session在各个服务器之间是不共通的,所以此时整个登录拦截功能就会出现问题,而redis数据本身就是共享的,就可以避免session共享的问题了

redis方案

业务流程

  • 将验证码存储到redis中
  • 将用户数据存储到redis中

选用的数据结构

存储验证码时采用String类型,key采用业务代码+手机号

存储user数据时采用hash,key采用业务代码+随机的tonke

hash可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少,实际上也可以采用String类型,但hash类型消耗内存较少,故选用String类型

整体访问流程

 当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,简化后续业务获取用户信息的代码,后续业务需要登录用户的信息,只要在threadLocal中取即可,无需从redis中取从而增加复杂度,最后放行。

66fa57b113e7443a80b588a092f4d75c.png

redis业务代码常量(后续继续补充)

  1. public class RedisConstants {
  2. //发送验证码业务标识
  3. public static final String LOGIN_CODE_KEY = "login:code:";
  4. public static final Long LOGIN_CODE_TTL = 2L;
  5. //用户登录业务标识
  6. public static final String LOGIN_USER_KEY = "login:token:";
  7. public static final Long LOGIN_USER_TTL = 30L;
  8. }

发送短信验证码

采用日志打印形式,将验证码打印在控制台

  1. public Result sendCode(String phone) {
  2. //校验手机号
  3. if (RegexUtils.isPhoneInvalid(phone)) {
  4. return Result.fail("手机号码格式不正确");
  5. }
  6. //手机号格式正确生成验证码
  7. String code = RandomUtil.randomNumbers(6);
  8. //保存到redis
  9. stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
  10. //打印日志
  11. log.debug("手机验证码为:" + code);
  12. return Result.ok();
  13. }

 获取校验验证码

我们需要对用户敏感信息进行筛选,只给前端返回必要的用户信息数据,需要封装dto对象,同时在uuid生成token,作为redis取数据的key,最后要设置过期时间,防止reids内存爆炸,设置为30分钟,因为session的过期时间也是30分钟

  1. public Result login(LoginFormDTO loginForm) {
  2. String phone = loginForm.getPhone();
  3. String code = loginForm.getCode();
  4. //校验手机号
  5. if (RegexUtils.isPhoneInvalid(phone)) {
  6. return Result.fail("手机号码格式不正确");
  7. }
  8. //从redis取验证码,进行比对
  9. String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
  10. if (cacheCode == null || !cacheCode.equals(code)) {
  11. return Result.fail("验证码错误");
  12. }
  13. //根据电话号码从数据库查询用户是否存在
  14. LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
  15. queryWrapper.eq(User::getPhone, phone);
  16. User user = userMapper.selectOne(queryWrapper);
  17. //不存在则创建
  18. if (user == null) {
  19. user = createUserWithPhone(phone);
  20. }
  21. String token = UUID.randomUUID().toString(true);
  22. //只返回不敏感的信息,使用dto进行封装
  23. UserDTO userDTO = new UserDTO();
  24. BeanUtils.copyProperties(user, userDTO);
  25. //将dto对象转化为map结构
  26. Map<String, String> userMap = new HashMap<>();
  27. userMap.put("id", userDTO.getId().toString());
  28. userMap.put("icon", userDTO.getIcon());
  29. userMap.put("nickName", userDTO.getNickName());
  30. //业务代码+token形成redis中的key
  31. String tokenKey = LOGIN_USER_KEY + token;
  32. stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
  33. stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
  34. return Result.ok(token);
  35. }

配置登录拦截器

拦截器注册配置类

  1. @Configuration
  2. public class MvcConfig implements WebMvcConfigurer {
  3. @Autowired
  4. private StringRedisTemplate stringRedisTemplate;
  5. @Override
  6. public void addInterceptors(InterceptorRegistry registry) {
  7. // 登录拦截器
  8. registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
  9. .excludePathPatterns(
  10. "/shop/**",
  11. "/voucher/**",
  12. "/shop-type/**",
  13. "/upload/**",
  14. "/blog/hot",
  15. "/user/code",
  16. "/user/login"
  17. );
  18. }
  19. }

拦截器

拦截器需要的stringRedisTemplate对象不能通过@Autowired直接注入

因为LoginInterceptor 对象是我们手动创建的,不受spring管控,不在spring容器中,故不能注入spring容器中的bean,只能通过在配置类中通过构造方法注入stringRedisTemplate对象

  1. public class LoginInterceptor implements HandlerInterceptor {
  2. private StringRedisTemplate stringRedisTemplate;
  3. public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
  4. this.stringRedisTemplate = stringRedisTemplate;
  5. }
  6. @Override
  7. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  8. // 1.获取请求头中的token
  9. String token = request.getHeader("authorization");
  10. if (StrUtil.isBlank(token)) {
  11. response.setStatus(401);
  12. return false;
  13. }
  14. String tokenKey = LOGIN_USER_KEY + token;
  15. Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
  16. //3.判断用户是否存在
  17. if (userMap.isEmpty()) {
  18. //4.不存在,拦截,返回401状态码
  19. response.setStatus(401);
  20. return false;
  21. }
  22. UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
  23. //5.存在,保存用户信息到Threadlocal
  24. UserHolder.saveUser(userDTO);
  25. //刷新登录状态
  26. stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
  27. //6.放行
  28. return true;
  29. }
  30. }

用户状态刷新问题

那就是登录认证和刷新用户状态绑定在一个拦截器中,而需要登录认证的路径并不是全部路径,如果用户不访问需要登录认证的路径,那就刷新不了用户状态,即30分钟之后登录就会退出,这明显不符合我们的常识

刷新问题解决方案

我们可以再加一个拦截器,形成一个拦截器链,第一个拦截器对所有路径进行拦截,而它的功能就是刷新登录状态,登录认证则由第二个拦截器完成

 

2bad35b884d24d54ab1d2d27bfd97849.png

 

 刷新状态拦截器

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

登录认证拦截器

  1. public class LoginInterceptor implements HandlerInterceptor {
  2. @Override
  3. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4. // 1.判断是否需要拦截(ThreadLocal中是否有用户)
  5. if (UserHolder.getUser() == null) {
  6. // 没有,需要拦截,设置状态码
  7. response.setStatus(401);
  8. // 拦截
  9. return false;
  10. }
  11. // 有用户,则放行
  12. return true;
  13. }
  14. }

为了保证拦截器的执行先后顺序,配置类需设置order的大小,设定拦截器执行的先后的顺序,如若不设置,就按照注册顺序的先后来执行

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/627197
推荐阅读
相关标签
  

闽ICP备14008679号