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 自带身份信息,业务服务可 独立验签,不依赖认证中心!
📝 流程步骤
- 📲 用户发起登录请求
- 客户端发送
POST /login
,携带用户名 + 密码(或者其他登录模式信息)
到 认证服务。
- 客户端发送
- 🔍 认证服务校验身份
- 认证服务查询 数据库,检查账号密码是否正确。
- ❌ 失败 → 返回
401
(登录失败)。 - ✅ 成功 → 进入下一步!
- 🛠️ 生成 JWT + 存储权限
- 认证服务生成 JWT(包含
userId
等基本信息)。 - 同时,将用户权限(如
order:read
、order:write
)存入 Redis(HSET user:{userId}:perms
)。
- 认证服务生成 JWT(包含
- 📤 返回 Token 给客户端
- JWT 通过网关返回,客户端后续请求需携带它!
📡 阶段 2:业务请求(鉴权阶段)
📌 目标:校验 JWT 有效性,动态检查权限,返回业务数据。
💡 分层校验:先 本地验签(快!),再 动态鉴权(查 Redis)。
📝 流程步骤
- 📲 客户端发起业务请求(如
GET /api/orders
)- 在
Authorization
头携带 JWT。
- 在
- 🔐 业务服务验签
- 解析 JWT,检查 签名是否有效 + 是否过期。
- ❌ 验签失败 → 返回
401
需登录。 - ✅ 验签成功 → 进入权限检查!
- 🛡️ 权限校验(查 Redis)
- 业务服务查询 Redis,检查用户是否有权限(如
HGET user:{userId}:perms "order:read"
)。 - ❌ 无权限 → 返回
403
无权限访问。 - ✅ 有权限 → 继续执行!
- 业务服务查询 Redis,检查用户是否有权限(如
- 📊 查询数据并返回
🔄~~ 阶段 3:权限变更(实时同步)(本文不做具体说明和实现)~~
📌 目标:管理员修改权限后,所有服务 实时生效!
📝 流程步骤
- 👨💻 管理员更新权限
- 在管理后台修改用户权限(如允许
order:delete
)。
- 在管理后台修改用户权限(如允许
- 📡 数据库 → 认证服务 → Redis
- 数据库变更后,通知 认证服务,更新 Redis 权限数据。
- 📢 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
顺延续期方案,修改UserTokenCache
为UserAuthCache
- 基于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 决策投票,另一种是 重写 AbstractAuthenticationToken
的 getAuthorities
方法。
🔧 复杂方案 - AccessDecisionVoter
(不用)
SpringSecurity 已经提供了 AccessDecisionVoter
接口,允许我们实现自定义鉴权逻辑,比如:
- ✅ 从 Redis 缓存(
UserAuthCache
)动态加载用户角色 - ✅ 支持
@PreAuthorize("hasRole('ADMIN')")
注解 - ✅ 灵活控制权限校验逻辑
**但!我们不采用这种方式! **❌
原因:实现复杂,维护成本高,需要处理投票逻辑、表达式解析,还要和 Spring Security 的默认机制兼容。
接下来我将要介绍 一种简单粗暴的方案进行实现!!!很简单!!很粗暴!
⚡ 简单粗暴方案 - 重写 AbstractAuthenticationToken
的getAuthorities()
(推荐!)
核心思想:
- 👉 无论 Spring Security 怎么玩权限校验,最终都得调用
AbstractAuthenticationToken
的getAuthorities()
拿权限列表! - 👉 所以,我们直接“劫持”这个方法,让它按我们的规则返回权限!
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 的认证管理方案已经大功告成。
就是这么简单 🎉🎉🎉🎉🎉
总结
总结下来代码实现的关键点就 **三步 **
- 登录后在 Redis 中缓存该用户的权限信息
- 后续请求提取 Header 中的 accessToken 获取用户信息,生成一个只带有 username 的
CacheableUserAuthenticationToken
- 在
CacheableUserAuthenticationToken
中实现getAuthorities
方法,从 Redis 缓存中获取当前用户的权限信息
**邪修就是比正道快啊 **
RefreshToken过程
- 客户端请求
- 携带 过期AccessToken和 有效RefreshToken
- 服务端验证
- 从Redis查询该用户设备对应的RefreshToken
- 比对请求中的Token与存储的是否一致
- 新旧Token交替
- 删除旧Token:确保单次有效性
- 生成新Token对:新的AccessToken + RefreshToken
- 存储新Token:Redis更新为最新RefreshToken
- 失败处理
- 若Redis中无匹配Token,立即返回401强制重新登录
总结
在系统的认证鉴权设计中,没有放之四海而皆准的"完美方案",只有最适合业务场景的技术组合。Redis+JWT的混合方案,本质上是在无状态灵活性与动态控制力之间找到了最佳平衡点:
- 当JWT的无状态特性让系统轻装上阵时,Redis为它装上了可控的"刹车系统"
- 当Redis的实时管理赋予系统敏捷响应能力时,JWT又为其插上了性能的翅膀
这种设计不仅适用于认证鉴权领域,更是架构设计的核心要义——通过解耦与协作,让每个组件发挥最大价值。正如文中展现的:
- ✅ JWT如身份证——快速证明"你是谁"
- ✅ Redis如指挥中心——动态决定"你能做什么"
无限进步,进步无限