18 KiB
18 KiB
会话管理
**本文引用的文件** - [ActiveSessionRegistry.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java) - [AuthService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java) - [JwtUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java) - [ClientInfo.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfo.java) - [ClientInfoExtractor.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java) - [ActiveSessionRegistryTests.java](file://fms-api/src/test/java/cn/g3soft/fmsapi/service/ActiveSessionRegistryTests.java)目录
简介
本技术文档聚焦FMS系统的会话管理,围绕ActiveSessionRegistry的设计与实现,系统阐述会话注册、状态校验、多设备登录控制策略(同账号限制与会话冲突)、以及JWT令牌与会话的绑定与失效机制。同时给出会话数据的内存存储模型、监控与管理接口建议、安全最佳实践与性能优化建议,帮助读者在理解现有实现的基础上进行扩展与维护。
项目结构
当前会话管理相关代码位于后端API模块中,主要涉及以下职责:
- 认证与会话注册:AuthService负责登录流程,生成JWT并激活会话;ActiveSessionRegistry维护“机构+账号”维度的活跃会话映射。
- JWT工具:JwtUtils负责令牌的签发与解析,将sessionId作为令牌ID并与orgid、userId绑定。
- 客户端信息采集:ClientInfo与ClientInfoExtractor用于采集登录时的IP、UA、设备类型等环境信息,便于审计与风控。
graph TB
A["AuthService<br/>登录流程"] --> B["JwtUtils<br/>签发/解析JWT"]
A --> C["ActiveSessionRegistry<br/>活跃会话注册表"]
A --> D["LoginLogService<br/>登录日志记录"]
A --> E["DbUtils<br/>用户查询"]
A --> F["ClientInfoExtractor<br/>客户端信息采集"]
B --> G["AuthProperties<br/>JWT密钥/过期配置"]
图表来源
- AuthService.java:19-84
- JwtUtils.java:18-80
- ActiveSessionRegistry.java:8-29
- ClientInfoExtractor.java:13-61
章节来源
- AuthService.java:19-84
- JwtUtils.java:18-80
- ActiveSessionRegistry.java:8-29
- ClientInfoExtractor.java:13-61
核心组件
- ActiveSessionRegistry:基于ConcurrentHashMap维护“机构+账号”到“最新会话ID”的映射,提供激活、存在性检查、会话有效性校验能力。
- AuthService:封装登录流程,包含用户校验、密码比对、JWT签发、会话激活与登录日志记录。
- JwtUtils:负责JWT的初始化、签发与解析,将sessionId写入令牌ID,并在解析时返回userId、orgId、sessionId三元组。
- ClientInfo/ClientInfoExtractor:采集客户端IP、地理位置、浏览器/操作系统/设备类型及原始UA,用于登录审计与风控。
章节来源
- ActiveSessionRegistry.java:8-29
- AuthService.java:19-84
- JwtUtils.java:18-80
- ClientInfo.java:1-16
- ClientInfoExtractor.java:13-61
架构总览
下图展示了从登录请求到会话激活与JWT签发的完整调用链,以及后续鉴权时对会话有效性的校验路径。
sequenceDiagram
participant Client as "客户端"
participant Auth as "AuthService"
participant DB as "DbUtils"
participant JWT as "JwtUtils"
participant Reg as "ActiveSessionRegistry"
participant Log as "LoginLogService"
Client->>Auth : "提交登录(orgId, userId, password)"
Auth->>DB : "查询用户信息"
DB-->>Auth : "用户数据或空"
alt "用户不存在或密码错误"
Auth->>Log : "记录失败登录(含原因)"
Auth-->>Client : "返回失败"
else "认证成功"
Auth->>JWT : "generateToken(userId, orgId, sessionId)"
JWT-->>Auth : "返回token"
Auth->>Reg : "activate(orgId, accountId, sessionId)"
Auth->>Log : "记录成功登录(含sessionId)"
Auth-->>Client : "返回{token, orgId, user}"
end
图表来源
详细组件分析
ActiveSessionRegistry:设计模式与实现原理
- 设计要点
- 使用线程安全的ConcurrentHashMap存储“账户键→最新会话ID”,保证并发下的正确性与高性能。
- 账户键由orgId与userId组合而成,确保不同机构下同名用户的会话相互独立。
- 提供activate、isActive、hasActiveSession三个核心方法,分别用于激活会话、校验指定会话是否仍为当前活跃会话、判断是否存在活跃会话。
- 多设备登录控制
- 同一账号在同一机构内多次登录会覆盖之前的会话ID,即“后登录优先”。旧会话ID将不再通过isActive校验,从而实现“强制下线”的效果。
- 测试用例验证了同账号新登录替换旧会话、跨机构会话独立的语义。
- 过期清理机制
- 当前实现未内置定时清理逻辑。可结合JWT过期时间或应用启动/定时任务扫描,移除已过期或长时间无心跳的会话。
- 复杂度与内存
- 插入/查找/判空均为O(1)。随着在线用户数增长,内存占用线性增长。需关注高并发场景下的内存上限与淘汰策略。
classDiagram
class ActiveSessionRegistry {
- Map~AccountKey, String~ activeSessions
+ activate(orgId, userId, sessionId) void
+ isActive(orgId, userId, sessionId) boolean
+ hasActiveSession(orgId, userId) boolean
}
class AccountKey {
+ String orgId
+ String userId
}
ActiveSessionRegistry --> AccountKey : "组合键"
图表来源
章节来源
AuthService:登录流程与会话绑定
- 登录流程
- 标准化orgId与userId,查询用户信息,校验密码。
- 成功后生成UUID作为sessionId,调用JwtUtils生成token,并将orgId、accountId、sessionId注册到ActiveSessionRegistry。
- 记录登录日志(成功/失败),返回token、orgId与用户基本信息。
- 多设备冲突处理
- 通过ActiveSessionRegistry的activate覆盖旧会话,实现“同账号单设备在线”的策略。
- 安全与健壮性
- 使用常量时间比较避免时序攻击风险(MessageDigest.isEqual)。
- 对缺失字段进行严格校验,抛出异常以快速失败。
flowchart TD
Start(["登录入口"]) --> Normalize["标准化输入(orgId, userId)"]
Normalize --> QueryUser["查询用户信息"]
QueryUser --> UserFound{"找到用户?"}
UserFound --> |否| LogFail["记录失败登录"] --> ReturnFail["返回失败"]
UserFound --> |是| CheckPwd["密码校验"]
CheckPwd --> PwdOk{"密码正确?"}
PwdOk --> |否| LogFail
PwdOk --> |是| GenToken["生成sessionId与JWT"]
GenToken --> Activate["注册活跃会话"]
Activate --> LogSuccess["记录成功登录"]
LogSuccess --> ReturnOK["返回token/orgId/user"]
图表来源
章节来源
JwtUtils:令牌与会话的关联机制
- 令牌绑定
- generateToken时将sessionId放入JWT的ID字段,subject为用户ID,orgid作为自定义声明,从而建立“令牌↔会话”的强绑定。
- 同步更新
- 每次登录都会生成新的sessionId并覆盖旧会话,旧令牌即使未过期也会因会话不匹配而失效。
- 失效处理
- 服务端可通过解析令牌获取sessionId,再与ActiveSessionRegistry中的当前会话对比,不一致则拒绝访问。
- 令牌自身有效期由配置控制,到期后自然失效。
sequenceDiagram
participant Auth as "AuthService"
participant JWT as "JwtUtils"
participant Parser as "JwtParser"
Auth->>JWT : "generateToken(userId, orgId, sessionId)"
JWT-->>Auth : "token"
Note over Auth,JWT : "sessionId被写入令牌ID"
Auth->>JWT : "parseToken(token)"
JWT->>Parser : "解析签名与载荷"
Parser-->>JWT : "Claims(subject, orgid, id=sessionId)"
JWT-->>Auth : "TokenClaims(userId, orgId, sessionId)"
图表来源
章节来源
客户端信息采集:审计与风控基础
- 采集内容
- IP地址、IP地理位置、浏览器名称与版本、操作系统与版本、设备类型(桌面/移动/平板/机器人)及原始UA。
- 用途
- 登录日志审计、异常登录检测、设备指纹辅助风控。
- 容错
- UA解析失败时降级为默认值,不影响主流程。
章节来源
依赖关系分析
- 组件耦合
- AuthService依赖JwtUtils、ActiveSessionRegistry、DbUtils、LoginLogService与ClientInfoExtractor,承担编排职责。
- JwtUtils依赖配置AuthProperties,负责密钥与过期时间管理。
- ActiveSessionRegistry仅依赖JDK并发容器,低耦合、高内聚。
- 外部依赖
- JWT库用于令牌签发与解析。
- uap-java用于UA解析。
- 潜在循环依赖
- 当前未发现循环依赖。
graph LR
AuthService --> JwtUtils
AuthService --> ActiveSessionRegistry
AuthService --> DbUtils
AuthService --> LoginLogService
AuthService --> ClientInfoExtractor
JwtUtils --> AuthProperties
图表来源
章节来源
性能考虑
- 并发与锁
- ActiveSessionRegistry使用ConcurrentHashMap,读写操作无锁化,适合高并发场景。
- 内存占用
- 每个在线会话占用一个Map条目,需评估最大在线用户规模与堆内存上限。
- 过期清理
- 当前未实现自动清理。建议引入定时任务或事件驱动清理,依据JWT过期时间或最近访问时间剔除无效会话,降低内存压力。
- 令牌解析开销
- 高频鉴权场景下,JWT解析成本可控,但应避免重复解析;可在网关或过滤器层缓存解析结果(注意安全性)。
- 登录风暴防护
- 建议在AuthService前增加限流与验证码机制,防止暴力破解与资源耗尽。
[本节为通用性能建议,不直接分析具体文件]
故障排查指南
- 登录失败
- 账号不存在或密码错误:检查DbUtils查询与密码比对逻辑,确认数据库数据与加密方式一致。
- 登录日志记录失败:检查LoginLogService与数据库连接。
- 会话冲突
- 新登录后旧会话无法访问:这是预期行为(同账号单设备在线)。如需允许多设备,请修改ActiveSessionRegistry为“按设备维度”存储。
- JWT问题
- 密钥长度不足或过期时间为非正数:检查AuthProperties配置,确保满足最小长度与正值要求。
- 令牌解析失败:检查签名密钥一致性与时钟同步。
- 客户端信息异常
- UA解析失败:查看日志告警,必要时降级处理或屏蔽UA解析以提升稳定性。
章节来源
结论
当前FMS会话管理采用“轻量级内存注册表 + JWT绑定”的方案,实现了同账号单设备在线与会话冲突的强制下线策略。该方案简单高效、易于维护,适用于中小规模在线用户场景。若需支持多设备登录、精细化会话生命周期管理与更丰富的监控能力,可在现有基础上扩展会话存储结构、引入过期清理与统一鉴权拦截器。
[本节为总结性内容,不直接分析具体文件]
附录
会话数据存储结构与内存管理策略
- 数据结构
- 键:AccountKey = (orgId, userId)
- 值:sessionId(字符串)
- 内存管理
- 使用ConcurrentHashMap,O(1)读写。
- 建议:结合定时任务或事件触发清理过期/空闲会话,避免无限增长。
- 可考虑LRU或分片策略以应对超大规模在线用户。
章节来源
会话与JWT令牌的关联机制
- 绑定方式
- sessionId写入JWT的ID字段,subject为用户ID,orgid作为声明。
- 同步更新
- 每次登录生成新sessionId并覆盖旧会话,旧令牌即使未过期也会因会话不匹配而失效。
- 失效处理
- 鉴权时解析令牌获取sessionId,与ActiveSessionRegistry中的当前会话比对,不一致则拒绝。
章节来源
多设备登录控制策略
- 当前策略
- 同账号在同一机构内仅允许一个活跃会话,新登录将覆盖旧会话,实现“强制下线”。
- 冲突检测
- 通过ActiveSessionRegistry.isActive判断请求携带的sessionId是否为当前活跃会话。
- 扩展建议
- 若需多设备登录,可将键改为(orgId, userId, deviceId),并引入设备注册与去重逻辑。
章节来源
会话监控与管理接口建议
- 监控指标
- 在线会话数(按orgId/userId维度)、登录成功率、失败原因分布、JWT解析失败率。
- 管理接口
- 查询某账号当前活跃会话列表(可扩展为按设备维度)。
- 强制下线:根据orgId与userId清除对应会话。
- 批量清理:按过期时间或最后访问时间清理无效会话。
- 实现建议
- 在AuthService或新增SessionController暴露REST接口,内部调用ActiveSessionRegistry与JwtUtils完成操作。
[本节为概念性建议,不直接分析具体文件]
安全最佳实践
- 密钥管理
- JWT密钥长度不少于32字节,定期轮换;生产环境使用密钥管理服务。
- 传输安全
- 全站HTTPS,设置Cookie安全属性(如HttpOnly、Secure、SameSite)。
- 防暴力破解
- 登录接口限流、验证码、账号锁定策略。
- 会话安全
- 会话ID随机且不可预测;鉴权时校验会话与令牌一致性;及时清理过期/空闲会话。
- 审计与合规
- 记录登录成功/失败、设备信息、IP与位置;保留足够审计周期。
[本节为通用安全建议,不直接分析具体文件]
性能优化建议
- 减少解析次数
- 在网关或过滤器层缓存JWT解析结果(注意短期缓存与失效策略)。
- 降低GC压力
- 合理设置JVM参数,避免频繁创建大对象;会话清理定时任务分批执行。
- 水平扩展
- 若部署多实例,需将ActiveSessionRegistry迁移至分布式缓存(如Redis),保证跨实例一致性。
[本节为通用性能建议,不直接分析具体文件]