Files
railwaysystem/docs/development/dms-r3/04_auth_and_token_strategy.md
T

143 lines
9.1 KiB
Markdown
Raw Normal View History

# 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 传输方式** | 文档声明支持两种方式:<br>1. **Cookie**(放到请求 Cookie 中)<br>2. **URL 参数**(`?token=...`) | **不支持** `Authorization: Bearer <token>`!严禁使用未经文档支持的 Header 格式 |
| **401 错误特征** | HTTP 状态码 `401 Unauthorized`,返回 JSON:<br>`{"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` 时的处理规范:
```mermaid
flowchart TD
Start["业务请求触发调用"] --> CallDms["使用本地 Token 调用 DMS API"]
CallDms -->|"成功 200 OK"| Success1["正常返回业务数据<br/>通知 TokenManager.touch() 延展寿命"]
CallDms -->|"失败 401 Unauthorized"| Invalidate["立即令本地缓存的 Token 失效 (invalidate)"]
Invalidate --> SingleFlight["单飞排他锁获取新 Token (Single-Flight Login)"]
SingleFlight -->|"重登录成功"| Retry["安全重试原始业务请求 (最多 1 次)"]
SingleFlight -->|"重登录失败"| LoginFail["标记操作失败 (FAILED)<br/>记录审计日志<br/>不影响本地住宿业务事实"]
Retry -->|"重试成功"| Success2["成功返回"]
Retry -->|"依然 401"| CircuitBreak["熔断抛出异常"]
```
### 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`。