Spring Security权限认证机制详解 实战
Admin 权限认证机制详解
从小白到精通:深入理解 Spring Security + JWT 双重认证机制
适用人群:Java 后端开发者、系统架构师、安全工程师
技术栈:Spring Boot 2.7.18 + Spring Security 6 + JWT + Redis
📚 目录
- 核心概念与技术背景
 - 整体架构设计
 - 认证流程全景图
 - 核心组件详解
 - 登录流程源码解析
 - Token验证流程源码解析
 - 安全机制深度剖析
 - 如何在其他项目中应用
 - 常见问题与最佳实践
 
1. 核心概念与技术背景
1.1 什么是认证(Authentication)与授权(Authorization)?
认证(Authentication):验证"你是谁"
- 例如:用户登录时,系统验证用户名和密码是否正确
 
授权(Authorization):验证"你能做什么"
- 例如:普通用户只能查看数据,管理员可以删除数据
 
在 Admin 系统中,主要关注认证机制,即如何确保访问后台管理系统的人是合法的管理员。
1.2 什么是 JWT(JSON Web Token)?
JWT 是一种无状态的认证方案,由三部分组成:
Header.Payload.Signature
 
1.2.1 JWT 结构详解
Header(头部)
{"alg": "HS512",  // 签名算法"typ": "JWT"     // Token 类型
}
 
Payload(载荷)
{"sysUserId": 10001,           // 用户ID"cacheKey": "uuid-string",    // 缓存密钥"created": 1698765432000      // 创建时间戳
}
 
Signature(签名)
HMACSHA512(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret  // 密钥
)
 
1.2.2 JWT 优势
- 无状态:服务器不需要保存 Session,适合分布式系统
 - 跨域友好:可以在不同域名之间传递
 - 性能好:不需要每次请求都查数据库
 - 扩展性强:Payload 可以存储自定义信息
 
1.2.3 JWT 的安全隐患
⚠️ 重要警告:JWT 本身存在安全风险!
- 一旦签发,无法撤销:如果用户密码被修改,旧的 JWT 依然有效
 - 泄露风险:JWT 被盗后,攻击者可以冒充用户
 - 无法控制并发登录:同一账号可以生成多个 JWT 同时使用
 
1.3 什么是 Spring Security?
Spring Security 是一个强大的安全框架,提供:
- 认证(Authentication)
 - 授权(Authorization)
 - 防护常见攻击(CSRF、XSS、Session Fixation 等)
 
1.3.1 Spring Security 核心概念
SecurityContext(安全上下文)
- 存储当前认证用户的信息
 - 类似于 ThreadLocal,每个线程独立
 
Authentication(认证对象)
- 表示一个已认证或待认证的用户
 - 包含用户身份、凭证、权限等
 
UserDetails(用户详情)
- 用户信息的抽象接口
 - 包含用户名、密码、权限、账户状态等
 
AuthenticationManager(认证管理器)
- 负责执行认证逻辑
 - 调用 UserDetailsService 加载用户,验证密码
 
Filter Chain(过滤器链)
- 每个 HTTP 请求都会经过一系列过滤器
 - JWT 认证就是在过滤器中实现的
 
1.4 为什么选择 JWT + Redis 双重验证?
Admin 采用了创新的混合方案:
JWT(Token 传输) + Redis(Session 管理)
 
这种设计结合了两者的优点:
| 特性 | 纯 JWT | JWT + Redis | 
|---|---|---|
| 无状态 | ✅ 完全无状态 | ⚠️ 半无状态 | 
| 可撤销性 | ❌ 无法撤销 | ✅ 删除 Redis 即撤销 | 
| 并发控制 | ❌ 无法限制 | ✅ 可以踢出旧会话 | 
| 性能 | ✅ 无需查询存储 | ⚠️ 需查询 Redis(极快) | 
| 安全性 | ⚠️ 依赖 Token 安全 | ✅ 双重验证 | 
设计思路:
- JWT 携带基本信息(用户ID、cacheKey),用于无状态传输
 - Redis 存储完整用户信息,用于会话管理和撤销
 - 每次请求验证 JWT 中的 
cacheKey与 Redis 中的是否一致 
这样即使 JWT 被盗,只要修改密码或主动登出,Redis 中的 cacheKey 会改变,旧 JWT 立即失效。
2. 整体架构设计
2.1 系统架构图
┌─────────────────────────────────────────────────────────────────┐
│                        客户端(前端)                              │
│  发送请求时在 Header 中携带:authorization: <JWT Token>           │
└────────────────────────────┬────────────────────────────────────┘│▼
┌─────────────────────────────────────────────────────────────────┐
│                    Spring Boot 应用                               │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │         Spring Security Filter Chain                     │    │
│  │  (所有请求必须经过的过滤器链)                           │    │
│  └─────────────────────────────────────────────────────────┘    │
│                             │                                     │
│                             ▼                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  AuthenticationTokenFilter                           │    │
│  │  (自定义 JWT 过滤器 - 在用户名密码验证之前执行)         │    │
│  │                                                           │    │
│  │  1. 从 Header 提取 JWT Token                              │    │
│  │  2. 调用 TokenValidationHelper 验证 Token                 │    │
│  │  3. 验证通过 → 设置 SecurityContext                        │    │
│  │  4. 验证失败 → 继续执行(交给 Spring Security 处理)       │    │
│  └─────────────────────────────────────────────────────────┘    │
│                             │                                     │
│                             ▼                                     │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  WebSecurityConfig                                       │    │
│  │  (Spring Security 核心配置)                             │    │
│  │                                                           │    │
│  │  - 哪些路径需要认证?                                     │    │
│  │  - 哪些路径允许匿名访问?                                 │    │
│  │  - 认证失败如何处理?                                     │    │
│  └─────────────────────────────────────────────────────────┘    │
│                             │                                     │
│                             ▼                                     │
│            ┌────────────────┴────────────────┐                   │
│            │      是否已认证?                 │                   │
│            └────────────────┬────────────────┘                   │
│                   是 │           │ 否                             │
│              ┌───────┘           └───────┐                        │
│              ▼                           ▼                        │
│   ┌──────────────────┐      ┌──────────────────────────┐        │
│   │  放行到 Controller│      │ AuthenticationEntry  │        │
│   │  正常处理业务逻辑 │      │ Point(认证失败处理器)    │        │
│   └──────────────────┘      │ 返回 401 Unauthorized     │        │
│                              └──────────────────────────┘        │
└─────────────────────────────────────────────────────────────────┘
 
