当前位置: 首页 > news >正文

Redis+JWT 认证管理最佳实践

在单体应用中,通过 Session 或简单的 JWT 即可完成认证与鉴权。但在分布式架构下,用户的请求可能跨越多个服务,认证和鉴权又会带来新的问题:

  • 🔑 认证状态共享:用户登录后,如何让所有微服务识别其身份?
  • 🔄 权限一致性:角色权限变更时,如何实时同步到所有服务?
  • 🔗 跨服务安全:服务间调用(如 Feign、RPC)如何传递身份并鉴权?
  • 🚨 高可用性:认证服务宕机时,如何保证系统仍能安全运行?

认证管理方案分析

Redis 和 JWT 都天然支持分布式场景下的认证管理。

Redis 会话共享

  • 🔵 核心思路:集中存储会话数据,所有服务读取同一Redis集群
  • ✅ 优势:状态完整、支持强制下线、自动过期
  • ⚠️ 注意:Redis需高可用部署,网络延迟影响性能

JWT 无状态

  • 🟢 核心思路:自包含签名令牌,服务本地验签不依赖存储
  • ✅ 优势:无状态、高性能、天然跨域
  • ⚠️ 注意:无法主动失效,需设短期有效期+刷新机制

Redis+JWT

  • 🟠 核心思路:JWT传递基础身份+Redis存储动态权限
  • ✅ 优势:平衡性能与灵活性,支持权限实时更新
  • ⚠️ 注意:需维护两种机制,架构略复杂

差异比对

对比维度Redis会话共享方案JWT无状态方案混合方案
核心原理集中存储会话数据,所有服务读取同一Redis自包含签名令牌,服务本地验签JWT传递身份 + Redis存储动态权限
状态管理有状态完全无状态部分无状态(身份无状态,权限有状态)
实时性即时生效依赖令牌过期身份即时生效,权限可调控(缓存TTL)

Redis+JWT 方案实现

该方案结合 JWT(无状态令牌) 和 Redis(动态状态管理),实现分布式环境下的高效、安全的认证与鉴权:

  • JWT 负责 身份认证,携带用户基础信息(如user_id),由服务端签名,客户端存储并随请求发送。
  • Redis 负责 权限管理,存储动态数据(如roles、权限黑名单、Refresh Token、用户设备绑定),支持实时失效和扩展性。

Redis+JWT 方案解析

整体流程的时序图

在这里插入图片描述

🚀 整个过程分为 3 个核心阶段,每个阶段都有明确的职责和交互逻辑,确保系统既 高性能 又 安全可控。

🔑 阶段 1:用户登录(认证阶段)

📌 目标:验证用户身份,生成 Token,并预加载权限到 Redis。

💡 权限预加载:登录时就把权限存入 Redis,避免每次请求查数据库!

💡 无状态 Token:JWT 自带身份信息,业务服务可 独立验签,不依赖认证中心!

📝 流程步骤

  1. 📲 用户发起登录请求
    • 客户端发送 POST /login,携带 用户名 + 密码(或者其他登录模式信息) 到 认证服务。
  2. 🔍 认证服务校验身份
    • 认证服务查询 数据库,检查账号密码是否正确。
    • ❌ 失败 → 返回 401(登录失败)。
    • ✅ 成功 → 进入下一步!
  3. 🛠️ 生成 JWT + 存储权限
    • 认证服务生成 JWT(包含 userId 等基本信息)。
    • 同时,将用户权限(如 order:readorder:write)存入 Redis(HSET user:{userId}:perms)。
  4. 📤 返回 Token 给客户端
    • JWT 通过网关返回,客户端后续请求需携带它!

📡 阶段 2:业务请求(鉴权阶段)

📌 目标:校验 JWT 有效性,动态检查权限,返回业务数据。

💡 分层校验:先 本地验签(快!),再 动态鉴权(查 Redis)。

