Redis实现token生存、校验和革新

打印 上一主题 下一主题

主题 978|帖子 978|积分 2934

一、简要介绍

​ 基于session实现登录流程时,在部署多台服务器环境下会出现session共享题目,对于这一题目有很多解决方案,本文利用Redis存储token,解决session共享题目,并实现用户登录状态生存、校验和革新。主要技能栈利用SpringBoot框架(3.x),Redis非关系型数据库 (3.x)等,MyBatisPlus,Gson等。
二、实现思路

1.用户登录


​ 在用户登录流程结束前,生成一个token,生存登录状态同时将token返回给前端生存,用作之后的哀求校验。
2.哀求校验


​ 登录乐成的用户之后的每次哀求都会在哀求头中携带token,这里通过拦截器获取到哀求头中的token,判断这个token的具体状态,实现对用户登录流程和页面访问的具体管控。
三、具体实现

1.用户登录令牌发放

​ 这里主要纪录一下service层的实现方法
  1. /**
  2. * 用户登录
  3. *
  4. * @param userLoginDTO
  5. * @return
  6. */
  7. @Override
  8. public UserLoginVO userLogin(UserLoginDTO userLoginDTO) {
  9.     // 1.校验用户账号和密码格式
  10.     String userAccount = userLoginDTO.getUserAccount();
  11.     String userPassword = userLoginDTO.getUserPassword();
  12.     if (!AccountUtils.checkAccount(userAccount)) {
  13.         throw new BaseException(ExceptionConstant.ACCOUNT_ERROR);
  14.     }
  15.     if (!PasswordUtils.checkPassword(userPassword)) {
  16.         throw new BaseException(ExceptionConstant.PASSWORD_ERROR);
  17.     }
  18.     // 2.校验验证码
  19.     String code = userLoginDTO.getCode();
  20.     VerifyServiceImpl.checkVerify(code,userLoginDTO.getKey(),stringRedisTemplate);
  21.     // 3.查询账号密码是否正确
  22.     // 将密码转换为加密状态
  23.     String transPassword = DigestUtils.md5DigestAsHex((userPassword + saltProperties.getUserPasswordSalt()).getBytes());
  24.     LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
  25.     lambdaQueryWrapper.eq(User::getUserAccount,userAccount)
  26.             .eq(User::getUserPassword,transPassword);
  27.     User user = userMapper.selectOne(lambdaQueryWrapper);
  28.     if (user == null) {
  29.         // 未查到说明用户名或密码错误
  30.         throw new BaseException(ExceptionConstant.LOGIN_FAIL);
  31.     }
  32.     // 4.校验账号状态
  33.     if (user.getUserStatus() == UserConstant.EX_USER_STATUS) {
  34.         throw new BaseException(ExceptionConstant.ACCOUNT_EX_CODE,ExceptionConstant.ACCOUNT_EX);
  35.     }
  36.     // 5.生成token
  37.     String token = UUID.randomUUID().toString();
  38.     // 6.将token作为key将用户信息保存在redis
  39.     RedisOperateUtils.saveUserInRedis(user,token,stringRedisTemplate);
  40.     // 7.返回vo
  41.     return UserLoginVO.builder()
  42.             .id(user.getId())
  43.             .userAccount(user.getUserAccount())
  44.             .token(token)
  45.             .build();
  46. }
复制代码
​ token这里先用uuid实现,扩展阶段可以思量联合jwt令牌等方法,redis相关操作在这里封装了工具类,生存方法具体如下:
  1. /**
  2. * 向redis中保存user
  3. * @param user 用户
  4. * @param token token
  5. * @param stringRedisTemplate stringRedisTemplate
  6. */
  7. public static void saveUserInRedis(User user, String token, StringRedisTemplate stringRedisTemplate) {
  8.     UserSafetyDTO userSafetyDTO = UserSafetyDTO.builder()
  9.             .id(user.getId())
  10.             .userAccount(user.getUserAccount())
  11.             .phone(user.getPhone())
  12.             .build();
  13.     Gson gson = new Gson();
  14.     String userJson = gson.toJson(userSafetyDTO);
  15.     stringRedisTemplate.opsForValue().set(RedisConstant.USER_TOKEN_KEY + token, userJson, RedisConstant.USER_TOKEN_TIMEOUT, TimeUnit.MINUTES);
  16. }
复制代码
​ 先把用户信息举行简化处理,利用json处理工具将用户信息序列化为json格式(这里json处理推荐Gson),将token作为key,json字符串作为value,设置过期时间(这里设置的是30min)存入Redis中。
   后期扩展可以思量利用Hash格式在redis中生存用户数据,节省内存空间
  2.哀求头令牌校验

​ 利用redis的相关性质,可以实现对token过期时间的革新。在用户登录乐成后,如果30min(过期时间)之内访问了相关页面,就可以对用户的token令牌举行。一样平常用户访问站内的页面都需要对token举行革新,但是token另有着校验身份的需求(需要登录才能举行访问的页面),分析以上场景,这就需要利用两个拦截器实现这个业务,一个拦截器拦截客户端的全部哀求,革新对应哀求头中的token过期时间,一个拦截器只拦截需要校验用户是否登录。

