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

Spring Security权限认证机制详解 实战

Admin 权限认证机制详解

从小白到精通:深入理解 Spring Security + JWT 双重认证机制

适用人群:Java 后端开发者、系统架构师、安全工程师

技术栈:Spring Boot 2.7.18 + Spring Security 6 + JWT + Redis


📚 目录

  1. 核心概念与技术背景
  2. 整体架构设计
  3. 认证流程全景图
  4. 核心组件详解
  5. 登录流程源码解析
  6. Token验证流程源码解析
  7. 安全机制深度剖析
  8. 如何在其他项目中应用
  9. 常见问题与最佳实践

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 优势
  1. 无状态:服务器不需要保存 Session,适合分布式系统
  2. 跨域友好:可以在不同域名之间传递
  3. 性能好:不需要每次请求都查数据库
  4. 扩展性强: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 管理)

这种设计结合了两者的优点:

特性纯 JWTJWT + Redis
无状态✅ 完全无状态⚠️ 半无状态
可撤销性❌ 无法撤销✅ 删除 Redis 即撤销
并发控制❌ 无法限制✅ 可以踢出旧会话
性能✅ 无需查询存储⚠️ 需查询 Redis(极快)
安全性⚠️ 依赖 Token 安全✅ 双重验证

设计思路

  1. JWT 携带基本信息(用户ID、cacheKey),用于无状态传输
  2. Redis 存储完整用户信息,用于会话管理和撤销
  3. 每次请求验证 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

这是每个请求的守门员,负责:

  1. 从请求中提取 JWT Token
  2. 验证 Token 是否有效
  3. 如果有效,告诉 Spring Security:“这个用户已认证”
4.2.1 为什么继承 OncePerRequestFilter?
public class AuthenticationTokenFilter extends OncePerRequestFilter {// ...
}

OncePerRequestFilter 作用

  • 确保每个请求只执行一次过滤
  • 即使有内部转发(forward),也只执行一次

为什么不用 @Component 注解?

// 注意:不使用@Component注解,避免被Spring自动装配为全局Filter导致重复过滤

如果加了 @Component,Spring 会自动注册这个 Filter,导致它被执行两次:

  1. Spring Security 的 Filter Chain 执行一次
  2. 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);
}

支持两种传递方式

  1. Header(推荐)

    GET /api/program/list
    authorization: eyJhbGciOiJIUzUxMiJ9.eyJzeXNVc2VySWQiOjEwMDAxLCJjYWNoZUtleSI...
    
  2. 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='系统用户表';

为什么分两张表?

  1. 解耦认证与用户信息:一个用户可以有多种登录方式(邮箱、手机号、用户名)
  2. 安全隔离:密码信息单独存储,降低泄露风险
  3. 扩展性强:支持第三方登录(微信、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();
}

什么时候会触发?

  1. 访问受保护接口,但没有提供 Token
  2. Token 验证失败(签名错误、Redis 中不存在等)
  3. 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 注解

  • 自动验证请求参数
  • 如果 usernamepassword 为空,抛出 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 宕机会导致所有用户被踢下线,需要重新登录。

解决方案

  1. Redis 高可用:使用 Redis Sentinel 或 Redis Cluster
  2. 降级策略:Redis 不可用时,暂时允许 JWT 通过(风险较高)
  3. 监控告警:Redis 宕机立即通知运维
Q3: Token 过期时间应该设置多久?

A: 取决于业务场景:

场景Access TokenRefresh Token
内部后台管理系统30分钟(滑动)7天
移动App15分钟(滑动)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 核心要点回顾

  1. JWT + Redis 双重验证

    • JWT 负责无状态传输
    • Redis 负责会话管理和撤销
  2. cacheKey 机制

    • 每次登录生成唯一 UUID
    • JWT 和 Redis 双重验证,防止旧 Token 继续使用
  3. Spring Security 集成

    • Filter Chain 统一处理认证
    • UserDetailsService 连接数据库
    • AuthenticationManager 自动验证密码
  4. 安全机制

    • BCrypt 慢哈希防暴力破解
    • HTTPS 传输防窃听
    • 滑动过期防长期有效

10.2 架构优势

优势说明
可扩展性微服务架构下共享认证,水平扩展容易
安全性双重验证 + 可撤销 + BCrypt 加密
性能Redis 查询极快,支持高并发
灵活性支持多设备、多租户、权限控制
可维护性代码结构清晰,职责分明
http://www.dtcms.com/a/565020.html

相关文章:

  • java每日精进 11.03【基于Spring AOP和事件驱动的资源操作消息处理流程(类似于若依框架的@Log注解)】
  • Spring 从 0 → 1 保姆级笔记:IOC、DI、多配置、Bean 生命周期一次讲透
  • SpringBoot 项目基于责任链模式实现复杂接口的解耦和动态编排
  • Java 入门核心知识点分类学习
  • 叫人做网站后不提供源码商机网创业好项目
  • 【2052】范围判断
  • (1)pytest+Selenium自动化测试-序章
  • 用Python来学微积分29-原函数与不定积分完全指南
  • JavaSE---文件(File)、IO流(基础)
  • 论坛类网站备案吗红色专题网站首页模板
  • 网页设计师主要是做什么的呢深圳seo
  • C++多线程之 安全日志系统
  • 哪里有做效果图的网站wordpress文章内模板
  • Nof1:探索大语言模型作为量化交易者的极限(翻译)
  • 做网站整理信息的表格免费有效的推广网站
  • 基于ASM1042A系列芯片的CAN协议扩展方案在汽车座椅控制器中的应用探讨
  • 超越金融:深入解析STC的“绿色算力网络”与参与机制
  • 【大模型 Tokenizer 核心技术解析】从 BPE 到 Byte-Level 的完整指南
  • 黄岛网站建设价格怎么做自动下单网站
  • 关于我遇到的豆包的bug:mermaid图无法加载
  • Milvus:通过Docker安装Milvus向量数据库(一)
  • 第三方软件测试机构:【“Bug预防”比“Bug发现”更有价值:如何建立缺陷根因分析与流转机制?】
  • Milvus:Schema详解(四)
  • maven的jakarta项目直接运用jetty插件运行
  • 建设外贸网站哪家好网页制作流程视频
  • Java-166 Neo4j 安装与最小闭环 | 10 分钟跑通 + 远程访问 Docker neo4j.conf
  • 如何建立小企业网站wordpress图片上传地址修改
  • 【开题答辩过程】以《基于SpringBoot的中国传统文化推广系统的设计与实现》为例,不会开题答辩的可以进来看看
  • QML笔记
  • Android 在屏幕的右下角添加客户Logo