9.6 KiB
9.6 KiB
04 DMS 认证鉴权、Token 生命周期与高并发策略 (Auth and Token Strategy)
审计基准:DMS REST API 规范第一节《登录鉴权接口》 核心风险:DMS 后台有限制,多次调用登录接口,只有最后一次生成的 Token 有效! 设计目标:建立零泄露凭据管理、内存安全缓存、防击穿 Single-Flight 并发锁与 401 自愈重试机制。
1. DMS 认证机制技术事实 (DMS_AUTH_FACTS)
基于文档第 3~78 行的逐行事实提取:
| 维度 | DMS 平台真实实现规则 | 对集成的要求与约束 |
|---|---|---|
| 登录接口 | POST https://dms.dmsiot.com/public/rest/third-party-app/token/get |
请求头 Content-Type: application/json |
| 凭据载荷 | Body 为 JSON: {"appid": "...", "appsecret": "..."} |
必须通过受控配置/环境变量加载,绝不硬编码 |
| Token 有效期 | expiresIn: 7200(标准 2 小时,7200 秒) |
本地需跟踪到期时间戳 |
| 滑动刷新机制 | “如果在 token 有效期内成功调用任一平台接口,则会刷新有效期,即从调用接口时间开始,2小时有效期” | 只要系统保持定期调用或心跳,Token 可实现无感延续,无需反复调用登录接口 |
| 单点登录互斥性 | “注意,后台有限制,多次调用登录接口,只有最后一次的 token 有效” | 重大并发红线:若有多个线程或集群节点并发调用登录,后发登录将立即废除前面的 Token,导致正在执行的其他请求瞬间遭遇 401 报错!必须实现单飞互斥锁 |
| Token 传输方式 | 文档声明支持两种方式: 1. Cookie(放到请求 Cookie 中) 2. URL 参数( ?token=...) |
不支持 Authorization: Bearer <token>!严禁使用未经文档支持的 Header 格式 |
| 401 错误特征 | HTTP 状态码 401 Unauthorized,返回 JSON:{"status": 401, "error": "Unauthorized", "path": "..."} |
统一由底层 HTTP 客户端拦截捕获,触发自动重登录恢复流 |
| 退出登录接口 | POST https://dms.dmsiot.com/public/rest/security/logout |
成功调用后该 Token 立即在服务端失效 |
2. 生产凭据存储规范 (Credential Policy)
2.1 敏感凭据零泄露规范
- 禁止项:
- 严禁将真实
appid、appsecret、token写入 Git 仓库中的任何文件。 - 严禁在
application.yml或application-druid.yml中保存生产明文。 - 严禁在代码异常堆栈、日志打印、HTTP Trace、错误提示信息中输出凭据。
- 严禁将真实
- 推荐配置格式:
在
ruoyi-admin/src/main/resources/application.yml中仅保留占位符配置项,真实值由生产环境变量注入:# DMS 智能门锁外部门禁平台配置 dms: base-url: ${DMS_BASE_URL:https://dms.dmsiot.com} app-id: ${DMS_APP_ID:} app-secret: ${DMS_APP_SECRET:} project-id: ${DMS_PROJECT_ID:0} token-expire-margin-seconds: 600 # 提前10分钟预防性刷新 connect-timeout-ms: 5000 read-timeout-ms: 10000
3. Token 传输安全策略 (TOKEN_TRANSPORT_RECOMMENDATION)
3.1 传输模式比选与推荐
- Cookie 传输(首选推荐):
- HTTP 请求头:
Cookie: token={token_value}。 - 优势:Token 位于 HTTP Header,绝不会记录在反向代理(Nginx/网关)的
access.log中,也不会泄露在浏览器历史记录、Referer 标头或请求 URL 监控中。
- HTTP 请求头:
- URL 传参(降级备选,需日志脱敏):
- 如果第三方 DMS 平台的某些 API 对 Cookie 解析存在兼容性缺陷,被迫采用
?token=...: - 必须实施全局 Log Redaction:在所有日志拦截器、HttpClient 监控日志中,通过正则表达式
(?<=token=)[^&]+将所有 Token 字符串替换为******。
- 如果第三方 DMS 平台的某些 API 对 Cookie 解析存在兼容性缺陷,被迫采用
4. DmsTokenManager 高并发防击穿架构 (TOKEN_CONCURRENCY_STRATEGY)
4.1 架构设计原理
为应对“多次登录导致先前 Token 立即失效”的严苛限制,必须采用 进程级单例 Token 缓存 + Single-Flight 并发互斥锁 + 提前刷新 + 401 兜底自愈 架构。
sequenceDiagram
autonumber
actor Thread1 as 业务线程 A
actor Thread2 as 业务线程 B
participant TM as DmsTokenManager
participant Cache as 本地 AtomicReference 缓存
participant Lock as Single-Flight 并发锁
participant DMS as DMS 鉴权服务器
Thread1->>TM: getToken()
Thread2->>TM: getToken()
TM->>Cache: 检查当前缓存中的 Token
alt 缓存有效 (当前时间 < 到期时间 - 10分钟)
Cache-->>TM: 返回有效 Token
TM-->>Thread1: 复用有效 Token
TM-->>Thread2: 复用有效 Token
else 缓存缺失或即将到期
TM->>Lock: 尝试获取登录排他锁 (ReentrantLock)
activate Lock
Note over Lock: 线程 A 成功获取锁,线程 B 阻塞等待
Thread1->>Lock: 再次双重检查缓存 (DCL)
Thread1->>DMS: POST /public/.../token/get
DMS-->>Thread1: 返回 200 OK (新 Token, expiresIn: 7200)
Thread1->>Cache: 更新缓存及过期时间戳
Thread1-->>Lock: 释放排他锁
deactivate Lock
Note over Lock: 线程 B 被唤醒,再次检查缓存 (DCL)
Thread2->>Cache: 命中线程 A 刚更新的 Token
Cache-->>Thread2: 返回最新 Token
TM-->>Thread1: 返回新 Token
TM-->>Thread2: 复用新 Token (无重复登录!)
end
4.2 核心并发控制机制说明
- 双重检查锁定 (Double-Checked Locking, DCL):
- 当多个业务请求同时探测到 Token 缺失或过期时,仅有拿到
ReentrantLock的首个线程发起真实 HTTP 登录请求。 - 处于等待队列的后续线程在获得锁后,先进行二次检查(DCL);若发现前序线程已刷新成功,则直接读取最新 Token,绝不发起第二次远程登录。
- 当多个业务请求同时探测到 Token 缺失或过期时,仅有拿到
- 滑动过期时间更新 (
Sliding Expiry Tracking):- 鉴于 DMS 规定“成功调用任一平台接口会刷新 2 小时有效期”,HttpClient 拦截器在收到任意 200 响应时,通知
DmsTokenManager.touch(),将本地缓存的失效时间向后顺延,减少不必要的登录刷新。
- 鉴于 DMS 规定“成功调用任一平台接口会刷新 2 小时有效期”,HttpClient 拦截器在收到任意 200 响应时,通知
- 预防性主动刷新 (
Proactive Refresh):- 设置
margin = 600秒(10分钟)。当有效剩余时间不足 10 分钟时,异步触发平滑刷新,杜绝业务请求在临界点遇到 401。
- 设置
5. 401 自愈与单次安全重试设计 (DMS 401 Recovery)
当由于外部原因(例如运维人员在第三方控制台手动注销、或 DMS 服务端异常重启导致 Session 丢失)出现 401 Unauthorized 时的处理规范:
业务请求触发调用
│
▼
使用本地 Token 调用 DMS API
│
├─────────────────────────────────┐
▼ [成功 200 OK] ▼ [失败 401 Unauthorized]
正常返回业务数据 立即令本地缓存的 Token 失效 (invalidate)
通知 TokenManager.touch() 延展寿命 │
▼
单飞排他锁获取新 Token (Single-Flight Login)
│
├─────────────────────────┐
▼ [重登录成功] ▼ [重登录失败]
安全重试原始业务请求 (最多 1 次) 标记操作失败 (FAILED)
│ 记录审计日志
├──────────────┐ 不影响本地住宿业务事实
▼ [重试成功] ▼ [依然401]
成功返回 熔断抛出异常
5.1 恢复红线守则
- 单次重试原则:因 401 触发的自动重试上限严格为 1 次。严禁设计任何
while(true)或大于 1 次的递归重试,防止由于网络异常或密码错误引发死循环或轰炸外部服务器。 - 业务解耦原则:若重新登录失败,DmsAdapter 应抛出封装后的
DmsAuthenticationException,将门禁任务状态置为RETRY_WAIT或FAILED,绝不可向上传播导致本地住宿分配主事务发生回滚。
6. Wave 2 Implementation Result (Wave 2 implemented against Mock DMS only)
- Token 核心实现:
TOKEN_TRANSPORT_MODEL=Cookie: token={token}(严禁使用未经证明的Authorization: Bearer)。TOKEN_CACHE_MODEL=SINGLE_PROCESS_IN_MEMORY(全应用单一集中缓存,严禁各个客户端分散维护)。TOKEN_SINGLE_FLIGHT_MODEL=REENTRANT_LOCK_DCL(ReentrantLock + 双重检查锁定,20 线程并发获取 Token 实测远程登录仅 1 次)。TOKEN_TTL_MODEL=7200S_WITH_REFRESH_SKEW(默认提前 120 秒在滑动窗口内平滑单飞刷新)。SUCCESSFUL_API_EXTENDS_LOCAL_TTL=YES(成功调用 DMS API 触发tokenManager.markSuccessfulUse()顺延本地失效时间戳)。AUTH_FAILURE_REFRESH_MODEL= 401 触发集中invalidateToken(),下一次调用单飞获取新令牌,杜绝 login storm。AMBIGUOUS_MUTATION_AUTO_RETRY=NO(写操作读取超时绝对不自动重试,规避重复凭据下发风险)。
- 验证结论:自动化并发测试(20 线程单飞登录、提前刷新单飞、401 自愈)全部通过,仅针对本地
MockDmsServer验证,LIVE_DMS_REQUEST_COUNT = 0。