📝 流程步骤

  1. 📲 客户端发起业务请求(如 GET /api/orders
    • Authorization 头携带 JWT。
  2. 🔐 业务服务验签
    • 解析 JWT,检查 签名是否有效 + 是否过期。
    • ❌ 验签失败 → 返回 401需登录。
    • ✅ 验签成功 → 进入权限检查!
  3. 🛡️ 权限校验(查 Redis)
    • 业务服务查询 Redis,检查用户是否有权限(如 HGET user:{userId}:perms "order:read")。
    • ❌ 无权限 → 返回 403无权限访问。
    • ✅ 有权限 → 继续执行!
  4. 📊 查询数据并返回

🔄~~ 阶段 3:权限变更(实时同步)(本文不做具体说明和实现)~~

📌 目标:管理员修改权限后,所有服务 实时生效!

📝 流程步骤

  1. 👨‍💻 管理员更新权限
    • 在管理后台修改用户权限(如允许 order:delete)。
  2. 📡 数据库 → 认证服务 → Redis
    • 数据库变更后,通知 认证服务,更新 Redis 权限数据。
  3. 📢 Redis Pub/Sub 通知所有业务服务
    • 通过 发布-订阅 机制,让所有业务服务 清除本地缓存,立即生效新权限!

Redis+JWT 实现

基于 Spring Security 实践之认证鉴权 文中已实现的内容进行 Redis+JWT 的混合方案实现。

登录过程实现

基于 Spring Security 实践之登录 中的实现过程,JwtLoginFilter对登录完成请求过滤,已实现了 SMS 的短信认证,本文中实现 UserPassword的登录认证。
在这里插入图片描述

修改 JwtLoginFilter实现对 用户密码登录的支持,主要为判断前端传输的 loginModel是否为密码登录,在 密码登录的模式下,生成 UserPasswordLoginAuthToken之后,交给 ProviderManager进行 Provider的匹配,匹配到 UserPasswordAuthProvider进行实际的登录操作。

JwtLoginFilter修改内容:

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {public JwtLoginFilter() {// 设置当前 Filter ,也就是登录动作super(new AntPathRequestMatcher("/auth/login", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException {// 规定前端传递登录模式String loginType = request.getParameter("loginType");Authentication authentication = null;// 判断前端使用的登录模式if (CommonConstant.LoginType.SMS.equals(loginType)) {// 手机短信String phone = request.getParameter("phone");String code = request.getParameter("code");authentication = new SmsAuthenticationToken(phone, code);}if (CommonConstant.LoginType.WX.equals(loginType)) {String code = request.getParameter("code");authentication = new WxAuthenticationToken(code);}// 用户名密码登录if (CommonConstant.LoginType.USER_PASSWORD.equals(loginType)) {String username = request.getParameter("username");String password = request.getParameter("password");authentication = new UserPasswordLoginAuthToken(username, password);}if (authentication == null) {throw new UnsupportedLoginTypeException();}return getAuthenticationManager().authenticate(authentication);}}

UserPasswordAuthProvider实现

@Component
public class UserPasswordAuthProvider implements AuthenticationProvider {@Autowiredprivate AbstractLogin abstractLogin;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {UserPasswordLoginAuthToken token = (UserPasswordLoginAuthToken) authentication;// password 暂明文传输至后台String password = authentication.getCredentials().toString();String username = authentication.getPrincipal().toString();try {AuthUserInfo authUserInfo = abstractLogin.userPasswordLogin(username, password);token.setDetails(authentication);} catch (AuthenticationException authenticationException) {throw authenticationException;} catch (Exception e) {throw new LoginFailException();}return token;}@Overridepublic boolean supports(Class<?> authentication) {return UserPasswordLoginAuthToken.class.equals(authentication);}
}

接下来需对 LoginSuccessHandler用户信息缓存 进行方案改写。

改写计划:

  • 双Token方案实现(AccessToken+RefreshToken)
  • AccessToken中存储 UserName 简单信息(30分钟有效期)
  • RefreshToken存储 UserName 简单信息, 长期有效 (12小时)
  • 用户权限信息预加载至 Redis 缓存
  • 返回前端 双 Token 信息
  • 改写原 单Token顺延续期方案,修改 UserTokenCacheUserAuthCache
  • 基于Redis对用户 roles 信息进行存储

基于Redis实现 UserAuthCache

@Component
public class UserAuthCache {private static final Logger LOGGER = LoggerFactory.getLogger(UserAuthCache.class);private static final String AUTH_REDIS_PREFIX = "USER:AUTH::";/*** 超时时间应与 RefreshToken 一致* 此为 12 小时*/private static final long ROLE_TIME_OUT_SECOND = 12 * 60 * 60;@Autowiredprivate RedisTemplate<String, String> redisTemplate;public void setUserAuth(String username, AuthUserInfo userInfo) {ValueOperations<String, String> ops = redisTemplate.opsForValue();ops.set(getUserAuthKey(username), JSON.toJSONString(userInfo), ROLE_TIME_OUT_SECOND, TimeUnit.SECONDS);}public AuthUserInfo getUserAuth(String username) {ValueOperations<String, String> ops = redisTemplate.opsForValue();String rolesJson = ops.get(getUserAuthKey(username));return JSON.parseObject(rolesJson, AuthUserInfo.class);}String getUserAuthKey(String username) {return AUTH_REDIS_PREFIX + username;}}

LoginSuccessHandler中对双 Token 生成 和 角色信息缓存的具体实现

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {/*** accessToken 过期时间 30分钟*/private static final long ACCESS_TOKEN_TIME_OUT_SECOND = 30 * 60;/*** refreshToken 过期时间 12小时*/private static final long REFRESH_TOKEN_TIME_OUT_SECOND = 12 * 60 * 60;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate UserAuthCache userAuthCache;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throws IOException, ServletException {// 生成 token 返回前端AuthUserInfo details = (AuthUserInfo) authentication.getDetails();String username = details.getUsername();String accessToken = jwtUtil.createToken(username, ACCESS_TOKEN_TIME_OUT_SECOND);String refreshToken = jwtUtil.createToken(username, REFRESH_TOKEN_TIME_OUT_SECOND);// 设置 UserAuth 也就是当前登录人信息userAuthCache.setUserAuth(username, details);Map<String, String> tokenMap = new HashMap<>();tokenMap.put("accessToken", accessToken);tokenMap.put("refreshToken", refreshToken);response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);response.getWriter().write(JSON.toJSONString(ApiResult.success(tokenMap)));}
}

至此登录过程已完成实现,包括用户名和密码的登录模式识别匹配、密码校验、双Token生成、角色信息加载和缓存等过程。后续前端携带 accessToken 访问后台接口,后台进行认证鉴权。

鉴权过程实现

在 Spring Security 实践之认证鉴权 中 我们已经完成了 JwtAuthFilter的实现,包括 Token 提取、验证、解析 和 角色信息加载。

本文中需要对其中的 角色信息加载做分离处理,因为在一般的系统请求中 **80% **的都是只验证是否登录,而不验证是否有某种角色权限。

这样的分离设计可以保证 80% 的请求只用CPU级别的JWT解析即可完成,极大的减少了在 单Token中与Redis的网络交互,提升处理性能。

鉴权的具体流程
在这里插入图片描述

  • 网关透传请求
  • 服务提取JWT accessToken
  • 解析 accessToken 获取 username
  • 构建基础的 AuthenticationToken 对象,不携带 角色信息
  • 如果该请求需要进行角色权限判断,再从 Redis 中根据 username 获取角色信息进行动态匹配

在这种分离设计中,与原权限校验不同的是,只有当真正需要鉴权的情况下,才会从 Redis 缓存中加载用户权限信息。

分离认证信息和权限加载的方案有两种,一种是自定义 AccessDecisionVoter 决策投票,另一种是 重写 AbstractAuthenticationTokengetAuthorities 方法。

🔧 复杂方案 - AccessDecisionVoter(不用)

SpringSecurity 已经提供了 AccessDecisionVoter 接口,允许我们实现自定义鉴权逻辑,比如:

  • ✅ 从 Redis 缓存(UserAuthCache)动态加载用户角色
  • ✅ 支持 @PreAuthorize("hasRole('ADMIN')") 注解
  • ✅ 灵活控制权限校验逻辑

**但!我们不采用这种方式! **

原因:实现复杂,维护成本高,需要处理投票逻辑、表达式解析,还要和 Spring Security 的默认机制兼容。

接下来我将要介绍 一种简单粗暴的方案进行实现!!!很简单!!很粗暴!

简单粗暴方案 - 重写 AbstractAuthenticationTokengetAuthorities()(推荐!)

核心思想:

  • 👉 无论 Spring Security 怎么玩权限校验,最终都得调用 AbstractAuthenticationTokengetAuthorities() 拿权限列表!
  • 👉 所以,我们直接“劫持”这个方法,让它按我们的规则返回权限!

CacheUserAuthenticationToken 重写 AbstractAuthenticationToken 的逻辑跟我们一开始说的从 Redis 中根据 username 获取角色信息 一致,步骤包括:

  • setAuthenticated(true) 标记当前 token 为 已认证 状态,否则会返回 401
  • 设计 authorities 权限集合的 多级缓存 (内存+Redis)
  • 获取当前 token 中的已缓存的权限集合 cachedAuthorities ,如果不会空,则返回
  • 如果为空(null or empty),则从 UserAuthCache中获取
  • 将从 UserAuthCache中获取到的权限集合 进行内存缓存至 cachedAuthorities
  • 返回权限集合

优点:

🚀** 超简单:**不用管 Spring Security 的复杂机制,直接控制权限来源!

** 高性能:**JWT+内存缓存减少 Redis 查询,80% 请求不走网络!

🔧** 易维护:**代码清晰,逻辑集中,改权限逻辑不用动整个鉴权流程!

CacheUserAuthenticationToken实现

public class CacheableUserAuthenticationToken extends AbstractAuthenticationToken {private final UserAuthCache userAuthCache;private final UserDetails userDetails;/*** 缓存的 authorities 集合*/private List<GrantedAuthority> cachedAuthorities;CacheableUserAuthenticationToken(UserDetails userDetails, UserAuthCache userAuthCache) {// 初始化一个空的权限集合super(Collections.emptyList());// 默认是已经认证过的 token,如果这一步设为 false 责会标记当前 token 为 未认证setAuthenticated(true);this.userAuthCache = userAuthCache;this.userDetails = userDetails;}/*** 从缓存中获取当前用户的权限集合** @return*/@Overridepublic Collection<GrantedAuthority> getAuthorities() {if (cachedAuthorities == null || cachedAuthorities.isEmpty()) {AuthUserInfo userAuth = userAuthCache.getUserAuth(userDetails.getUsername());if (userAuth != null && userAuth.getRoles() != null) {cachedAuthorities = userAuth.getRoles().stream().map(r -> {return new GrantedAuthority() {@Overridepublic String getAuthority() {return "ROLE_" + r;}};}).collect(Collectors.toList());}}return cachedAuthorities;}@Overridepublic Object getCredentials() {// 凭证为空return null;}@Overridepublic Object getPrincipal() {// 身份信息return userDetails.getUsername();}
}

同时,我们需要修改 JwtAuthFilter,将加载的当前用户的认证对象修改为 CacheableUserAuthenticationToken

@Component
public class JwtAuthFilter extends OncePerRequestFilter {private JwtUtil jwtUtil;private UserAuthCache userAuthCache;public JwtAuthFilter(JwtUtil jwtUtil, UserAuthCache userAuthCache) {this.jwtUtil = jwtUtil;this.userAuthCache = userAuthCache;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {// 1. 从请求头提取TokenString token = getToken(request);if (token != null && jwtUtil.validateToken(token)) {// 2. 构建认证对象Authentication auth = buildAuthentication(token);SecurityContextHolder.getContext().setAuthentication(auth);}// 3. 继续过滤器链chain.doFilter(request, response);}private String getToken(HttpServletRequest request) {String header = request.getHeader("Authorization");if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {return header.substring(7);}return null;}/*** 根据JWT构建Authentication对象* @param token 有效的JWT令牌* @return 已认证的Authentication对象*/public Authentication buildAuthentication(String token) {// 从JWT自定义声明中直接读取权限(推荐无状态方案)UserDetails userDetails = getTokenUser(token);// 构建为使用 userAuthCache 缓存的 AuthenticationToken 对象return new CacheableUserAuthenticationToken(userDetails, userAuthCache);}/*** 从缓存中获取用户权限信息*/private UserDetails getTokenUser(String token) {String username = jwtUtil.parseToken(token);// 直接返回简单 AuthUserInfoAuthUserInfo authUserInfo = new AuthUserInfo();authUserInfo.setUsername(username);authUserInfo.setRoles(Collections.emptyList());return authUserInfo;}
}

至此 Redis+JWT 的认证管理方案已经大功告成。

就是这么简单 🎉🎉🎉🎉🎉

总结

总结下来代码实现的关键点就 **三步 **

  1. 登录后在 Redis 中缓存该用户的权限信息
  2. 后续请求提取 Header 中的 accessToken 获取用户信息,生成一个只带有 username 的 CacheableUserAuthenticationToken
  3. CacheableUserAuthenticationToken中实现 getAuthorities方法,从 Redis 缓存中获取当前用户的权限信息

**邪修就是比正道快啊 **

RefreshToken过程

在这里插入图片描述

  1. 客户端请求
    • 携带 过期AccessToken和 有效RefreshToken
  2. 服务端验证
    • 从Redis查询该用户设备对应的RefreshToken
    • 比对请求中的Token与存储的是否一致
  3. 新旧Token交替
    • 删除旧Token:确保单次有效性
    • 生成新Token对:新的AccessToken + RefreshToken
    • 存储新Token:Redis更新为最新RefreshToken
  4. 失败处理
    • 若Redis中无匹配Token,立即返回401强制重新登录

总结

在系统的认证鉴权设计中,没有放之四海而皆准的"完美方案",只有最适合业务场景的技术组合。Redis+JWT的混合方案,本质上是在无状态灵活性与动态控制力之间找到了最佳平衡点:

  • 当JWT的无状态特性让系统轻装上阵时,Redis为它装上了可控的"刹车系统"
  • 当Redis的实时管理赋予系统敏捷响应能力时,JWT又为其插上了性能的翅膀

这种设计不仅适用于认证鉴权领域,更是架构设计的核心要义——通过解耦与协作,让每个组件发挥最大价值。正如文中展现的:

  • ✅ JWT如身份证——快速证明"你是谁"
  • ✅ Redis如指挥中心——动态决定"你能做什么"

无限进步,进步无限

http://www.lryc.cn/news/603219.html

相关文章:

  • TOPSIS(Technique for Order Preference by Similarity to Ideal Solution )简介与简单示例
  • Ext JS极速项目之 Coworkee
  • 随缘玩 一: 代理模式
  • 算法第29天|动态规划dp2:不同路径、不同路径Ⅱ、整数拆分、不同的二叉搜索树
  • 【图像处理基石】如何对遥感图像进行实例分割?
  • 小白学OpenCV系列1-图像处理基本操作
  • 在 Web3 时代通过自我主权合规重塑 KYC/AML
  • [SWPU2019]Web1
  • 链表反转中最常用的方法————三指针法
  • PHP云原生架构:容器化、Kubernetes与Serverless实践
  • redis【1】
  • 小程序视频播放,与父视图一致等样式设置
  • zama test
  • 百元级工业级核心板:明远智睿×瑞萨V2H,开启AIoT开发新纪元
  • PDF转Word免费工具!批量处理PDF压缩,合并, OCR识别, 去水印, 签名等全功能详解
  • 数据结构之时间复杂度
  • 前端css 的固定布局,流式布局,弹性布局,自适应布局,响应式布局
  • ZKmall开源商城中台架构实践:API 网关与服务治理如何撑起电商技术骨架
  • 在 PolkaVM 上用 Rust 实现 ERC20 合约的全流程开发指南
  • 接口自动化测试pytest框架
  • c++-list
  • 【VOS虚拟操作系统】未来之窗打包工具在前端资源优化中的应用与优势分析——仙盟创梦IDE
  • Redis内存使用耗尽情况分析
  • 40+个常用的Linux指令——下
  • 艾利特机器人:光伏机器人如何重塑清洁能源制造新格局
  • 【CDH】CDH环境中升级ZooKeeper的实战记录
  • 基于KMeans、AgglomerativeClustering、DBSCAN、PCA的聚类分析的区域经济差异研究
  • 【Linux知识】Linux Shell 脚本中的 `set -ex` 命令深度解析
  • 复现cacti的RCE(CVE-2022-46169)
  • Go 客户端玩转 ES|QL API 直连与 Mapping Helpers 实战详解