具体代码实现示例:
这里token名称设置为了配置类,统一配置方便更改;用本身封装的redis工具类获取用户,ThreadLocal的操作封装成了UserHolder
革新token拦截器
  1. public class RefreshTokenInterceptor implements HandlerInterceptor {
  2.     private TokenProperties tokenProperties;
  3.     private StringRedisTemplate stringRedisTemplate;
  4.     public RefreshTokenInterceptor(TokenProperties tokenProperties, StringRedisTemplate stringRedisTemplate) {
  5.         this.tokenProperties = tokenProperties;
  6.         this.stringRedisTemplate = stringRedisTemplate;
  7.     }
  8.     @Override
  9.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  10.         // 1.取出请求头中的token
  11.         String token = request.getHeader(tokenProperties.getUserTokenName());
  12.         if(StrUtil.isBlankIfStr(token)) {
  13.             return true;
  14.         }
  15.         // 2.基于token去redis中寻找用户
  16.         UserSafetyDTO userSafetyDTO = RedisOperateUtils.getUserInRedis(token, stringRedisTemplate);
  17.         if (userSafetyDTO == null) {
  18.             return true;
  19.         }
  20.         // 3.存在将用户保存
  21.         UserHolder.saveUser(userSafetyDTO);
  22.         // 4.刷新redis有效期
  23.         stringRedisTemplate.expire(RedisConstant.USER_TOKEN_KEY + token, RedisConstant.USER_TOKEN_TIMEOUT, TimeUnit.MINUTES);
  24.         // 5.放行
  25.         return true;
  26.     }
  27.     @Override
  28.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  29.         UserHolder.removeUser();
  30.     }
  31. }
复制代码
校验是否登录拦截器
  1. public class LoginInterceptor implements HandlerInterceptor {
  2.     @Override
  3.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4.         //1.判断是否需要拦截
  5.         UserSafetyDTO user = UserHolder.getUser();
  6.         if (user == null){
  7.             response.setStatus(401);
  8.             return false;
  9.         }
  10.         //2.有用户放行
  11.         return true;
  12.     }
  13.     @Override
  14.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  15.         UserHolder.removeUser();
  16.     }
  17. }
复制代码
工具类获取方法
  1. /**
  2. * 从redis中获取用户
  3. * @param token token
  4. * @param stringRedisTemplate stringRedisTemplate
  5. * @return 用户安全类
  6. */
  7. public static UserSafetyDTO getUserInRedis(String token, StringRedisTemplate stringRedisTemplate) {
  8.     String userSafetyInfo = stringRedisTemplate.opsForValue().get(RedisConstant.USER_TOKEN_KEY + token);
  9.     if (userSafetyInfo == null || userSafetyInfo.isEmpty()) {
  10.         return null;
  11.     }
  12.     Gson gson = new Gson();
  13.     return gson.fromJson(userSafetyInfo, UserSafetyDTO.class);
  14. }
复制代码
在配置类中加入拦截器
   拦截路径简单示例,可以根据具体环境举行配置
  1. /**
  2. * 开启拦截器配置
  3. * @param registry
  4. */
  5. @Override
  6. public void addInterceptors(InterceptorRegistry registry){
  7.     log.info("开启拦截器配置...");
  8.     // order升序执行
  9.     // 拦截需要刷新token过期时间的路径
  10.     registry.addInterceptor(new RefreshTokenInterceptor(tokenProperties,stringRedisTemplate))
  11.             .addPathPatterns("/user/**").order(0);
  12.     // 拦截需要登录的路径
  13.     registry.addInterceptor(new LoginInterceptor())
  14.             .addPathPatterns("/user/**")
  15.             .excludePathPatterns("/user/info/**")
  16.             .order(1);
  17. }
复制代码
四、后续扩展

​ 本文基于Redis简单实现了token生存,革新等功能,后期另有很大的扩展空间,比如:
引入 JWT 实现更加安全的 Token
利用 JWT(JSON Web Token)代替简单的 UUID Token,可以在 Token 中携带用户的根本信息与权限信息,利用签名技能防止 Token 被窜改。通过 JWT 加密和签名的特性,即便 Token 在传输中被截获,也很难被伪造或窜改,进一步提升安全性。
引入 OAuth2 等鉴权框架
在复杂的体系中,可以思量利用 OAuth2 等标准化的鉴权框架,OAuth2 支持多种授权模式(如授权码模式、密码模式等),可以实现更为细致的权限管理。同时,OAuth2 可以实现跨应用、跨体系的权限共享,适合复杂场景。
利用 Redis Hash 结构优化存储
如果用户信息较多,可以利用 Redis 的 Hash 结构来存储用户信息。这种结构在 Redis 中更高效,可以减少内存占用。比方,可以将每个 Token 对应的用户信息存储在一个 Redis Hash 表中。
利用异步使命清理过期 Token
可以设置定期使命去清理过期的 Token,减少 Redis 中的无效数据,提高性能。比如利用 Spring 的 @Scheduled 注解,定时清理失效的 Token 信息。
支持自定义 Token 革新策略
可以允许用户自定义 Token 革新策略,如设定用户活跃时间,或根据业务需求对不同类型的 Token 设置不同的过期时间。这种方式更加灵活,可以大概适应不同的业务场景需求。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表