2.2 认证组件关系图
┌──────────────────────────────────────────────────────────────────┐
│                          认证核心组件                              │
└──────────────────────────────────────────────────────────────────┘┌─────────────────┐    依赖    ┌─────────────────────┐
│ WebSecurityConfig│ ─────────→ │ Authentication  │
│ (配置类)         │            │ TokenFilter         │
│                 │            │ (JWT过滤器)          │
│ - 配置过滤器链   │            └──────────┬──────────┘
│ - 配置认证管理器 │                       │ 调用
│ - 配置访问规则   │                       ▼
└────────┬────────┘            ┌──────────────────────┐│ 注入                 │ TokenValidation      │▼                      │ Helper               │
┌─────────────────────┐        │ (Token验证工具)       │
│ Authentication  │        │                      │
│ EntryPoint          │        │ 1. 解析JWT           │
│ (认证失败处理器)     │        │ 2. 查询Redis         │
│                     │        │ 3. 验证cacheKey      │
│ - 返回401错误        │        │ 4. 刷新过期时间      │
└─────────────────────┘        └──────────┬───────────┘│ 使用┌────────────────────────────────┤│                                │▼                                ▼
┌─────────────────┐            ┌──────────────────────┐
│ JWTUtils        │            │ AuthTokenRds         │
│ (JWT工具类)      │            │ (Redis操作工具)       │
│                 │            │                      │
│ - 生成Token      │            │ - 存储用户信息        │
│ - 解析Token      │            │ - 查询用户信息        │
│ - 验证签名       │            │ - 删除用户信息        │
└─────────────────┘            │ - 刷新过期时间        │└──────────────────────┘┌────────────────────────────────┘│ 读写▼
┌─────────────────────┐
│ Redis               │
│                     │
│ Key: AUTH_TOKEN_    │
│      {sysUserId}    │
│                     │
│ Value: {           │
│   sysUser: {...},  │
│   cacheKey: "xxx", │
│   credential: "***"│
│ }                  │
│                     │
│ TTL: 30分钟         │
└─────────────────────┘
 
2.3 登录认证组件关系图
┌──────────────────────────────────────────────────────────────────┐
│                          登录流程组件                              │
└──────────────────────────────────────────────────────────────────┘客户端请求
POST /api/auth/validate
{username, password}│▼
┌─────────────────────┐
│ AuthController      │
│ (登录控制器)         │
│                     │
│ @NotAuthorization   │ ← 标记为公开接口,无需认证
└──────────┬──────────┘│ 调用▼
┌─────────────────────────┐
│ AdminAuthServiceImpl    │
│ (认证服务实现)           │
│                         │
│ 登录步骤:               │
│ 1. 创建认证Token         │
│ 2. 调用认证管理器        │
│ 3. 获取用户详情          │
│ 4. 生成cacheKey         │
│ 5. 缓存到Redis          │
│ 6. 生成JWT Token        │
│ 7. 返回给客户端          │
└──────────┬──────────────┘│ 使用▼
┌──────────────────────────┐
│ AuthenticationManager    │
│ (Spring Security认证管理器)│
│                          │
│ authenticate() 方法会:   │
│ 1. 调用UserDetailsService │
│ 2. 加载用户信息           │
│ 3. 验证密码(BCrypt)     │
│ 4. 返回Authentication     │
└──────────┬───────────────┘│ 调用▼
┌────────────────────────────┐
│ UserDetailsService     │
│ (用户详情加载服务)          │
│                            │
│ loadUserByUsername():      │
│ 1. 查询sys_user_auth表     │
│ 2. 验证用户是否存在         │
│ 3. 查询sys_user表          │
│ 4. 验证用户状态            │
│ 5. 返回UserDetails     │
└────────────────────────────┘│▼┌──────────┐│  MySQL   ││  数据库   │└──────────┘
 
3. 认证流程全景图
3.1 完整登录流程(带时序图)
客户端          Controller       Service           AuthManager      UserDetailsService    MySQL      Redis│                │               │                    │                  │              │          ││ POST /login    │               │                    │                  │              │          │├───────────────→│               │                    │                  │              │          ││                │ login(req)    │                    │                  │              │          ││                ├──────────────→│                    │                  │              │          ││                │               │ authenticate()     │                  │              │          ││                │               ├───────────────────→│                  │              │          ││                │               │                    │ loadUserByUsername()            │          ││                │               │                    ├─────────────────→│              │          ││                │               │                    │                  │ SELECT       │          ││                │               │                    │                  ├─────────────→│          ││                │               │                    │                  │ User Data    │          ││                │               │                    │                  │←─────────────┤          ││                │               │                    │ UserDetails      │              │          ││                │               │                    │←─────────────────┤              │          ││                │               │                    │                  │              │          ││                │               │                    │ 验证密码(BCrypt)  │              │          ││                │               │                    │                  │              │          ││                │               │ Authentication     │                  │              │          ││                │               │←───────────────────┤                  │              │          ││                │               │                    │                  │              │          ││                │               │ 生成cacheKey(UUID) │                  │              │          ││                │               │                    │                  │              │          ││                │               │                    │                  │              │ SET      ││                │               │                    │                  │              │ Key-Value││                │               ├───────────────────────────────────────────────────────→│          ││                │               │                    │                  │              │ TTL 30min││                │               │                    │                  │              │          ││                │               │ 生成JWT(含cacheKey)│                  │              │          ││                │               │                    │                  │              │          ││                │ {token, user} │                    │                  │              │          ││                │←──────────────┤                    │                  │              │          ││ 200 OK         │               │                    │                  │              │          ││ {accessToken}  │               │                    │                  │              │          ││←───────────────┤               │                    │                  │              │          ││                │               │                    │                  │              │          │
 
3.2 完整访问流程(带Token验证)
客户端          Filter           Helper        JWTUtils      Redis          Controller│                │               │               │            │               ││ GET /api/xxx   │               │               │            │               ││ Header:        │               │               │            │               ││ authorization  │               │               │            │               │├───────────────→│               │               │            │               ││                │               │               │            │               ││                │ 提取Token      │               │            │               ││                │               │               │            │               ││                │ validateToken()               │            │               ││                ├──────────────→│               │            │               ││                │               │ parseToken()  │            │               ││                │               ├──────────────→│            │               ││                │               │ JWTPayload    │            │               ││                │               │ {sysUserId,   │            │               ││                │               │  cacheKey}    │            │               ││                │               │←──────────────┤            │               ││                │               │               │            │               ││                │               │               │ GET        │               ││                │               │               │ AUTH_TOKEN │               ││                │               ├───────────────────────────→│               ││                │               │               │ UserDetails│               ││                │               │←───────────────────────────┤               ││                │               │               │            │               ││                │               │ 比对cacheKey  │            │               ││                │               │ ✓ 匹配        │            │               ││                │               │               │            │               ││                │               │               │ EXPIRE     │               ││                │               │               │ 刷新30min  │               ││                │               ├───────────────────────────→│               ││                │               │               │            │               ││                │ UserDetails   │               │            │               ││                │←──────────────┤               │            │               ││                │               │               │            │               ││                │ 设置SecurityContext            │            │               ││                │               │               │            │               ││                │ doFilter()    │               │            │               ││                ├──────────────────────────────────────────────────────────→││                │               │               │            │               ││                │               │               │            │   执行业务逻辑││                │               │               │            │               ││                │               │               │            │ 200 OK        ││ 200 OK         │               │               │            │←──────────────┤│ {data}         │               │               │            │               ││←───────────────┤               │               │            │               ││                │               │               │            │               │
 
4. 核心组件详解
4.1 WebSecurityConfig - Spring Security 配置中心
文件路径: auth-admin/src/main/java/com///admin/security/WebSecurityConfig.java
这是整个认证系统的配置中枢,决定了:
- 哪些接口需要认证?
 - 哪些接口允许匿名访问?
 - 使用什么密码加密方式?
 - 如何处理认证失败?
 
4.1.1 核心配置项
@Configuration
@EnableWebSecurity  // 启用 Spring Security
@EnableMethodSecurity(prePostEnabled = true)  // 启用方法级权限控制
public class WebSecurityConfig {// ...
}
 
