后端安全框架:Spring Security + JWT 认证详解

技术基础:Spring Boot 4 + Spring Security 6 + JWT


一、基础配置

SecurityConfiguration 核心组件

组件 说明
PasswordEncoder BCrypt 密码加密
FormAndJsonLoginFilter 登录过滤器
SecurityFilterChain 安全过滤链

配置要点
– CSRF 禁用(前后端分离场景)
– Session 无状态(JWT 方案)
– 白名单配置(登录、刷新、文档等接口)

JWT 配置参数

参数 说明
access-secret 签名密钥
access-expire-seconds 30分钟(access JWT 有效期)
access-refresh-seconds 1天(refresh JWT 有效期)

白名单接口

/v1/auth/login      # 登录
/v1/auth/register   # 注册
/v1/auth/refresh    # 刷新 Token
/v3/api-docs/**     # OpenAPI 文档
/actuator/health    # 健康检查

二、核心概念

Spring Security 认证流程

请求 → JwtAuthFilter → 验证 JWT → 从 Redis 获取 UserDetails → 设置 SecurityContext

两个关键点

  1. JWT 只做身份标识:JWT 里没有 SecurityContext 信息
  2. SecurityContext 只在 Redis 里:JwtAuthFilter 验证 JWT 后,从 Redis 获取 UserDetails 设置到 SecurityContext

三、Filter 执行顺序

JwtAuthFilter → FormAndJsonLoginFilter → UsernamePasswordAuthenticationFilter

四、核心组件

1. JwtAuthFilter

继承 OncePerRequestFilter,核心功能:

  • 检查白名单(跳过不需要认证的接口)
  • 从 Header 获取 Token(Bearer Token)
  • 验证 JWT 签名
  • 从 Redis 获取 UserDetails,设置 SecurityContext

自动延期机制
– 当 access JWT 剩余时间 < 50% 时
– 在 Response Header 中返回新的 access JWT
注意:延期的只是 JWT 的生命周期,Redis TTL 不变

2. FormAndJsonLoginFilter

继承 UsernamePasswordAuthenticationFilter,处理 /v1/auth/login

  • 支持 JSON 请求体
  • 支持表单提交

3. LoginSuccessHandler

实现 AuthenticationSuccessHandler,登录成功后:

  • 生成 JWT Token(userId, username, updateSeq, tokenUUID)
  • 存储到 Redis(TTL = 1天)
  • 返回 JSON 响应(包含 access JWT 和 refresh JWT)

4. LogoutCustomHandler

实现 LogoutHandler,登出时:

  • 删除 Redis Key(用户必须重新登录)
  • 清理 SecurityContext

5. Refresh Token Handler

接收 refreshToken(JWT),验证后:

  • 读取用户表,获取当前 updateSeq
  • 对比 JWT 中的 updateSeq,不一致则返回 401
  • 延期:延长 JWT 的生命周期(不换 tokenUUID)
  • 返回新的 access JWT + refresh JWT

五、角色设计

角色 说明
ROLE_SUPER_ADMIN 超级管理员,拥有所有权限
ROLE_ADMIN 管理员,系统管理
ROLE_VOLUNTEER 志愿者
ROLE_USER 普通用户,基本功能

接口认证注解

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public void admin() {}

@PreAuthorize("isAuthenticated()")
@GetMapping("/user")
public void user() {}

六、JWT 数据结构

字段 说明
userId 用户 ID
username 用户名
updateSeq 秒级时间戳(来自 update_at 字段)
tokenUUID 随机 UUID(同一用户多端登录)

updateSeq 来源:用户的 update_at 字段转为秒级时间戳

两种 JWT

类型 有效期 用途
access JWT 30分钟 每次 API 请求都带
refresh JWT 1天 刷新 token、重新打开浏览器

七、Redis Key 设计

  • Keysecurity:{username}:{updateSeq}:{tokenUUID}
  • 示例security:admin:1743840000123:550e8400-e29b-41d4-a716
  • TTL:1天(access-refresh-seconds)
  • 存储内容:Spring Security UserDetails 子类

为什么 Redis TTL = 1天?
– 减少 Redis key 数量
– refresh JWT 有效期内都能找到用户信息
– 超过 1 天需要重新登录

为什么 JWT 里没有 SecurityContext?

  • JWT 是公开的(base64 可解码),不适合存敏感信息
  • SecurityContext 需要实时从数据库/Redis 获取最新状态
  • 通过 updateSeq 实现旧 token 自动失效

八、Token 延期机制

自动延期(JwtAuthFilter)

  • 触发条件:access JWT 剩余时间 < 50%(< 15分钟)
  • 行为:Response Header 返回新的 access JWT
  • 注意:Redis TTL 不变

主动刷新(Refresh Handler)

  • 触发条件:前端收到 401 或重新打开浏览器
  • 行为:返回新的 access JWT + refresh JWT,Redis TTL 延期

为什么需要自动延期?
– 用户持续使用时无需频繁刷新
– 减少前后端交互
– 保证用户状态定期更新


九、Token 失效场景

场景 失效原因 结果
登出 删除 Redis Key 必须重新登录
强制下线 清理 Redis Key 必须重新登录
密码修改 updateSeq 更新 旧 token 不匹配
access JWT 过期 用 refresh JWT 刷新 获取新 token
refresh JWT 过期 Redis Key 无效 需要重新登录

十、接口示例

10.1 登录 /v1/auth/login

请求

{
  "username": "admin",
  "password": "xxx"
}

响应

{
  "code": 200,
  "data": {
    "accessToken": "xxx",
    "refreshToken": "xxx",
    "expiresIn": 1800
  }
}

10.2 刷新 /v1/auth/refresh

请求

{
  "refreshToken": "xxx"
}

响应

{
  "code": 200,
  "data": {
    "accessToken": "xxx",
    "refreshToken": "xxx",
    "expiresIn": 1800
  }
}

注意:每次刷新都生成新的 JWT,但 tokenUUID 不变

10.3 登出 /v1/auth/logout

响应

{
  "code": 200,
  "message": "登出成功"
}

📝 技术分享,代码改变世界