在单体应用中,通过 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"));
}
@Override
public 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 {
@Autowired
private AbstractLogin abstractLogin;
@Override
public 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;
}
@Override
public 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;
@Autowired
private 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;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserAuthCache userAuthCache;
@Override
public 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
*/
@Override
public 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() {
@Override
public String getAuthority() {
return "ROLE_" + r;
}
};
}
).collect(Collectors.toList());
}
}
return cachedAuthorities;
}
@Override
public Object getCredentials() {
// 凭证为空
return null;
}
@Override
public 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;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 1. 从请求头提取Token
String 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);
// 直接返回简单 AuthUserInfo
AuthUserInfo 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如指挥中心——动态决定"你能做什么"
无限进步,进步无限