注解说明:
@Configuration:标记为 Spring 配置类@EnableWebSecurity:启用 Web 安全功能@EnableMethodSecurity:允许在方法上使用@PreAuthorize("hasRole('ADMIN')")等注解
4.1.2 密码加密器配置
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
}
 
BCrypt 加密原理:
- 每次加密结果都不同(因为有随机盐值)
 - 无法解密,只能验证
 - 计算成本高,防暴力破解
 
示例:
String password = "123456";
String hash1 = encoder.encode(password);
// $2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5E
String hash2 = encoder.encode(password);
// $2a$10$M.xmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5F// hash1 != hash2,但都能验证成功
encoder.matches(password, hash1); // true
encoder.matches(password, hash2); // true
 
4.1.3 认证管理器配置
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();
}
 
作用:
- 这是 Spring Security 的认证核心
 - 登录时调用它的 
authenticate()方法 - 它会自动调用 
UserDetailsService加载用户,验证密码 
4.1.4 访问规则配置(逐行解析)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 步骤1: 禁用CSRF(使用JWT不需要).csrf(AbstractHttpConfigurer::disable)// 为什么禁用?// - JWT存储在Header中,不是Cookie// - 恶意网站无法读取和设置Header,天然防CSRF// 步骤2: 启用CORS.cors(cors -> cors.configure(http))// 允许跨域请求(前后端分离必需)// 步骤3: 认证失败处理.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))// 当用户未登录或Token无效时,由AuthenticationEntryPoint处理// 返回401错误和统一的JSON格式// 步骤4: 基于token,不需要session.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// STATELESS: 完全无状态,不创建HttpSession// 为什么?因为我们使用JWT,不需要传统的Session// 步骤5: 请求授权配置.authorizeHttpRequests(auth -> auth// 登录接口允许匿名访问.requestMatchers("/api/auth/validate", "/admin/sysUser/add", "/api/**").permitAll()// OpenAPI接口允许匿名访问(由ApiKeyInterceptor验证).requestMatchers("/openapi/**").permitAll()// Swagger相关接口允许匿名访问.requestMatchers("/swagger-resources/**","/v2/api-docs/**","/v3/api-docs/**","/doc.html/**","/webjars/**","/favicon.ico").permitAll()// 静态资源.requestMatchers("/css/**","/js/**","/images/**","/fonts/**","/**/*.html").permitAll()// 其他所有请求需要认证.anyRequest().authenticated())// 步骤6: 设置AuthenticationProvider.authenticationProvider(authenticationProvider())// 指定认证提供者,关联UserDetailsService和PasswordEncoder// 步骤7: 添加JWT过滤器(关键!).addFilterBefore(new AuthenticationTokenFilter(this.jwtSecret),UsernamePasswordAuthenticationFilter.class)// 在用户名密码过滤器之前执行// 为什么在之前?先尝试JWT认证,如果JWT有效就不需要再走用户名密码流程// 步骤8: 禁用缓存.headers(headers -> headers.cacheControl(cache -> {}));return http.build();
}
 
过滤器执行顺序:
请求 → AuthenticationTokenFilter (JWT验证)→ UsernamePasswordAuthenticationFilter (用户名密码验证)→ ... 其他过滤器→ Controller
 
4.2 AuthenticationTokenFilter - JWT 认证过滤器
文件路径: auth-admin/src/main/java/com///admin/security/AuthenticationTokenFilter.java
这是每个请求的守门员,负责:
- 从请求中提取 JWT Token
 - 验证 Token 是否有效
 - 如果有效,告诉 Spring Security:“这个用户已认证”
 
4.2.1 为什么继承 OncePerRequestFilter?
public class AuthenticationTokenFilter extends OncePerRequestFilter {// ...
}
 
OncePerRequestFilter 作用:
- 确保每个请求只执行一次过滤
 - 即使有内部转发(forward),也只执行一次
 
为什么不用 @Component 注解?
// 注意:不使用@Component注解,避免被Spring自动装配为全局Filter导致重复过滤
 
如果加了 @Component,Spring 会自动注册这个 Filter,导致它被执行两次:
- Spring Security 的 Filter Chain 执行一次
 - Spring Boot 的全局 Filter 执行一次
 
我们在 WebSecurityConfig 中手动添加,所以不需要 @Component。
4.2.2 核心过滤逻辑(逐行解析)
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throws ServletException, IOException {// 步骤1: 调用通用过滤逻辑,验证TokenUserDetails UserDetails = commonFilter(request);// 步骤2: 如果Token无效或不存在,继续执行过滤链if (null == UserDetails) {// 没有有效token,继续执行过滤链// Spring Security会处理未认证的请求chain.doFilter(request, response);return;}// 步骤3: Token有效,设置Spring Security上下文// 创建认证对象UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(UserDetails,  // principal(主体,即用户)null,             // credentials(凭证,已验证所以为null)null              // authorities(权限,暂时为null));// 设置请求详情(IP地址、SessionId等)authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证对象放入Spring Security上下文SecurityContextHolder.getContext().setAuthentication(authentication);// 步骤4: 继续执行过滤链,进入Controllerchain.doFilter(request, response);
}
 
逐行讲解:
第1步:调用 commonFilter
UserDetails UserDetails = commonFilter(request);
 
- 这个方法会提取并验证 Token
 - 如果验证成功,返回用户详情
 - 如果验证失败,返回 null
 
第2步:Token无效的处理
if (null == UserDetails) {chain.doFilter(request, response);return;
}
 
- 为什么继续执行?因为可能是公开接口(如登录接口)
 - Spring Security 会根据配置决定是否拦截
 - 如果访问受保护接口,会被 
AuthenticationEntryPoint拦截,返回 401 
第3步:设置认证上下文
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(UserDetails, null, null);
 
这一步非常关键!告诉 Spring Security:“这个用户已经通过认证了”
参数说明:
- principal(UserDetails):用户对象,后续可以通过 
SecurityContextHolder获取 - credentials(null):凭证(密码),已验证所以不需要了
 - authorities(null):权限列表,暂时为 null(可以根据需要添加)
 
SecurityContextHolder.getContext().setAuthentication(authentication);
 
将认证对象存入 ThreadLocal,这样同一个请求的后续逻辑都能获取到当前用户。
在 Controller 中获取当前用户:
// 方法1: 使用静态方法
UserDetails user = UserDetails.getCurrentUserDetails();// 方法2: 使用 SecurityContextHolder
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails user = (UserDetails) auth.getPrincipal();
 
4.2.3 提取 Token 的逻辑
private UserDetails commonFilter(HttpServletRequest request) {// 从header或parameter中获取iTokenString authToken = request.getHeader(AuthConstants.TOKEN_NAME);if (StringUtils.isEmpty(authToken)) {authToken = request.getParameter(AuthConstants.TOKEN_NAME);}if (StringUtils.isEmpty(authToken)) {return null;}// 验证token并返回用户详情return TokenValidationHelper.validateToken(authToken, this.jwtSecret);
}
 
