# 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存储与自动刷新方案](#附录前端token存储与自动刷新方案) ## 简介 本技术文档聚焦FMS系统的JWT认证机制,系统性阐述令牌的生成、解析与验证流程,过滤器拦截逻辑,多设备会话控制,以及多租户上下文传递。同时给出配置项说明、安全最佳实践与前端令牌存储和自动刷新策略建议,帮助开发者快速理解并正确集成。 ## 项目结构 后端采用Spring Boot + JJWT实现无状态鉴权,关键模块如下: - 认证过滤器:请求级拦截,提取并校验Authorization头中的Bearer Token - 工具类:负责JWT签名、验签、载荷解析 - 认证服务:登录成功后签发Token并登记活跃会话 - 会话注册表:维护当前账号的活跃会话(支持单设备或最后登录设备) - 多租户上下文:通过ThreadLocal在请求链路中传递orgId与userId ```mermaid graph TB Client["客户端"] --> Filter["JwtAuthFilter
请求拦截与校验"] Filter --> Utils["JwtUtils
解析与验签"] Filter --> Registry["ActiveSessionRegistry
会话有效性检查"] Filter --> Ctx["OrgContext
设置多租户上下文"] AuthService["AuthService
登录与签发Token"] --> Utils AuthService --> Registry ``` **图示来源** - [JwtAuthFilter.java:20-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L20-L88) - [JwtUtils.java:18-82](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L82) - [AuthService.java:19-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L84) - [ActiveSessionRegistry.java:8-29](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L29) - [OrgContext.java:3-55](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L3-L55) **章节来源** - [JwtAuthFilter.java:20-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L20-L88) - [JwtUtils.java:18-82](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L82) - [AuthService.java:19-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L84) - [ActiveSessionRegistry.java:8-29](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L29) - [OrgContext.java:3-55](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L3-L55) ## 核心组件 - 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,确保多租户数据隔离与审计。 **章节来源** - [JwtAuthFilter.java:20-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L20-L88) - [JwtUtils.java:18-82](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L82) - [AuthService.java:19-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L84) - [ActiveSessionRegistry.java:8-29](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L29) - [OrgContext.java:3-55](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L3-L55) ## 架构总览 下图展示了从登录到受保护接口访问的完整时序: ```mermaid 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 ``` **图示来源** - [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84) - [JwtUtils.java:45-66](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L66) - [ActiveSessionRegistry.java:13-25](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L13-L25) - [JwtAuthFilter.java:40-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L40-L88) - [OrgContext.java:13-53](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L13-L53) ## 详细组件分析 ### JwtAuthFilter 过滤器工作原理 - 请求拦截:跳过OPTIONS预检与登录路径,避免重复鉴权。 - 令牌解析:从Authorization头提取Bearer Token,调用JwtUtils.parseToken进行签名校验与载荷解析。 - 会话校验:先检查该账号是否存在活跃会话,再校验当前sessionId是否与活跃会话一致,实现“最后登录设备”或“单设备登录”控制。 - 上下文注入:将orgId与userId写入OrgContext,并在finally中清理,保证线程安全。 - 错误处理:对缺失凭证、解析失败、会话无效等场景统一返回结构化错误响应。 ```mermaid 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(["结束"]) ``` **图示来源** - [JwtAuthFilter.java:34-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L34-L88) **章节来源** - [JwtAuthFilter.java:34-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L34-L88) ### JwtUtils 令牌结构与签名验证 - 令牌结构: - subject:用户ID - id:会话ID - claim.orgid:机构码 - iat:签发时间 - exp:过期时间 - 签名算法:HMAC-SHA,密钥长度不少于32字节,由配置项注入。 - 解析过程:使用JJWT解析并验签,提取必要字段,缺失则抛出异常。 - 有效期:由配置项jwt.expiration决定,初始化时校验必须大于0。 ```mermaid 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 : "返回" ``` **图示来源** - [JwtUtils.java:18-82](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L82) **章节来源** - [JwtUtils.java:18-82](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L82) ### AuthService 登录与令牌签发 - 登录流程: - 标准化输入(orgId大写、userId去空白) - 查询用户并校验密码 - 生成唯一sessionId与JWT - 登记活跃会话(覆盖旧会话,实现“最后登录设备”) - 记录登录日志 - 返回token、orgid与用户信息 - 安全要点: - 密码比较使用常量时间比较防止时序攻击 - 登录失败也记录日志,便于审计 ```mermaid 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}" ``` **图示来源** - [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84) - [JwtUtils.java:45-57](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L57) - [ActiveSessionRegistry.java:13-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L13-L15) **章节来源** - [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84) ### ActiveSessionRegistry 会话管理 - 数据结构:ConcurrentHashMap,键为orgId+userId,值为当前活跃sessionId。 - 行为: - activate:覆盖旧会话,实现“最后登录设备”策略 - isActive:校验传入sessionId是否为当前活跃会话 - hasActiveSession:判断账号是否存在活跃会话 - 扩展点:如需严格“单设备登录”,可在activate前增加并发控制或引入Redis分布式锁。 ```mermaid flowchart TD A["activate(orgId, userId, sessionId)"] --> Put["写入Map"] B["isActive(orgId, userId, sessionId)"] --> Get["读取当前sessionId并比较"] C["hasActiveSession(orgId, userId)"] --> Contains{"键存在?"} ``` **图示来源** - [ActiveSessionRegistry.java:11-25](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L11-L25) **章节来源** - [ActiveSessionRegistry.java:11-25](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L11-L25) ### OrgContext 多租户上下文传递 - 作用:在请求生命周期内以ThreadLocal形式持有orgId与userId,供数据源路由、权限校验、审计等使用。 - 安全性:提供requireXxx方法强制校验上下文存在,避免空指针与越权访问;finally中清理上下文,防止线程复用污染。 - 典型用法:在过滤器中设置,在业务层读取,在DAO/Service层根据orgId选择数据源或限定查询范围。 ```mermaid 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.java:81-87](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L81-L87) - [OrgContext.java:13-53](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L13-L53) **章节来源** - [OrgContext.java:13-53](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java#L13-L53) ## 依赖关系分析 - JwtAuthFilter依赖JwtUtils与ActiveSessionRegistry,并通过OrgContext注入多租户上下文。 - AuthService依赖JwtUtils与ActiveSessionRegistry,完成登录与令牌签发。 - JwtUtils依赖配置项AuthProperties(密钥与有效期),并使用JJWT库进行签名与解析。 - 各组件之间耦合度低、职责清晰,易于测试与替换。 ```mermaid graph LR Filter["JwtAuthFilter"] --> Utils["JwtUtils"] Filter --> Reg["ActiveSessionRegistry"] Filter --> Ctx["OrgContext"] Auth["AuthService"] --> Utils Auth --> Reg Utils --> Config["AuthProperties"] ``` **图示来源** - [JwtAuthFilter.java:20-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L20-L88) - [AuthService.java:19-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L84) - [JwtUtils.java:18-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L43) **章节来源** - [JwtAuthFilter.java:20-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L20-L88) - [AuthService.java:19-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L84) - [JwtUtils.java:18-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L43) ## 性能与安全建议 - 密钥管理 - 使用强随机生成的HMAC密钥,长度不少于32字节 - 通过环境变量或密钥管理服务注入,避免硬编码 - 定期轮换密钥,并考虑灰度发布与兼容期 - 令牌有效期 - 合理设置jwt.expiration,平衡安全与用户体验 - 短时效令牌配合刷新机制可降低泄露风险 - 安全头配置 - 启用HTTPS,防止中间人窃听 - 设置安全响应头(如Strict-Transport-Security、Content-Security-Policy) - 限制跨域访问(CORS白名单) - 会话控制 - 默认“最后登录设备”策略,如需“单设备登录”,可增加并发控制或引入分布式会话存储 - 登出时主动失效会话(删除活跃会话记录) - 防重放与篡改 - 使用HMAC-SHA签名,服务端验签 - 结合请求签名或一次性nonce(可选)增强安全性 - 审计与监控 - 记录登录成功/失败、会话顶替事件 - 监控异常解析与非法请求频率 [本节为通用指导,不直接分析具体文件] ## 故障排查指南 - 常见错误 - 缺少Authorization头或格式错误:检查前端是否正确携带Bearer Token - 解析失败或签名错误:检查密钥配置是否一致,是否被修改 - 未登录或已失效:检查是否已登录且会话未过期 - 账号已在其他设备登录:确认是否允许单设备登录策略 - 定位步骤 - 查看过滤器日志与返回码,确认拦截阶段 - 核对JwtUtils初始化日志,确认密钥长度与有效期配置 - 检查ActiveSessionRegistry中是否存在对应会话 - 验证OrgContext是否在过滤器中正确设置与清理 - 修复建议 - 修正前端请求头格式 - 更新密钥并重启服务 - 调整会话策略或前端刷新逻辑 **章节来源** - [JwtAuthFilter.java:46-88](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java#L46-L88) - [JwtUtils.java:28-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L28-L43) - [ActiveSessionRegistry.java:17-25](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L17-L25) ## 结论 FMS系统采用无状态JWT认证与轻量级会话注册表相结合的设计,既保证了高并发下的可扩展性,又实现了灵活的多设备登录控制。通过OrgContext在多租户环境下精准传递组织与用户上下文,确保了数据隔离与审计能力。配合合理的密钥管理与有效期策略,整体安全与性能达到生产可用水平。 [本节为总结性内容,不直接分析具体文件] ## 附录:前端Token存储与自动刷新方案 - 存储位置 - 推荐:内存变量(关闭标签页即失效)或HttpOnly Cookie(更安全) - 避免:localStorage长期存储敏感Token(易受XSS影响) - 自动刷新策略 - 在请求拦截器中检测Token即将过期(如剩余时间小于阈值) - 调用后端刷新接口获取新Token,更新本地存储 - 若刷新失败,引导用户重新登录 - 错误处理 - 401未登录:清除本地状态并跳转登录页 - 401会话被顶替:提示用户在其他设备登录,要求重新登录 - 安全加固 - 仅通过HTTPS传输Token - 设置合理的Cookie属性(Secure、SameSite) - 避免在URL或日志中暴露Token [本节为通用方案建议,不直接分析具体文件]