Files
workspace/code/fms/.qoder/repowiki/zh/content/认证授权系统/JWT认证机制.md
T
2026-08-23 21:02:41 +08:00

17 KiB
Raw Blame History

JWT认证机制

**本文引用的文件** - [JwtAuthFilter.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java) - [JwtUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java) - [AuthService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java) - [ActiveSessionRegistry.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java) - [OrgContext.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java) - [application.yaml](file://fms-api/src/main/resources/application.yaml)

目录

  1. 简介
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能与安全建议
  8. 故障排查指南
  9. 结论
  10. 附录:前端Token存储与自动刷新方案

简介

本技术文档聚焦FMS系统的JWT认证机制,系统性阐述令牌的生成、解析与验证流程,过滤器拦截逻辑,多设备会话控制,以及多租户上下文传递。同时给出配置项说明、安全最佳实践与前端令牌存储和自动刷新策略建议,帮助开发者快速理解并正确集成。

项目结构

后端采用Spring Boot + JJWT实现无状态鉴权,关键模块如下:

  • 认证过滤器:请求级拦截,提取并校验Authorization头中的Bearer Token
  • 工具类:负责JWT签名、验签、载荷解析
  • 认证服务:登录成功后签发Token并登记活跃会话
  • 会话注册表:维护当前账号的活跃会话(支持单设备或最后登录设备)
  • 多租户上下文:通过ThreadLocal在请求链路中传递orgId与userId
graph TB
Client["客户端"] --> Filter["JwtAuthFilter<br/>请求拦截与校验"]
Filter --> Utils["JwtUtils<br/>解析与验签"]
Filter --> Registry["ActiveSessionRegistry<br/>会话有效性检查"]
Filter --> Ctx["OrgContext<br/>设置多租户上下文"]
AuthService["AuthService<br/>登录与签发Token"] --> Utils
AuthService --> Registry

图示来源

章节来源

核心组件

  • JwtAuthFilter:统一拦截所有非登录且非OPTIONS请求,从Authorization头提取Bearer Token,调用JwtUtils解析并校验;随后校验会话是否活跃且未被顶替;将orgId与userId注入到OrgContext供后续业务使用。
  • JwtUtils:基于HMAC-SHA密钥对Token进行签名与验签;生成包含用户ID、机构码、会话ID、签发时间、过期时间的紧凑Token;提供载荷解析方法。
  • AuthService:完成用户名密码校验后,生成Token并登记活跃会话,返回包含token、orgid和用户信息的响应。
  • ActiveSessionRegistry:以orgId+userId为键维护当前活跃sessionId,用于“最后登录设备”策略或单设备登录控制。
  • OrgContext:线程安全的上下文容器,保存当前请求的orgId与userId,确保多租户数据隔离与审计。

章节来源

架构总览

下图展示了从登录到受保护接口访问的完整时序:

sequenceDiagram
participant FE as "前端"
participant AuthS as "AuthService"
participant Utils as "JwtUtils"
participant Reg as "ActiveSessionRegistry"
participant Filter as "JwtAuthFilter"
participant Ctx as "OrgContext"
FE->>AuthS : "POST /auth/login(用户名, 密码)"
AuthS->>AuthS : "校验账号与密码"
AuthS->>Utils : "generateToken(userId, orgId, sessionId)"
Utils-->>AuthS : "返回token"
AuthS->>Reg : "activate(orgId, userId, sessionId)"
AuthS-->>FE : "{token, orgid, user}"
FE->>Filter : "GET /api/... (Authorization : Bearer token)"
Filter->>Utils : "parseToken(token)"
Utils-->>Filter : "TokenClaims(userId, orgId, sessionId)"
Filter->>Reg : "hasActiveSession(orgId, userId)"
Reg-->>Filter : "true/false"
alt 未激活
Filter-->>FE : "401 未登录或已失效"
else 已激活
Filter->>Reg : "isActive(orgId, userId, sessionId)"
Reg-->>Filter : "true/false"
alt 被顶替
Filter-->>FE : "401 账号已在其他设备登录"
else 有效
Filter->>Ctx : "setOrgId/setUserId"
Filter-->>FE : "继续处理请求"
Note over Ctx : "业务层可读取上下文进行多租户隔离"
end
end

图示来源

详细组件分析

JwtAuthFilter 过滤器工作原理

  • 请求拦截:跳过OPTIONS预检与登录路径,避免重复鉴权。
  • 令牌解析:从Authorization头提取Bearer Token,调用JwtUtils.parseToken进行签名校验与载荷解析。
  • 会话校验:先检查该账号是否存在活跃会话,再校验当前sessionId是否与活跃会话一致,实现“最后登录设备”或“单设备登录”控制。
  • 上下文注入:将orgId与userId写入OrgContext,并在finally中清理,保证线程安全。
  • 错误处理:对缺失凭证、解析失败、会话无效等场景统一返回结构化错误响应。
flowchart TD
Start(["进入过滤器"]) --> Skip{"是否OPTIONS或登录路径?"}
Skip --> |是| Next["放行"]
Skip --> |否| Extract["提取Authorization头"]
Extract --> Parse["JwtUtils.parseToken(token)"]
Parse --> Valid{"解析成功?"}
Valid --> |否| Err1["返回未登录或已失效"]
Valid --> |是| CheckActive["检查是否有活跃会话"]
CheckActive --> Has{"存在活跃会话?"}
Has --> |否| Err1
Has --> |是| CheckSame["检查sessionId是否匹配"]
CheckSame --> Match{"匹配?"}
Match --> |否| Err2["返回账号已在其他设备登录"]
Match --> |是| SetCtx["设置OrgContext(orgId, userId)"]
SetCtx --> DoFilter["继续过滤链"]
DoFilter --> Clean["finally清理上下文"]
Clean --> End(["结束"])

图示来源

章节来源

JwtUtils 令牌结构与签名验证

  • 令牌结构:
    • subject:用户ID
    • id:会话ID
    • claim.orgid:机构码
    • iat:签发时间
    • exp:过期时间
  • 签名算法:HMAC-SHA,密钥长度不少于32字节,由配置项注入。
  • 解析过程:使用JJWT解析并验签,提取必要字段,缺失则抛出异常。
  • 有效期:由配置项jwt.expiration决定,初始化时校验必须大于0。
classDiagram
class JwtUtils {
-SecretKey signingKey
-Duration expiration
-JwtParser jwtParser
+init() void
+generateToken(userId, orgId, sessionId) String
+parseToken(token) TokenClaims
+getOrgId(token) String
}
class TokenClaims {
+String userId
+String orgId
+String sessionId
}
JwtUtils --> TokenClaims : "返回"

图示来源

章节来源

AuthService 登录与令牌签发

  • 登录流程:
    • 标准化输入(orgId大写、userId去空白)
    • 查询用户并校验密码
    • 生成唯一sessionId与JWT
    • 登记活跃会话(覆盖旧会话,实现“最后登录设备”)
    • 记录登录日志
    • 返回token、orgid与用户信息
  • 安全要点:
    • 密码比较使用常量时间比较防止时序攻击
    • 登录失败也记录日志,便于审计
sequenceDiagram
participant FE as "前端"
participant AS as "AuthService"
participant DB as "数据库"
participant JU as "JwtUtils"
participant AR as "ActiveSessionRegistry"
FE->>AS : "login(orgId, userId, password)"
AS->>DB : "loadUserForAuthentication()"
DB-->>AS : "用户记录"
AS->>AS : "校验密码(常量时间比较)"
AS->>JU : "generateToken(accountId, orgId, sessionId)"
JU-->>AS : "token"
AS->>AR : "activate(orgId, accountId, sessionId)"
AS-->>FE : "{token, orgid, user}"

图示来源

章节来源

ActiveSessionRegistry 会话管理

  • 数据结构:ConcurrentHashMap,键为orgId+userId,值为当前活跃sessionId。
  • 行为:
    • activate:覆盖旧会话,实现“最后登录设备”策略
    • isActive:校验传入sessionId是否为当前活跃会话
    • hasActiveSession:判断账号是否存在活跃会话
  • 扩展点:如需严格“单设备登录”,可在activate前增加并发控制或引入Redis分布式锁。
flowchart TD
A["activate(orgId, userId, sessionId)"] --> Put["写入Map<orgId+userId, sessionId>"]
B["isActive(orgId, userId, sessionId)"] --> Get["读取当前sessionId并比较"]
C["hasActiveSession(orgId, userId)"] --> Contains{"键存在?"}

图示来源

章节来源

OrgContext 多租户上下文传递

  • 作用:在请求生命周期内以ThreadLocal形式持有orgId与userId,供数据源路由、权限校验、审计等使用。
  • 安全性:提供requireXxx方法强制校验上下文存在,避免空指针与越权访问;finally中清理上下文,防止线程复用污染。
  • 典型用法:在过滤器中设置,在业务层读取,在DAO/Service层根据orgId选择数据源或限定查询范围。
sequenceDiagram
participant F as "JwtAuthFilter"
participant C as "OrgContext"
participant S as "业务服务"
F->>C : "setOrgId/setUserId"
F->>S : "doFilter -> 执行业务"
S->>C : "requireOrgId/requireUserId"
S-->>F : "返回结果"
F->>C : "clear()"

图示来源

章节来源

依赖关系分析

  • JwtAuthFilter依赖JwtUtils与ActiveSessionRegistry,并通过OrgContext注入多租户上下文。
  • AuthService依赖JwtUtils与ActiveSessionRegistry,完成登录与令牌签发。
  • JwtUtils依赖配置项AuthProperties(密钥与有效期),并使用JJWT库进行签名与解析。
  • 各组件之间耦合度低、职责清晰,易于测试与替换。
graph LR
Filter["JwtAuthFilter"] --> Utils["JwtUtils"]
Filter --> Reg["ActiveSessionRegistry"]
Filter --> Ctx["OrgContext"]
Auth["AuthService"] --> Utils
Auth --> Reg
Utils --> Config["AuthProperties"]

图示来源

章节来源

性能与安全建议

  • 密钥管理
    • 使用强随机生成的HMAC密钥,长度不少于32字节
    • 通过环境变量或密钥管理服务注入,避免硬编码
    • 定期轮换密钥,并考虑灰度发布与兼容期
  • 令牌有效期
    • 合理设置jwt.expiration,平衡安全与用户体验
    • 短时效令牌配合刷新机制可降低泄露风险
  • 安全头配置
    • 启用HTTPS,防止中间人窃听
    • 设置安全响应头(如Strict-Transport-Security、Content-Security-Policy)
    • 限制跨域访问(CORS白名单)
  • 会话控制
    • 默认“最后登录设备”策略,如需“单设备登录”,可增加并发控制或引入分布式会话存储
    • 登出时主动失效会话(删除活跃会话记录)
  • 防重放与篡改
    • 使用HMAC-SHA签名,服务端验签
    • 结合请求签名或一次性nonce(可选)增强安全性
  • 审计与监控
    • 记录登录成功/失败、会话顶替事件
    • 监控异常解析与非法请求频率

[本节为通用指导,不直接分析具体文件]

故障排查指南

  • 常见错误
    • 缺少Authorization头或格式错误:检查前端是否正确携带Bearer Token
    • 解析失败或签名错误:检查密钥配置是否一致,是否被修改
    • 未登录或已失效:检查是否已登录且会话未过期
    • 账号已在其他设备登录:确认是否允许单设备登录策略
  • 定位步骤
    • 查看过滤器日志与返回码,确认拦截阶段
    • 核对JwtUtils初始化日志,确认密钥长度与有效期配置
    • 检查ActiveSessionRegistry中是否存在对应会话
    • 验证OrgContext是否在过滤器中正确设置与清理
  • 修复建议
    • 修正前端请求头格式
    • 更新密钥并重启服务
    • 调整会话策略或前端刷新逻辑

章节来源

结论

FMS系统采用无状态JWT认证与轻量级会话注册表相结合的设计,既保证了高并发下的可扩展性,又实现了灵活的多设备登录控制。通过OrgContext在多租户环境下精准传递组织与用户上下文,确保了数据隔离与审计能力。配合合理的密钥管理与有效期策略,整体安全与性能达到生产可用水平。

[本节为总结性内容,不直接分析具体文件]

附录:前端Token存储与自动刷新方案

  • 存储位置
    • 推荐:内存变量(关闭标签页即失效)或HttpOnly Cookie(更安全)
    • 避免:localStorage长期存储敏感Token(易受XSS影响)
  • 自动刷新策略
    • 在请求拦截器中检测Token即将过期(如剩余时间小于阈值)
    • 调用后端刷新接口获取新Token,更新本地存储
    • 若刷新失败,引导用户重新登录
  • 错误处理
    • 401未登录:清除本地状态并跳转登录页
    • 401会话被顶替:提示用户在其他设备登录,要求重新登录
  • 安全加固
    • 仅通过HTTPS传输Token
    • 设置合理的Cookie属性(Secure、SameSite)
    • 避免在URL或日志中暴露Token

[本节为通用方案建议,不直接分析具体文件]