支持两种传递方式:
-  
Header(推荐)
GET /api/program/list authorization: eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxLCJjYWNoZUtleSI... -  
Query Parameter(备用)
GET /api/program/list?authorization=eyJhbGciOiJIUzUxMiJ9... 
为什么支持参数传递?某些场景下无法设置 Header(如下载文件的 <a> 标签)
4.3 TokenValidationHelper - Token 验证核心
文件路径: auth-admin/src/main/java/com///admin/utils/TokenValidationHelper.java
这是验证逻辑的核心,实现了 JWT + Redis 双重验证。
4.3.1 验证流程(逐行解析)
public static UserDetails validateToken(String authToken, String jwtSecret) {// 步骤0: 空Token检查if (StringUtils.isEmpty(authToken)) {return null; // 放行,交给Spring Security进行验证}try {// ========== 步骤1: 反解析JWT token ==========JWTPayload jwtPayload = JWTUtils.parseToken(authToken, jwtSecret);// token字符串解析失败if (jwtPayload == null || StringUtils.isEmpty(jwtPayload.getCacheKey())) {log.warn("Failed to parse JWT token or missing cacheKey");return null;}// 此时我们得到:// jwtPayload.sysUserId = 10001// jwtPayload.cacheKey = "550e8400-e29b-41d4-a716-446655440000"// jwtPayload.created = 1698765432000// ========== 步骤2: 从Redis获取缓存的用户信息 ==========String tokenKey = String.format(RedisConstant.AUTH_TOKEN, jwtPayload.getSysUserId());// tokenKey = "AUTH_TOKEN_10001"String cachedPayload = AuthTokenRds.me().get(tokenKey);if (null == cachedPayload) {log.warn("Token not found in Redis. userId={}", jwtPayload.getSysUserId());return null;}// cachedPayload = "{\"sysUser\":{...},\"cacheKey\":\"550e8400-...\",\"credential\":\"...\"}"// ========== 步骤3: 解析缓存的payload并验证cacheKey ==========JWTPayload cachedJwtPayload = JSON.parseObject(cachedPayload, JWTPayload.class);// 关键验证:比对cacheKeyif (cachedJwtPayload == null ||!jwtPayload.getCacheKey().equals(cachedJwtPayload.getCacheKey())) {log.warn("CacheKey mismatch. userId={}", jwtPayload.getSysUserId());return null;}// 为什么验证 cacheKey?// 1. 防止旧Token继续使用(修改密码后,cacheKey会变)// 2. 防止并发登录(每次登录生成新的cacheKey,旧的失效)// ========== 步骤4: 续签 - 刷新Redis过期时间 ==========AuthTokenRds.me().expire(tokenKey, AuthConstants.TOKEN_CACHE_TIME);// 每次验证成功,重新设置30分钟过期时间// 这样活跃用户不会频繁掉线// ========== 步骤5: 反序列化完整的UserDetails ==========UserDetails userDetails = JSON.parseObject(cachedPayload, UserDetails.class);log.debug("Token validation successful. userId={}, cacheKey={}",userDetails.getSysUser().getSysUserId(),userDetails.getCacheKey().substring(0, 8) + "...");return userDetails;} catch (Exception e) {log.error("Token validation error", e);return null;}
}
 
4.3.2 为什么需要验证 cacheKey?
这是整个认证机制的安全核心!
场景1:用户修改密码
1. 用户A登录,获得 JWT1(cacheKey=abc)
2. 用户A修改密码
3. 系统生成新的 cacheKey=xyz,存入Redis
4. 用户A用旧的 JWT1 访问
5. JWT1 中的 cacheKey=abc,Redis中的cacheKey=xyz
6. 验证失败!旧Token立即失效
 
场景2:踢出已登录用户
1. 用户A在电脑登录,获得 JWT1(cacheKey=abc)
2. 用户A在手机登录,生成新的 cacheKey=xyz
3. Redis中的cacheKey更新为xyz
4. 电脑上的JWT1携带cacheKey=abc
5. cacheKey不匹配,电脑被踢下线
 
场景3:主动登出
1. 用户A登录,获得 JWT1(cacheKey=abc)
2. 用户A点击登出
3. 系统删除 Redis 中的 AUTH_TOKEN_{userId}
4. 用户A用JWT1访问
5. Redis中查不到数据,验证失败
 
4.3.3 续签机制(Sliding Expiration)
AuthTokenRds.me().expire(tokenKey, AuthConstants.TOKEN_CACHE_TIME);
 
滑动过期策略:
- 每次验证成功,重新设置 30 分钟过期时间
 - 用户持续活跃,永远不会掉线
 - 用户停止操作 30 分钟,自动登出
 
示例时间线:
10:00 - 用户登录,Redis TTL = 10:30
10:15 - 用户访问接口,Redis TTL 刷新为 10:45
10:40 - 用户访问接口,Redis TTL 刷新为 11:10
11:10 - 用户停止操作
11:40 - Redis Key 过期,用户被登出
 
4.4 JWTUtils - JWT 工具类
文件路径: auth-admin/src/main/java/com///admin/security/JWTUtils.java
这个工具类封装了 JWT 的生成和解析逻辑。
4.4.1 生成 Token(逐行解析)
public static String generateToken(JWTPayload jwtPayload, String jwtSecret) {return Jwts.builder()// 1. 设置载荷(Payload).claims(jwtPayload.toMap())// jwtPayload.toMap() 返回:// {//   "sysUserId": 10001,//   "cacheKey": "550e8400-e29b-41d4-a716-446655440000",//   "created": 1698765432000// }// 2. 使用密钥签名(HS512算法).signWith(getSecretKey(jwtSecret))// 这一步生成签名,防止Token被篡改// 3. 压缩并返回.compact();// 返回格式: Header.Payload.Signature// eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxLCJjYWNoZUtleSI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCIsImNyZWF0ZWQiOjE2OTg3NjU0MzIwMDB9.signature...
}
 
关键点:
- 不设置过期时间(
.expiration()):因为过期时间由 Redis 控制 - 使用 HS512 算法:HMAC + SHA512,安全性高
 - 密钥来自配置文件:
