# 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 `!严禁使用未经文档支持的 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` 中仅保留占位符配置项,真实值由生产环境变量注入: ```yaml # 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 监控中。 - **URL 传参(降级备选,需日志脱敏)**: - 如果第三方 DMS 平台的某些 API 对 Cookie 解析存在兼容性缺陷,被迫采用 `?token=...`: - **必须实施全局 Log Redaction**:在所有日志拦截器、HttpClient 监控日志中,通过正则表达式 `(?<=token=)[^&]+` 将所有 Token 字符串替换为 `******`。 --- ## 4. `DmsTokenManager` 高并发防击穿架构 (`TOKEN_CONCURRENCY_STRATEGY`) ### 4.1 架构设计原理 为应对“多次登录导致先前 Token 立即失效”的严苛限制,必须采用 **进程级单例 Token 缓存 + Single-Flight 并发互斥锁 + 提前刷新 + 401 兜底自愈** 架构。 ```mermaid 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 核心并发控制机制说明 1. **双重检查锁定 (Double-Checked Locking, DCL)**: - 当多个业务请求同时探测到 Token 缺失或过期时,仅有拿到 `ReentrantLock` 的首个线程发起真实 HTTP 登录请求。 - 处于等待队列的后续线程在获得锁后,先进行二次检查(DCL);若发现前序线程已刷新成功,则直接读取最新 Token,**绝不发起第二次远程登录**。 2. **滑动过期时间更新 (`Sliding Expiry Tracking`)**: - 鉴于 DMS 规定“成功调用任一平台接口会刷新 2 小时有效期”,HttpClient 拦截器在收到任意 200 响应时,通知 `DmsTokenManager.touch()`,将本地缓存的失效时间向后顺延,减少不必要的登录刷新。 3. **预防性主动刷新 (`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`。