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

9.1 KiB
Raw Blame 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 传输方式 文档声明支持两种方式:
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 监控中。
  • URL 传参(降级备选,需日志脱敏):
    • 如果第三方 DMS 平台的某些 API 对 Cookie 解析存在兼容性缺陷,被迫采用 ?token=...:
    • 必须实施全局 Log Redaction:在所有日志拦截器、HttpClient 监控日志中,通过正则表达式 (?<=token=)[^&]+ 将所有 Token 字符串替换为 ******。

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 核心并发控制机制说明

  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 时的处理规范:

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。