.jwt-secret 
4.4.2 解析 Token(逐行解析)
public static JWTPayload parseToken(String token, String secret) {try {// 1. 创建解析器Claims claims = Jwts.parser()// 2. 设置密钥(用于验证签名).verifyWith(getSecretKey(secret))// 3. 构建解析器.build()// 4. 解析Token并验证签名.parseSignedClaims(token)// 5. 获取Payload.getPayload();// 6. 从Claims中提取字段JWTPayload result = new JWTPayload();result.setSysUserId(claims.get("sysUserId", Integer.class));result.setCreated(claims.get("created", Long.class));result.setCacheKey(claims.get("cacheKey", String.class));return result;} catch (Exception e) {// 解析失败(签名无效、格式错误等)return null;}
}
 
可能的异常:
SignatureException:签名不匹配(Token被篡改)MalformedJwtException:Token格式错误ExpiredJwtException:Token过期(但我们没设置过期时间,所以不会抛出)IllegalArgumentException:参数非法
4.4.3 密钥生成
private static SecretKey getSecretKey(String jwtSecret) {// jjwt 0.12+ 需要使用 Keys.hmacShaKeyFor 生成密钥return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
 
为什么不直接使用字符串?
- JJWT 0.12+ 版本要求使用 
SecretKey对象 Keys.hmacShaKeyFor会确保密钥长度满足算法要求- HS512 要求密钥至少 512 位(64 字节)
 
密钥安全建议:
- 至少 64 个字符
 - 包含大小写字母、数字、特殊字符
 - 不要硬编码,使用配置文件
 - 定期更换(更换后旧Token会失效)
 
4.5 UserDetails - 用户详情对象
文件路径: auth-admin/src/main/java/com///admin/security/UserDetails.java
这是 Spring Security 的用户抽象,实现了 UserDetails 接口。
4.5.1 核心字段
@Data
public class UserDetails implements UserDetails {// 系统用户信息(数据库实体)private SysUser sysUser;// 密码凭证(BCrypt加密后的密码)private String credential;// 角色+权限集合(角色必须以ROLE_开头)private Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();// 缓存Key(用于Redis验证)private String cacheKey;// 登录IPprivate String loginIp;
}
 
字段说明:
sysUser:
// 包含用户的基本信息
{"sysUserId": 10001,"loginEmail": "admin@.com","realname": "Admin User","state": 1  // 1-启用, 0-禁用
}
 
credential:
// BCrypt加密后的密码
"$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5E"
 
authorities:
// 权限列表(可扩展)
[new SimpleGrantedAuthority("ROLE_ADMIN"),new SimpleGrantedAuthority("PROGRAM_CREATE"),new SimpleGrantedAuthority("PROGRAM_DELETE")
]
 
cacheKey:
// UUID字符串,用于验证Token有效性
"550e8400-e29b-41d4-a716-446655440000"
 
4.5.2 实现 UserDetails 接口
// Spring Security需要验证的密码
@Override
public String getPassword() {return getCredential();
}// Spring Security登录名
@Override
public String getUsername() {return getSysUser().getSysUserId() + "";
}// 账户是否过期
@Override
public boolean isAccountNonExpired() {return true;  // 永不过期
}// 账户是否已解锁
@Override
public boolean isAccountNonLocked() {return true;  // 永不锁定
}// 密码是否过期
@Override
public boolean isCredentialsNonExpired() {return true;  // 密码永不过期
}// 账户是否启用
@Override
public boolean isEnabled() {return true;  // 根据实际需求,可以改为检查 sysUser.getState()
}
 
为什么都返回 true?
- 简化设计,账户状态由数据库字段 
state控制 - 在 
UserDetailsService.loadUserByUsername()中已经检查了状态 - 如果需要更细粒度的控制,可以修改这些方法
 
4.5.3 获取当前登录用户
public static UserDetails getCurrentUserDetails() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {return (UserDetails) authentication.getPrincipal();}return null;
}
 
使用示例:
@RestController
public class ProgramController {@GetMapping("/api/program/list")public ApiResp<List<Program>> list() {// 获取当前登录用户UserDetails user = UserDetails.getCurrentUserDetails();if (user == null) {throw new ApiServiceException(ApiRespEnum.UNAUTHORIZED_OPERATION);}Integer sysUserId = user.getSysUser().getSysUserId();String email = user.getSysUser().getLoginEmail();// 执行业务逻辑...}
}
 
4.6 UserDetailsService - 用户加载服务
文件路径: auth-admin/src/main/java/com///admin/security/UserDetailsService.java
这是连接数据库与Spring Security的桥梁。
4.6.1 核心方法(逐行解析)
@Override
public UserDetails loadUserByUsername(String loginUsername) throws UsernameNotFoundException {log.info("Loading user by username: {}", loginUsername);// ========== 步骤1: 确定登录方式 ==========// 登录方式:默认邮箱登录(identity_type = 3)Byte identityType = 3;String sysType = "MCH"; // 商户系统// identity_type 说明:// 1 = 手机号登录// 2 = 用户名登录// 3 = 邮箱登录// sysType 说明:// MCH = 商户系统// ADMIN = 管理员系统// ========== 步骤2: 查询认证信息表 ==========SysUserAuth auth = sysUserAuthService.selectByLogin(loginUsername, identityType, sysType);// SQL查询:// SELECT * FROM sys_user_auth// WHERE identifier = 'admin@.com'//   AND identity_type = 3//   AND sys_type = 'MCH'if (null == auth) {log.warn("User not found: {}", loginUsername);throw new ApiServiceException(ApiRespEnum.INVALID_CREDENTIALS);}// auth 对象包含:// {//   "userId": 10001,//   "identifier": "admin@.com",//   "identityType": 3,//   "credential": "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5E...",//   "sysType": "MCH"// }// ========== 步骤3: 查询用户基本信息 ==========SysUser sysUser = sysUserService.getById(auth.getUserId());// SQL查询:// SELECT * FROM sys_user WHERE sys_user_id = 10001if (null == sysUser) {log.warn("SysUser not found for userId: {}", auth.getUserId());throw new ApiServiceException(ApiRespEnum.INVALID_CREDENTIALS);}// sysUser 对象包含:// {//   "sysUserId": 10001,//   "loginEmail": "admin@.com",//   "realname": "Admin User",//   "state": 1,  // 1-启用, 0-禁用//   "createTime": "2025-10-20 10:00:00"// }// ========== 步骤4: 检查用户状态 ==========if (sysUser.getState() != 1) {log.warn("User is disabled: {}", loginUsername);throw new ApiServiceException(ApiRespEnum.ACCOUNT_DISABLED);}log.info("User loaded successfully. userId={}, username={}",sysUser.getSysUserId(), loginUsername);// ========== 步骤5: 返回UserDetails ==========// Spring Security会自动验证密码return new UserDetails(sysUser, auth.getCredential());
}
 
4.6.2 数据库表结构
sys_user_auth 表(认证信息)
CREATE TABLE `sys_user_auth` (`auth_id` int NOT NULL AUTO_INCREMENT,`user_id` int NOT NULL COMMENT '用户ID',`identity_type` tinyint NOT NULL COMMENT '登录类型: 1-手机号, 2-用户名, 3-邮箱',`identifier` varchar(128) NOT NULL COMMENT '登录标识(手机号/邮箱/用户名)',`credential` varchar(128) NOT NULL COMMENT '密码凭证(BCrypt加密)',`sys_type` varchar(16) DEFAULT 'MCH' COMMENT '系统类型: MCH-商户, ADMIN-管理员',PRIMARY KEY (`auth_id`),UNIQUE KEY `uk_identifier` (`identifier`, `identity_type`, `sys_type`)
) COMMENT='系统用户认证表';
 
sys_user 表(用户基本信息)
CREATE TABLE `sys_user` (`sys_user_id` int NOT NULL AUTO_INCREMENT,`login_email` varchar(128) DEFAULT NULL COMMENT '登录邮箱',`realname` varchar(64) DEFAULT NULL COMMENT '真实姓名',`state` tinyint DEFAULT '1' COMMENT '状态: 0-禁用, 1-启用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`sys_user_id`)
) COMMENT='系统用户表';
 
为什么分两张表?
- 解耦认证与用户信息:一个用户可以有多种登录方式(邮箱、手机号、用户名)
 - 安全隔离:密码信息单独存储,降低泄露风险
 - 扩展性强:支持第三方登录(微信、Google等)
 
4.7 AuthenticationEntryPoint - 认证失败处理器
文件路径: auth-admin/src/main/java/com///admin/security/AuthenticationEntryPoint.java
当用户未登录或Token无效时,这个类负责返回错误响应。
4.7.1 核心逻辑(逐行解析)
@Override
public void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throws IOException, ServletException {// 步骤1: 记录日志log.warn("Authentication failed. uri={}, error={}",request.getRequestURI(), authException.getMessage());// 步骤2: 设置响应头response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  // 401// 步骤3: 返回统一的API响应ApiResp result = ApiResp.create(ApiRespEnum.UNAUTHORIZED_OPERATION);// {//   "code": 80000002,//   "message": "Unauthorized operation",//   "data": null// }response.getWriter().write(JSON.toJSONString(result));response.getWriter().flush();
}
 
什么时候会触发?
- 访问受保护接口,但没有提供 Token
 - Token 验证失败(签名错误、Redis 中不存在等)
 - Token 中的 cacheKey 与 Redis 不匹配
 
为什么不抛出异常?
- 这是 Spring Security 的最后一道防线
 - 如果这里抛异常,Spring Security 会返回默认的 HTML 错误页面
 - 我们需要返回 JSON 格式的响应,便于前端处理
 
5. 登录流程源码解析
5.1 登录接口(AuthController)
文件路径: auth-admin/src/main/java/com///admin/controller/AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate AdminAuthService adminAuthService;/*** 管理员登录*/@NotAuthorization  // 关键:标记为公开接口,无需认证@Operation(summary = "Admin Login")@PostMapping("/validate")public ApiResp<AuthValidateRes> validate(@Validated @RequestBody AuthValidateReq req) {AuthValidateRes login = adminAuthService.login(req);return ApiResp.create(login);}
}
 
@NotAuthorization 注解:
- 虽然这个注解存在,但在 Spring Security 中不起作用
 - 真正的公开配置在 
WebSecurityConfig.requestMatchers("/api/auth/validate").permitAll() 
@Validated 注解:
- 自动验证请求参数
 - 如果 
username或password为空,抛出BindException GlobalExceptionHandler会捕获并返回友好的错误信息
5.2 登录服务(AdminAuthServiceImpl)
文件路径: auth-admin/src/main/java/com///admin/service/impl/AdminAuthServiceImpl.java
这是登录的核心逻辑,分为 8 个步骤。
步骤1: 参数校验
String username = req.getUsername();
String password = req.getPassword();if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {throw new ApiServiceException(ApiRespEnum.PARAM_ERROR,"Username and password cannot be empty");
}
 
虽然 @Validated 已经验证过,这里再次检查是防御性编程。
步骤2: 创建认证 Token
UsernamePasswordAuthenticationToken upToken =new UsernamePasswordAuthenticationToken(username, password);
 
这个 Token 不是 JWT!它是 Spring Security 内部的认证对象,包含:
- principal:用户名(此时还未认证)
 - credentials:密码(明文)
 - authenticated:false(未认证状态)
 
步骤3: 调用认证管理器
Authentication authentication;
try {authentication = authenticationManager.authenticate(upToken);
} catch (InternalAuthenticationServiceException e) {if (e.getCause() instanceof ApiServiceException) {throw (ApiServiceException) e.getCause();} else {log.error("Authentication error", e);throw new ApiServiceException(ApiRespEnum.AUTHENTICATION_FAILED);}
} catch (BadCredentialsException e) {log.warn("Bad credentials for username: {}", username);throw new ApiServiceException(ApiRespEnum.INVALID_CREDENTIALS);
} catch (Exception e) {log.error("Authentication exception", e);throw new ApiServiceException(ApiRespEnum.AUTHENTICATION_FAILED);
}
 
authenticationManager.authenticate() 内部流程:
1. 调用 UserDetailsService.loadUserByUsername(username)↓
2. 查询数据库,获取用户信息和加密密码↓
3. 使用 BCryptPasswordEncoder.matches(password, hashedPassword)↓
4. 如果密码正确,返回 Authentication 对象(authenticated=true)↓
5. 如果密码错误,抛出 BadCredentialsException
 
异常处理说明:
-  
InternalAuthenticationServiceException:
loadUserByUsername()中抛出的异常会被包装成这个异常- 我们在 
loadUserByUsername()中抛出了ApiServiceException - 需要解包并重新抛出
 
 -  
BadCredentialsException:
- 密码错误
 - 返回 
INVALID_CREDENTIALS错误码 
 
步骤4: 获取认证后的用户详情
UserDetails UserDetails = (UserDetails) authentication.getPrincipal();
SysUser sysUser = UserDetails.getSysUser();log.info("Admin login successful. userId={}, username={}",sysUser.getSysUserId(), username);
 
此时的 authentication 对象:
{"principal": UserDetails {"sysUser": {...},"credential": "$2a$10$...","authorities": []},"credentials": null,  // 已清空"authenticated": true
}
 
步骤5: 生成 cacheKey
UserDetails.setCacheKey(IdUtil.fastUUID());
 
IdUtil.fastUUID():
- 来自 Hutool 工具库
 - 生成不带横线的 UUID
 - 例如:
550e8400e29b41d4a716446655440000 
为什么用 UUID?
- 全局唯一,防止冲突
 - 随机性强,无法预测
 - 每次登录生成新的,旧 Token 自动失效
 
步骤6: 缓存用户详情到 Redis
AuthTokenRds.me().set(String.format(RedisConstant.AUTH_TOKEN, sysUser.getSysUserId()),JSON.toJSONString(UserDetails),AuthConstants.TOKEN_CACHE_TIME
);
 
Redis 存储结构:
Key: AUTH_TOKEN_10001
Value: {"sysUser": {"sysUserId": 10001,"loginEmail": "admin@.com","realname": "Admin User","state": 1},"credential": "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5E...","cacheKey": "550e8400e29b41d4a716446655440000","authorities": []
}
TTL: 1800秒 (30分钟)
 
为什么存完整对象?
- Token 验证时直接反序列化,不需要查数据库
 - 包含 cacheKey,用于双重验证
 - 包含 credential,虽然用不到,但保持数据完整性
 
步骤7: 设置 Spring Security 上下文
UsernamePasswordAuthenticationToken authenticationRest =new UsernamePasswordAuthenticationToken(UserDetails, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationRest);
 
为什么要设置?
- 虽然登录接口不需要,但如果后续调用其他方法,可以获取当前用户
 - 统一处理方式,避免遗漏
 
步骤8: 生成 JWT Token
String jwtToken = JWTUtils.generateToken(new JWTPayload(UserDetails), jwtSecret);log.info("JWT token generated. userId={}, cacheKey={}",sysUser.getSysUserId(), UserDetails.getCacheKey().substring(0, 8) + "...");return AuthValidateRes.builder().accessToken(jwtToken).sysUserId(sysUser.getSysUserId()).username(sysUser.getLoginEmail()).build();
 
JWTPayload 构造:
public JWTPayload(UserDetails UserDetails) {this.setSysUserId(UserDetails.getSysUser().getSysUserId());this.setCreated(System.currentTimeMillis());this.setCacheKey(UserDetails.getCacheKey());
}
 
返回给客户端:
{"code": 0,"message": "success","data": {"accessToken": "eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxLCJjYWNoZUtleSI6IjU1MGU4NDAwZTI5YjQxZDRhNzE2NDQ2NjU1NDQwMDAwIiwiY3JlYXRlZCI6MTY5ODc2NTQzMjAwMH0.signature...","sysUserId": 10001,"username": "admin@.com"}
}
 
5.3 登录流程完整示例
请求:
curl -X POST http://localhost:8081/api/auth/validate \-H "Content-Type: application/json" \-d '{"username": "admin@.com","password": "123456"}'
 
执行流程:
1. Spring Security 检查 /api/auth/validate 是否允许匿名访问✓ 配置中有 .permitAll(),放行2. 进入 AuthController.validate()↓
3. 调用 AdminAuthServiceImpl.login()↓
4. 创建 UsernamePasswordAuthenticationToken↓
5. authenticationManager.authenticate()├─ 调用 UserDetailsService.loadUserByUsername()├─ 查询数据库: SELECT * FROM sys_user_auth WHERE identifier='admin@.com'├─ 查询数据库: SELECT * FROM sys_user WHERE sys_user_id=10001├─ BCrypt 验证密码└─ 返回 Authentication(authenticated=true)↓
6. 生成 cacheKey = "550e8400e29b41d4a716446655440000"↓
7. 存入 Redis:Key: AUTH_TOKEN_10001Value: {"sysUser":{...}, "cacheKey":"550e8400e29b41d4a716446655440000"}TTL: 1800秒↓
8. 生成 JWT Token:Header: {"alg":"HS512","typ":"JWT"}Payload: {"sysUserId":10001,"cacheKey":"550e8400e29b41d4a716446655440000","created":1698765432000}Signature: HMACSHA512(header + payload, secret)↓
9. 返回响应:{"code": 0,"data": {"accessToken": "eyJhbGciOiJIUzUxMiJ9...","sysUserId": 10001,"username": "admin@.com"}}
 
6. Token验证流程源码解析
6.1 访问受保护接口
假设我们访问一个受保护的接口:
curl -X GET http://localhost:8081/api/program/list \-H "authorization: eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxLCJjYWNoZUtleSI6IjU1MGU4NDAwZTI5YjQxZDRhNzE2NDQ2NjU1NDQwMDAwIiwiY3JlYXRlZCI6MTY5ODc2NTQzMjAwMH0.signature..."
 
6.2 执行流程
1. 请求到达 Spring Boot↓
2. 进入 Spring Security Filter Chain↓
3. AuthenticationTokenFilter.doFilterInternal()├─ 提取 Header: authorization├─ authToken = "eyJhbGciOiJIUzUxMiJ9..."├─ 调用 TokenValidationHelper.validateToken()│  ├─ JWTUtils.parseToken() 解析 JWT│  │  ├─ 验证签名│  │  ├─ 提取 Payload│  │  └─ 返回 JWTPayload {sysUserId:10001, cacheKey:"550e8400..."}│  ├─ Redis.get("AUTH_TOKEN_10001")│  │  └─ 返回 {"sysUser":{...}, "cacheKey":"550e8400e29b41d4a716446655440000"}│  ├─ 比对 JWT 中的 cacheKey 与 Redis 中的 cacheKey│  │  └─ ✓ 匹配│  ├─ Redis.expire("AUTH_TOKEN_10001", 1800)  // 续签│  └─ 返回 UserDetails├─ 创建 UsernamePasswordAuthenticationToken├─ SecurityContextHolder.setAuthentication()└─ chain.doFilter()  // 继续执行↓
4. 进入 Controller├─ UserDetails.getCurrentUserDetails()  // 可以获取当前用户└─ 执行业务逻辑↓
5. 返回响应
 
6.3 验证失败场景
场景1: Token 缺失
curl -X GET http://localhost:8081/api/program/list
# 没有 authorization header
 
执行流程:
AuthenticationTokenFilter↓
commonFilter() 返回 null (没有 Token)↓
UserDetails == null↓
继续执行 chain.doFilter()↓
Spring Security 检查 SecurityContext↓
没有 Authentication 对象↓
AuthenticationEntryPoint.commence()↓
返回 401 Unauthorized
 
响应:
{"code": 80000002,"message": "Unauthorized operation","data": null
}
 
场景2: Token 签名错误
curl -X GET http://localhost:8081/api/program/list \-H "authorization: eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxfQ.FAKE_SIGNATURE"
 
执行流程:
AuthenticationTokenFilter↓
TokenValidationHelper.validateToken()↓
JWTUtils.parseToken()├─ Jwts.parser().parseSignedClaims(token)└─ 抛出 SignatureException (签名不匹配)↓
catch (Exception e) { return null; }↓
返回 null↓
继续执行,最终返回 401
 
场景3: Redis 中没有记录
# 用户登录后,管理员手动删除了 Redis Key
redis-cli
> DEL AUTH_TOKEN_10001
 
执行流程:
TokenValidationHelper.validateToken()↓
JWTUtils.parseToken() 成功↓
Redis.get("AUTH_TOKEN_10001") 返回 null↓
log.warn("Token not found in Redis")↓
返回 null↓
最终返回 401
 
场景4: cacheKey 不匹配
# 用户在电脑登录后,又在手机登录
# 手机登录时生成新的 cacheKey,Redis 被更新
# 电脑上的旧 Token 中携带旧的 cacheKey
 
执行流程:
TokenValidationHelper.validateToken()↓
JWTPayload (从Token中): cacheKey = "old-uuid"↓
CachedPayload (从Redis中): cacheKey = "new-uuid"↓
!jwtPayload.getCacheKey().equals(cachedJwtPayload.getCacheKey())↓
log.warn("CacheKey mismatch")↓
返回 null↓
最终返回 401
 
7. 安全机制深度剖析
7.1 防止 Token 被盗用
7.1.1 HTTPS 传输
问题:Token 在网络传输时可能被截获
解决方案:
- 生产环境必须使用 HTTPS
 - HTTPS 加密整个通信过程,防止中间人攻击
 
7.1.2 Token 存储位置
前端应该把 Token 存在哪里?
| 存储位置 | 安全性 | 说明 | 
|---|---|---|
| LocalStorage | ⚠️ 中等 | 容易被 XSS 攻击窃取 | 
| SessionStorage | ⚠️ 中等 | 关闭标签页即失效,但仍可被 XSS 窃取 | 
| Cookie (HttpOnly) | ✅ 高 | 无法被 JavaScript 读取,防XSS | 
| 内存 | ✅ 最高 | 刷新页面即失效 | 
推荐方案:
// 使用 HttpOnly Cookie
// 后端设置
response.setHeader("Set-Cookie","accessToken=" + token + "; HttpOnly; Secure; SameSite=Strict");// 前端无需手动传递,浏览器会自动携带
 
或者:
// 使用内存 + LocalStorage 组合
// 敏感操作的 Token 存内存,普通 Token 存 LocalStorage
const tokenManager = {sensitiveToken: null,  // 内存normalToken: localStorage.getItem('token')  // 持久化
};
 
7.1.3 cacheKey 双重验证
即使 Token 被盗,只要用户执行以下操作,盗取的 Token 立即失效:
- 修改密码
 - 重新登录
 - 主动登出
 
实现原理:
// 修改密码时
public void changePassword(Integer userId, String newPassword) {// 1. 更新数据库密码userAuthService.updatePassword(userId, newPassword);// 2. 删除 Redis 中的缓存(关键!)AuthTokenRds.me().del(String.format(RedisConstant.AUTH_TOKEN, userId));// 3. 旧的 JWT Token 虽然还存在,但验证时会失败(Redis中查不到)
}
 
7.2 防止暴力破解
7.2.1 BCrypt 慢哈希
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
}
 
BCrypt 特点:
- 计算耗时约 100ms(可调节)
 - 暴力破解成本极高
 - 自动加盐,每次结果不同
 
示例:
// 登录验证耗时约 100ms
boolean matches = encoder.matches("123456", hashedPassword);// 如果攻击者尝试 1000 个密码,需要:
// 1000 × 100ms = 100秒
// 如果密码复杂度高,破解成本呈指数增长
 
7.2.2 登录失败次数限制(建议实现)
// 伪代码(需要自己实现)
public void login(String username, String password) {// 检查失败次数String key = "LOGIN_FAIL_COUNT_" + username;Integer failCount = Redis.get(key);if (failCount != null && failCount >= 5) {throw new ApiServiceException(ApiRespEnum.ACCOUNT_LOCKED,"Too many failed attempts, please try again after 30 minutes");}try {// 执行认证authenticationManager.authenticate(...);// 成功,清除失败次数Redis.del(key);} catch (BadCredentialsException e) {// 失败,增加计数Redis.incr(key);Redis.expire(key, 1800);  // 30分钟后重置throw e;}
}
 
7.3 防止 CSRF 攻击
7.3.1 什么是 CSRF?
跨站请求伪造(CSRF):
1. 用户登录银行网站 bank.com,获得 Cookie
2. 用户访问恶意网站 evil.com
3. evil.com 页面中有一个隐藏表单:<form action="https://bank.com/transfer" method="POST"><input name="to" value="attacker"><input name="amount" value="10000"></form><script>document.forms[0].submit()</script>
4. 浏览器自动携带 bank.com 的 Cookie,转账成功
 
7.3.2 JWT 如何防止 CSRF?
.csrf(AbstractHttpConfigurer::disable)
 
为什么禁用 CSRF 防护?
- JWT 存储在 Header 中,不是 Cookie
 - 恶意网站无法读取和设置 Header
 - 即使伪造请求,也无法获取 JWT Token
 
如果使用 Cookie 存储 JWT,必须启用 CSRF 防护:
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
 
8. 如何在其他项目中应用
8.1 快速集成步骤
步骤1: 添加依赖
<!-- Spring Boot Starter -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.1.5</version>
</dependency><!-- JWT -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.3</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.3</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.3</version><scope>runtime</scope>
</dependency><!-- Redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!-- Lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
 
步骤2: 配置文件
# application.yml
spring:# Redis配置redis:host: localhostport: 6379password:database: 0timeout: 3000jedis:pool:max-active: 8max-wait: -1max-idle: 8min-idle: 0# JWT配置
:jwt-secret: your-secret-key-must-be-at-least-64-characters-long-for-hs512-algorithm
 
步骤3: 复制核心类
从 项目中复制以下文件到你的项目:
src/main/java/your/package/
├── security/
│   ├── WebSecurityConfig.java               # Spring Security配置
│   ├── AuthenticationTokenFilter.java   # JWT过滤器
│   ├── AuthenticationEntryPoint.java    # 认证失败处理
│   ├── UserDetails.java                 # 用户详情
│   ├── UserDetailsService.java          # 用户加载服务
│   ├── JWTUtils.java                        # JWT工具
│   ├── JWTPayload.java                      # JWT载荷
│   └── AuthConstants.java                   # 常量
├── utils/
│   └── TokenValidationHelper.java           # Token验证
└── service/└── AdminAuthServiceImpl.java            # 认证服务
 
步骤4: 修改适配
修改 UserDetailsService:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// TODO: 根据你的数据库表结构修改查询逻辑User user = userRepository.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("User not found");}if (!user.isEnabled()) {throw new DisabledException("User is disabled");}return new UserDetails(user, user.getPassword());
}
 
修改 WebSecurityConfig:
.authorizeHttpRequests(auth -> auth// 修改为你的公开接口.requestMatchers("/api/auth/login", "/api/auth/register").permitAll().anyRequest().authenticated()
)
 
9. 常见问题与最佳实践
9.1 常见问题
Q1: 为什么用 JWT + Redis,不直接用 JWT?
A: 纯 JWT 无法撤销,存在安全隐患:
- 用户修改密码后,旧 Token 依然有效
 - 无法踢出已登录用户
 - 无法限制并发登录数量
 
使用 Redis 后,可以随时撤销 Token,安全性大幅提升。
Q2: Redis 挂了怎么办?
A: Redis 宕机会导致所有用户被踢下线,需要重新登录。
解决方案:
- Redis 高可用:使用 Redis Sentinel 或 Redis Cluster
 - 降级策略:Redis 不可用时,暂时允许 JWT 通过(风险较高)
 - 监控告警:Redis 宕机立即通知运维
 
Q3: Token 过期时间应该设置多久?
A: 取决于业务场景:
| 场景 | Access Token | Refresh Token | 
|---|---|---|
| 内部后台管理系统 | 30分钟(滑动) | 7天 | 
| 移动App | 15分钟(滑动) | 30天 | 
| 敏感操作(支付) | 5分钟(绝对) | 不使用 | 
9.2 最佳实践
1. 密钥管理
❌ 不要这样做:
private static final String JWT_SECRET = "my-secret-key";
 
✅ 应该这样做:
# application.yml
:jwt-secret: ${JWT_SECRET:default-secret-for-development}# 生产环境通过环境变量传递
export JWT_SECRET="production-secret-key-very-long-and-random"
 
2. 异常处理
❌ 不要暴露敏感信息:
throw new ApiServiceException("User not found in database: " + username);
 
✅ 使用通用错误信息:
// 登录失败统一返回 "用户名或密码错误"
// 不要区分 "用户不存在" 和 "密码错误"
throw new ApiServiceException(ApiRespEnum.INVALID_CREDENTIALS);
 
3. 日志记录
✅ 记录关键操作:
log.info("User login successful. userId={}, ip={}", userId, ip);
log.warn("Login failed. username={}, ip={}, reason={}", username, ip, reason);
 
❌ 不要记录敏感信息:
log.info("User login: username={}, password={}", username, password);  // 危险!
log.debug("JWT token: {}", token);  // 不要记录完整 Token
 
10. 总结
10.1 核心要点回顾
-  
JWT + Redis 双重验证
- JWT 负责无状态传输
 - Redis 负责会话管理和撤销
 
 -  
cacheKey 机制
- 每次登录生成唯一 UUID
 - JWT 和 Redis 双重验证,防止旧 Token 继续使用
 
 -  
Spring Security 集成
- Filter Chain 统一处理认证
 - UserDetailsService 连接数据库
 - AuthenticationManager 自动验证密码
 
 -  
安全机制
- BCrypt 慢哈希防暴力破解
 - HTTPS 传输防窃听
 - 滑动过期防长期有效
 
 
10.2 架构优势
| 优势 | 说明 | 
|---|---|
| 可扩展性 | 微服务架构下共享认证,水平扩展容易 | 
| 安全性 | 双重验证 + 可撤销 + BCrypt 加密 | 
| 性能 | Redis 查询极快,支持高并发 | 
| 灵活性 | 支持多设备、多租户、权限控制 | 
| 可维护性 | 代码结构清晰,职责分明 | 
