一、动态口令项目,卡点几乎都在"最后一公里"
做双因素认证项目,前 80% 的工作其实很轻松:选令牌形态、发令牌、让用户扫码绑定、后台配策略。真正把工期拖爆、把实施人员逼疯的,往往是最后那一小段——把动态口令服务端接进已有的业务系统里。
这个环节的典型症状是这样的:
- 堡垒机厂商说"我们支持 RADIUS",但联调三天都收不到 Access-Accept;
- 交换机上配好了认证服务器地址,日志里一直打印"共享密钥不匹配"或干脆没有任何日志;
- 远程接入网关上,静态口令和动态口令要怎么拼、拼在哪,每家设备的规则都不一样;
- 自研的业务系统没有 RADIUS 客户端,只能走接口,但接口怎么防重放、怎么防暴力尝试,没人说得清;
- 上线第一天,10% 的用户报"动态口令错误",但同样的令牌在测试环境是好的。
这些问题几乎都不是"OTP 算法"的问题,而是协议对接与工程细节的问题。TOTP 算法本身早在 RFC 6238 里就定义死了,三十秒步长、六位数字、HMAC 运算,没什么好争的;真正决定项目能不能落地的,是你怎么把它塞进现有的认证链路里。
很多团队在百度搜索"Radius对接"或"OTP双因素对接方案"时,真正想确认的其实不是算法原理,而是三件事:我的设备能不能接、接完会不会影响现有登录流程、出问题了怎么排查。这篇文章就围绕这三个问题,把工程实现讲透。
这里先明确一下本文的边界:本文不讨论 UKey、FIDO2 与 OTP 之间怎么选型(那是选型决策的问题),本文只讨论一旦确定要用 OTP,服务端怎么接进去。
二、为什么 OTP 服务端几乎都通过 RADIUS 对接
新入行的工程师经常会问一个问题:都什么年代了,为什么动态口令这种新潮的认证方式,还要靠 RADIUS 这个 1997 年就定稿的老协议?直接给一套 REST 接口不就完了?
答案藏在生态里,而不是技术里。
2.1 历史沿革:RADIUS 是网络设备的"普通话"
RADIUS(Remote Authentication Dial-In User Service)诞生于拨号上网时代,最初就是为了解决"大量分散的接入设备如何共享一套认证后端"这个问题。它的设计目标非常朴素:
- 协议简单到可以在性能极弱的网络设备里用几百行 C 代码实现;
- 网络设备(NAS,Network Access Server)只需要当"传声筒",把用户名口令原样转发给后端,自己不做任何认证决策;
- 后端集中管理账号和策略,设备的增删改不需要动后端。
这套设计在今天听来平淡无奇,但在当年是革命性的。更重要的是,它培养了一整代网络设备的接口习惯。三十年后,交换机、路由器、防火墙、无线控制器、负载均衡、堡垒机、远程接入网关、云桌面网关、存储设备、甚至一些老牌 ERP 系统,几乎无一例外都在登录模块里内置了 RADIUS 客户端。
这就是生态惯性。一个 RADIUS 客户端的代码量可能只有几百行,厂商一旦写进去,往后十几年都不会重写。所以现实情况是:你让这些设备去支持一套新的 REST 认证接口,几乎不可能;但你给它们一个 RADIUS 服务器地址,十分钟就配好了。
2.2 生态兼容的三层收益
对 OTP 服务端来说,支持 RADIUS 不只是"兼容老设备"这么简单,它带来三层实打实的收益:
第一层:覆盖面。一个后台可以同时承接网络设备登录、远程接入、堡垒机、云桌面、WiFi 等所有支持 RADIUS 的场景,不需要每个场景单独做适配。
第二层:解耦。RADIUS 服务器对 NAS 来说只是一个黑盒,后端换算法(比如从 SHA1 切到国密 SM3)、换令牌形态、换策略(比如加失败锁定),对设备侧完全透明,配置一行不用改。
第三层:改造成本。存量系统的登录流程完全不用动。RADIUS 只接管"这次认证通不通过"的判断,认证通过之后的会话管理、权限体系、审计逻辑,还是原系统自己的事。
2.3 什么时候不该用 RADIUS
RADIUS 也不是万能的,出现以下几种情况时,应该考虑 REST 接口:
- 需要富交互的挑战流程。比如要推送确认、要扫码、要图形验证码,RADIUS 的挑战响应机制表达能力很弱(只能回一段纯文本提示加一个不透明的状态串)。
- 需要精细的上下文。RADIUS 的属性字段有限,想把设备指纹、地理位置、终端合规状态、业务单据号都传进来做风控决策,会很别扭。
- 自研系统。如果业务系统本身就是自己写的,直接用 REST 接口比在应用里塞一个 RADIUS 客户端库要清爽得多,也便于做细粒度的错误处理。
- 需要国密传输层。RADIUS 的响应校验固定使用 MD5,且 User-Password 属性的混淆也基于 MD5,这在密评场景里会成为一个解释起来很麻烦的点;接口方案可以直接走国密 TLS,更容易说清楚。
一句话总结:面向存量设备用 RADIUS,面向自研系统用 REST 接口,两个都提供是最好的形态。
三、RADIUS 协议基础:NAS、报文与共享密钥
讲对接之前,必须先把协议的骨架理清楚。RADIUS 跑在 UDP 之上,认证用 1812 端口,计费用 1813 端口(早期实现用过 1645/1646,老设备上偶尔还能见到)。
3.1 三个角色
| 角色 | 说明 | 在 OTP 场景中的实体 |
|---|---|---|
| 客户端 / NAS | 被保护的设备,把用户凭证转交给服务器 | 堡垒机、交换机、防火墙、远程接入网关 |
| 服务器 | 做认证决策并返回结果 | 动态口令服务端(OTP 服务端) |
| 用户 | 实际的人 | 员工、运维、外包人员 |
注意术语上的坑:在 RADIUS 里,NAS 是"客户端",OTP 服务端是"服务端"。这个方向和直觉是反的——NAS 主动发起请求,但在协议语境里它叫客户端。
3.2 报文结构
RADIUS 报文是一个定长头部加变长属性区的结构:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Code | Identifier | Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Authenticator (16 字节) | | | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Attributes ... (TLV: Type(1) + Length(1) + Value) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+几个字段的含义:
- Code(1 字节):报文类型。认证相关的核心取值有 1=Access-Request、2=Access-Accept、3=Access-Reject、11=Access-Challenge;计费相关有 4=Accounting-Request、5=Accounting-Response。
- Identifier(1 字节):请求/响应的匹配标识。服务器回包时必须原样带回同一个 Identifier,NAS 靠它把响应和待处理的请求对上。超时重传时 Identifier 保持不变。
- Length(2 字节):整个报文长度,取值 20 到 4096。
- Authenticator(16 字节):认证字,请求和响应的含义不同,见下一节。
- Attributes:TLV 三元组,Type 一个字节、Length 一个字节(含 Type 和 Length 自身)、Value 最长 253 字节。
3.3 共享密钥与安全机制
RADIUS 的安全完全依赖一个预置在 NAS 和服务器两侧的共享密钥(Shared Secret)。它有两个用途:
用途一:响应校验(Response Authenticator)。服务器回包时,Authenticator 字段填的是
ResponseAuth = MD5( Code + Identifier + Length + RequestAuth + Attributes + SharedSecret )其中 RequestAuth 是对应请求报文里的 16 字节认证字。NAS 收到响应后,用自己持有的共享密钥重算一遍,比对一致才认这个响应。这一步解决的是响应伪造问题——攻击者不知道共享密钥,就造不出能被 NAS 接受的 Accept 包。
这里要注意,MD5 在这里的用法是"密钥在消息末尾的拼接哈希",不是标准 HMAC。从密码学上讲这是一种不严谨的构造,也是今天 RADIUS 被人诟病的地方之一。工程上的补偿手段有三条:把共享密钥做得足够长足够随机(建议 22 字节以上、随机生成)、限制能访问 UDP 1812 的源地址、以及在支持的情况下启用 Message-Authenticator 属性(80 号),它用 HMAC-MD5 覆盖了整个报文,能防请求伪造和报文篡改。
用途二:口令混淆(User-Password 属性)。2 号属性 User-Password 不是明文传输的,它用共享密钥做了流密码式的混淆。算法如下(设共享密钥为 S,请求认证字为 RA,口令为 P):
- 把口令补齐到 16 字节的整数倍(不足补零);
- 计算
b1 = MD5(S + RA),密文第一块c1 = p1 XOR b1; - 计算
b2 = MD5(S + c1),密文第二块c2 = p2 XOR b2; - 以此类推,每块的密钥流都依赖上一块的密文。
因为是 XOR 流,所以这个混淆不提供完整性保护,也不抗主动篡改,它的强度完全绑定在共享密钥的强度和 MD5 的性质上。理解这一点,对后面排查"口令拆不出来"类问题很关键——只要共享密钥错一个字节,解出来的口令就是一串乱码,而这串乱码送去做 OTP 校验,结果必然是拒绝。
3.4 高频属性速查
对接时你真正会打交道的属性其实不多,列一张表:
| Type | 名称 | 用途与注意事项 |
|---|---|---|
| 1 | User-Name | 用户名。注意可能被设备加上域后缀,需在服务端做归一化 |
| 2 | User-Password | 静态口令或"静态口令+动态口令"的拼接串 |
| 4 | NAS-IP-Address | NAS 的 IP,服务端常用它做客户端登记校验 |
| 5 | NAS-Port | 物理/逻辑端口号 |
| 6 | Service-Type | 登录类型(Login / Framed / Call-Check 等) |
| 18 | Reply-Message | 回显给用户的文本,挑战模式下用来提示"请输入动态口令" |
| 24 | State | 挑战模式下维持会话状态的不透明串,第二轮请求必须原样带回 |
| 25 | Class | 服务器下发、NAS 在计费包中回传的会话分类信息 |
| 26 | Vendor-Specific | 厂商私有属性,不同设备差异极大 |
| 31 | Calling-Station-Id | 用户侧地址(WiFi 场景是终端 MAC) |
| 32 | NAS-Identifier | NAS 的 textual 标识,多网卡设备的坑点 |
| 61 | NAS-Port-Type | 端口类型,可用于区分接入方式 |
| 80 | Message-Authenticator | 建议开启,防报文伪造与篡改 |
四、OTP 在 RADIUS 上的两种模式与报文时序
动态口令服务在 RADIUS 上落地,主流有两种模式。选错模式,是联调失败最常见的原因。
4.1 模式一:一次性口令直接校验(拼接提交)
这是最常见的模式,适合用户输入框只有一个、设备不支持挑战交互的场景。
流程是:用户在一个密码框里输入"静态口令 + 动态口令"的组合串(比如MyP@ss2024后面直接跟481920),NAS 把这个整串放进 User-Password 属性送过来;服务端收到后,按预先配置的规则把整串拆成两部分,静态部分送 LDAP/AD/本地库校验,动态部分走 TOTP 校验,两者都对才返回 Access-Accept。
拆分规则的三种常见约定:
- 后缀固定长度:动态口令固定 6 位,取末 6 位作动态口令,其余作静态口令。最通用,推荐优先用这种。
- 前缀固定长度:动态口令在前。少数设备这么干。
- 分隔符:用逗号、冒号等分隔。可读性好,但用户容易输错,且分隔符可能和口令字符冲突。
报文时序非常短,一次交互就结束:
用户 NAS(堡垒机) OTP 服务端 LDAP/账号源 | | | | |-- 用户名 --------->| | | |-- 口令+动态口令 -->| | | | | | | | |-- Access-Request ------->| | | | User-Name = zhangsan | | | | User-Password = 拼接串 | | | | NAS-IP-Address = 设备地址| | | | [Message-Authenticator]| | | | | | | | |-- 拆分静态口令 ------->| | | |<-- 静态口令通过 -------| | | | | | | |-- TOTP 校验 | | | | (取种子、算步长窗口) | | | | | | |<-- Access-Accept --------| | | | ResponseAuth 校验通过 | | |<-- 登录成功 -------| | | | | | | | |-- Accounting-Request --->| (Start, UDP 1813) | | | Acct-Status-Type=Start | | | |<-- Accounting-Response --| |如果任一部分失败,服务端直接回 Access-Reject。这里有个细节要强调:服务端不应该在 Reject 里区分"静态口令错"和"动态口令错"。返回不同的错误提示等于给了攻击者一个信息通道,让他可以先猜静态口令。正确的做法是统一回"用户名或口令错误",把具体的失败原因写进服务端日志。
4.2 模式二:挑战响应模式(两轮交互)
当设备的登录界面支持两轮密码输入时(很多堡垒机、远程接入网关、某些交换机的 SSH 登录支持),可以用挑战响应模式。
这个模式的流程是:第一轮 NAS 只提交静态口令,服务端校验通过后不立即返回 Accept,而是返回一个Access-Challenge(Code=11),里面带一个Reply-Message提示用户输入动态口令,同时带一个State属性,作为这次会话的凭据;NAS 收到 Challenge 后,弹出第二个输入框让用户输动态口令,然后发起第二轮Access-Request,这一轮必须把上一轮的State原样带回。
用户 NAS(远程接入网关) OTP 服务端 | | | |-- 用户名 --------->| | |-- 静态口令 ------->| | | | | | |-- Access-Request ------->| | | User-Name = zhangsan | | | User-Password = 静态口令 | | | State = (空) | | | | | |<-- Access-Challenge -----| | | State = 0x9f3a... | | | Reply-Message = | | | "请输入动态口令" | | | | |<-- 提示输动态口令 --| | |-- 动态口令 ------->| | | | | | |-- Access-Request ------->| | | User-Name = zhangsan | | | User-Password = 481920 | | | State = 0x9f3a... <=== 必须原样带回 | | | | | |-- TOTP 校验 | | | + State 有效性校验 | | | + State 一次性消费 | | | | |<-- Access-Accept --------| |<-- 登录成功 -------| |挑战响应模式有几个工程要点:
- State 必须服务端自持且不可预测。State 里通常加密封装了会话标识、用户名、挑战发起时间、随机数,服务端解密校验后才知道这轮挑战合不合法。绝不能让客户端构造 State,否则就成了越权通道。
- State 要有时效和一次性。挑战发出后 60 秒内没收到第二轮,State 作废;State 用过一次立即作废,防止重放。
- 第一轮不能泄露信息。即使静态口令错,也建议照常返回一个 Challenge(带随机 State),第二轮再统一拒绝。这样攻击者无法通过"有没有弹第二个框"来判断静态口令是否正确。当然,这会让用户体验略差(输错了也会让你输动态口令),需要在安全和体验之间权衡,一般推荐内部系统开启、面向公网的远程接入也开启。
- 不是所有设备都支持。这是这个模式最大的限制。交换机、防火墙这类设备的登录流程往往是"一次提交就结束",根本不支持弹第二个输入框,那就只能用模式一。
4.3 硬件令牌的"真挑战响应"
还有一种更严格意义的挑战响应,常见于硬件令牌(带键盘的令牌,支持 OATH OCRA)。流程是:服务端在 Challenge 里下发一个挑战值(比如一个 8 位数字),用户把这个挑战值敲进令牌,令牌用内置密钥和挑战值算出应答(也是一串数字),用户输入应答。
这种方式的好处是挑战值每次都不同,截获一次应答无法复用,可以防实时中继钓鱼(AITM)。代价是用户操作繁琐,而且要求令牌硬件支持。以安当OTP为例,服务端侧同时支持时间型(TOTP)口令和挑战响应型口令,令牌侧则可以用手机令牌或硬件令牌承载,具体选哪种取决于终端人群和管理成本。
五、常见设备的对接配置要点
这一节全是踩坑经验,可以当检查清单用。
5.1 共享密钥:强度与大小写
- 强度:不要用单词、不要短于 16 字节,建议 22 字节以上、由随机数生成器产生、包含大小写字母数字与符号。共享密钥是整个 RADIUS 安全体系的根,弱密钥等于把认证决策权交出去。
- 区分大小写:RADIUS 共享密钥是区分大小写的,而且没有任何机制能告诉你"密钥错了",现象只会是"响应校验失败"或直接"无响应"。复制粘贴时最常犯的错是尾部多带一个空格或换行符,肉眼完全看不出来。建议用文本编辑器比对,或者干脆两边都手工重输一遍。
- 特殊字符:部分老设备对
$、&、!等 shell 元字符处理有问题,配置时可能被 shell 吃掉。稳妥做法是只用字母数字加少数几个安全符号。 - 一设备一密钥:不要全网段共用一把共享密钥。一台设备泄露就波及全部,且无法定位来源。
5.2 端口与协议
- 认证 1812 / 计费 1813,UDP。务必确认设备配的是新端口还是旧端口(1645/1646)。
- UDP 的天然问题:无连接、无重传保证、可能被中间网络设备静默丢弃。所以 RADIUS 的重传完全靠 NAS 自己做。
- 放通方向:不只是 NAS 到服务器的 1812 要通,服务器回包的源端口也要能被 NAS 收到。有状态防火墙上要确认回包会话是放通的;NAT 环境下尤其要注意,服务器看到的源地址可能已经不是 NAS 的真实地址。
- 分片:如果属性很多导致报文超过 MTU,UDP 会分片,某些网络环境下分片会被丢弃导致间歇性失败。遇到"时好时坏"的怪现象,优先查这个。
5.3 超时与重传
- 超时时间:建议 3 到 5 秒。太短会在服务端偶发慢查询时产生大量无谓重传,太长会让用户感觉卡死。
- 重传次数:建议 2 到 3 次。重传时 Identifier 和 Authenticator 必须保持不变,否则服务端会当成新请求处理,导致同一份凭证被校验两次,进而触发"动态口令已使用"的误报。
- 服务端侧的幂等:服务端应该能识别重复请求(同 Identifier、同认证字、短时间内的重复),直接重放上次的响应而不重复校验口令。这是很多自研实现忽略的一个点,上线后会出现"用户点一次登录,失败计数加了两次"的诡异问题。
- 主备切换:配置主备两台服务器时,注意不同设备的切换策略不同,有的是"重试 N 次后切备机",有的是"主服务器标记为 dead 后切备机并在若干秒后探测恢复"。要提前确认,并测试拔网线的真实效果。
5.4 源地址白名单与 NAS 登记
服务端必须维护一张允许接入的 NAS 清单,清单项通常是"NAS 地址 + 共享密钥 + 备注 + 所属业务"。这里有几个高频坑:
- 多网卡/多地址:一台设备可能从多个地址发包(管理口、业务口、VRRP 虚地址、HA 心跳切换后的新地址)。排查方法是在服务端开抓包,看实际到达的源地址,而不是照着配置文档猜。
- NAT 之后:NAS 在 NAT 后面时,服务端看到的地址是 NAT 出口地址。要么把出口地址登记进去,要么改用 NAS-Identifier 匹配。
- IPv6:如果设备支持并启用了 IPv6,源地址可能是 IPv6,白名单要同时覆盖。
- 拒绝策略:未登记的 NAS 请求应当直接丢弃并记日志,不要回 Reject。回 Reject 等于告诉对方"我收到了",可能助长扫描探测。
六、REST API 对接:接口设计与防护
对于自研系统、或者需要富交互流程的场景,走 REST 接口更合适。不少开发团队在检索"双因素认证接口怎么对接"时,真正想确认的不是接口长什么样,而是重试会不会把口令用掉、重复提交会不会误锁用户、接口挂了业务还能不能登录。这一节就把这三个问题连同接口设计一起讲清楚。
6.1 同步校验接口的形态
典型的校验接口是一次同步调用:
POST /api/v1/otp/verify # 接口路径为示例,实际以服务端网关为准 请求体(示意): { "appId": "bastion-prod", # 接入方标识 "userId": "zhangsan", # 用户唯一标识 "otp": "481920", # 动态口令 "requestId": "a1b2c3d4-...", # 请求唯一流水号,用于幂等 "timestamp": 1735689600000, # 毫秒时间戳 "nonce": "7f3a9c21", # 随机数 "signature": "..." # 对规范串的签名 } 响应体(示意): { "code": 0, "message": "success", "data": { "result": "PASS", # PASS / FAIL "serial": "TOKEN-000123", # 命中的令牌序列号,便于审计 "remainAttempts": 4 # 剩余尝试次数 } }传输层必须使用 TLS 1.2 及以上;在密评或高安全场景,建议启用双向证书认证,让服务端也校验调用方的身份,而不只是靠 appId + 密钥。网络位置固定的接入方(比如只有几台堡垒机),还应该叠加 IP 白名单。
6.2 幂等设计
接口的幂等性是工程上最容易出事的地方。业务系统遇到网络抖动会重试,如果每次重试都被当成一次新的认证尝试,后果是:
- 正确的动态口令被重试请求消耗掉,第二次反而失败;
- 失败计数被重复累加,用户被误锁;
- 审计日志里出现多条"同一时刻的认证失败",干扰溯源。
正确的做法是:要求调用方在发起请求时生成一个全局唯一的 requestId,服务端以 requestId 为键做去重。同一个 requestId 在有效期内(比如 5 分钟)重复到达,服务端直接返回首次的处理结果,不重复校验、不重复计数。
去重键的存储可以用内存缓存(单机)或分布式缓存(集群);考虑到这是安全控制点,缓存不可用时建议降级为拒绝重试并提示稍后再试,而不是降级为不幂等——宁可牺牲一点可用性,也不要让安全计数失真。
6.3 重放防护
重放防护要解决两个不同层次的问题:
层次一:报文级重放。攻击者抓到一个合法的校验请求报文,原样重发。防护手段是三件套:timestamp时间戳(服务端校验与本地时间的偏差,超窗即拒,常见窗口 ±5 分钟)、nonce随机数(服务端记录近期已用过的 nonce,重复即拒)、signature签名(覆盖所有关键字段,防篡改)。三者缺一不可——只有时间戳防不住窗口内重放,只有 nonce 需要无限存储,只有签名防不住原样重放。
层次二:口令级重放。这是 OTP 特有的,也是最容易被忽略的。TOTP 在一个步长(30 秒)内产生的口令是恒定不变的,也就是说同一个口令在这 30 秒里可以被无限次使用。如果攻击者在这 30 秒内截获了口令(肩窥、键盘记录、中间人),他可以立刻用这个口令登录一次。
防护手段是口令一次性消费:服务端为每个令牌记录"最近一次成功使用的步长(counter 值)",任何一次成功校验后,该令牌的"已使用步长"就更新;在步长窗口允许的范围内,小于等于已使用步长的口令一律拒绝。这样即使窗口开了 ±1,同一个口令也无法用第二次。
这里要权衡:口令一次性消费会导致"用户 30 秒内连点两次登录,第二次失败"。解决办法是上面说的 requestId 幂等——重试属于同一次请求,不算新的消费。
6.4 失败锁定与限流
动态口令只有 6 位,理论上 100 万种组合。如果不做限制,攻击者只要能反复提交,数学期望 50 万次就能猜中一个有效口令。所以失败锁定与限流不是可选项,是必选项。
推荐的分层策略:
| 层级 | 规则建议 | 触发后果 |
|---|---|---|
| 单用户 + 单应用 | 连续失败 5 次 | 锁定该用户在该应用上的 OTP 校验 15 分钟 |
| 单用户(全局) | 连续失败 10 次 | 锁定令牌,需管理员解锁或用户走应急流程 |
| 单来源地址 | 每分钟失败超过 30 次 | 该来源进入观察/限流名单 |
| 单应用(全局) | 每分钟失败超过阈值 | 触发告警,可能存在批量撞库 |
几个设计细节:
- 计数要按"用户+应用"维度而非只用用户,否则一个应用被攻击会连累用户在所有系统上都登不了。
- 锁定后返回明确提示,告诉用户"已锁定,请 15 分钟后重试或联系管理员",而不是笼统的"口令错误"。
- 成功要清零计数。
- 限流优先于校验。被限流的请求不要进入口令比对逻辑,否则限流就只是"拒绝返回结果"而已,攻击者仍然能通过响应时间侧信道做枚举。
- 服务端要有告警。失败率的突增是最直接的攻击信号,应接入监控告警。
6.5 返回码设计
返回码要设计得让调用方能自动处理,又不能泄露太多信息给终端用户。建议区分给调用方的 code和给用户的 message:
| code | 含义 | 调用方应如何处理 | 是否展示给用户 |
|---|---|---|---|
| 0 | 校验通过 | 放行 | — |
| 10001 | 参数缺失或格式错误 | 记录日志,属接入 bug | 否,提示"系统异常" |
| 10002 | 签名校验失败 | 检查密钥与规范串生成 | 否 |
| 10003 | 时间戳超窗 | 校准调用方服务器时钟 | 否 |
| 10004 | requestId 重复但内容不一致 | 属接入 bug,告警 | 否 |
| 10005 | 应用未授权或已停用 | 检查接入配置 | 否 |
| 10006 | 用户不存在或未绑定令牌 | 引导用户去做绑定 | 是,提示"请先绑定动态口令" |
| 10007 | 动态口令错误 | 计入失败次数 | 是,统一提示"动态口令错误" |
| 10008 | 口令已被使用(重放) | 计入失败次数,可能是攻击 | 是,提示"请等待新口令" |
| 10009 | 用户/令牌已锁定 | 引导走解锁流程 | 是 |
| 10010 | 触发限流 | 退避重试 | 是,提示"操作过于频繁" |
| 20001 | 服务端内部错误 | 必须按失败处理,不得放行 | 否 |
最后一条是安全红线:接口不可用时的默认动作必须是"拒绝",绝不能是"放行"。很多事故就是因为在业务代码里写了"调不通就跳过二次认证"的兜底逻辑,结果一次网络抖动就让整个二次认证形同虚设。正确的做法是接口不可用时直接拒绝登录,并告警。
6.6 调用超时与重试
- 建议连接超时 1 秒、读超时 3 秒;
- 重试最多 2 次,重试必须携带相同的 requestId;
- 重试要有退避(比如 200 毫秒、500 毫秒),避免雪崩;
- 服务端不可用时,业务系统应当给出清晰提示,并保留管理员应急通道。
七、国密算法在 OTP 中的落点
合规场景下,很多团队会关心一个问题:动态口令能不能用国密?
能。TOTP 的算法骨架是 HMAC,HMAC 里的哈希函数是可替换的,RFC 6238 本身也允许使用不同的哈希算法。把 SHA-1 换成SM3,就是国密化的 TOTP。要点是:
- 两端必须一致:服务端和令牌 App 必须使用完全相同的哈希算法。如果服务端配成 SM3 而令牌按 SHA-1 算,算出来的六位数永远对不上,且没有任何提示,只会报"口令错误"。上线前一定要用同一批测试令牌把每种算法都验一遍。
- 种子编码:种子密钥的编码方式(base32 还是 hex)和大小写也必须两端一致。扫码注册的场景下,这些信息都编码在二维码里,所以要确保生成二维码的服务端配置和实际校验配置是同一套。
- 兼容第三方验证器:主流第三方验证器 App 通常只支持 SHA-1/SHA-256/SHA-512,不一定支持 SM3。如果要用 SM3,通常需要使用配套的专用手机令牌。
- 合规表述:在密评材料里,能说清楚"动态口令生成算法采用 SM3",对密码应用的合规性论证是有帮助的;但同时也要如实说明 RADIUS 链路上的 MD5 响应校验问题,并给出补偿措施(启用 Message-Authenticator、限制源地址、链路隔离)。这一点在做密评时经常被问到,提前准备比临场解释从容得多。
以安当OTP为例,服务端同时支持 SHA1、SHA256、SHA512、SHA224、SHA384 与国密 SM3,令牌侧提供手机令牌(扫码注册)、硬件令牌与短信口令,算法可以按应用维度分别配置,这样存量系统和新建的合规系统可以共存于同一个后台。
八、灰度上线验证清单
动态口令是在登录链路上加的一道闸,一旦出问题就是"全员进不了系统"。所以灰度必须做,而且要分步做。
8.1 四步灰度法
第一步:影子验证(1~3 天)。认证链路仍然走原有逻辑,但把动态口令的校验结果旁路记录到日志,不实际拦截。这一步的目的是验证"在真实流量下,动态口令的正确率是多少",把时钟漂移、令牌未激活、种子不同步等问题在数据层面暴露出来。如果影子阶段的通过率低于 95%,说明令牌发放或绑定环节有问题,先别急着切。
第二步:小范围强制(1~2 周)。选一个配合度高的部门(通常是 IT 部门自己)+ 一个非核心系统,强制开启动态口令。注意一定要先把 IT 和运维自己纳入进来——自己都不用的东西,出了问题没法快速定位。
第三步:分场景铺开(2~4 周)。按"远程接入 → 堡垒机 → 网络设备 → 业务系统"或按部门逐个铺开。每铺一个场景,观察三到五天,确认失败率、工单量、响应时延都正常。
第四步:全量强制 + 收敛例外。全量开启,同时把例外清单(比如产线终端、无法携带手机的岗位)做成正式的例外流程,而不是口头放行。
8.2 灰度期必须准备好的三件事
第一,逃生通道。至少要有一种在令牌不可用时的应急方式,且这个方式本身要受管控:
- 一次性应急口令(管理员生成,一次有效,用后即焚,全场次留痕);
- 管理员代解锁(需要二次审批,全程审计);
- 备用认证因子(如 UKey)。
逃生通道最怕的是变成"常态化后门",所以必须设定使用期限、使用次数和审批留痕,并定期复盘。
第二,回滚方案。保留原有的本地认证方式作为备用认证域,出现大面积故障时可以在服务端一键切回(把 RADIUS 指向备用认证源,或在接口侧把二次认证降级为审计模式)。这个开关要提前演练,不能到出事时才第一次按。
第三,监控指标。至少要能实时看到:
- 认证成功率 / 失败率(按应用、按部门拆分);
- 失败原因分布(口令错误、锁定、限流、超时);
- 服务端响应时延的 P99;
- 接口超时率与重传率;
- 挑战模式的挑战放弃率(发了 Challenge 却没有第二轮,通常意味着交互体验有问题)。
8.3 上线前检查清单
| 项 | 检查内容 | 通过标准 |
|---|---|---|
| 时间同步 | 服务端、NAS、令牌设备的时钟 | 均指向同一 NTP 源,偏差 < 1 秒 |
| 步长配置 | 步长长度与容错窗口 | 30 秒 / 容错 ±1 步 |
| 共享密钥 | 长度、随机性、一致性 | ≥22 字节随机,全设备逐一验证 |
| NAS 登记 | 源地址白名单 | 抓包确认真实源地址已全部登记 |
| 幂等 | 重复 requestId 行为 | 返回首次结果,不重复计数 |
| 重放 | 口令一次性消费 | 同一步长内二次使用被拒 |
| 锁定 | 失败锁定规则 | 按用户+应用维度生效并验证 |
| 限流 | 单地址/单应用阈值 | 触发后不再进入校验逻辑 |
| 失败默认 | 接口不可用时的行为 | 拒绝登录,不放行 |
| 逃生 | 应急口令流程 | 已演练,有审批与留痕 |
| 回滚 | 降级开关 | 已演练,切换时间 < 5 分钟 |
| 审计 | 认证日志字段 | 含用户、时间、来源、结果、令牌序列号 |
| 告警 | 失败率突增 | 已接入告警通道,值班人明确 |
九、故障排查手册
这一节按"现象 → 原因 → 处理"组织,是可以直接对着用的。
9.1 现象:所有用户都报动态口令错误,但令牌看起来正常
排查顺序:
- 查服务端时间。这是头号原因。服务端系统时间如果偏移超过容错窗口,所有人的口令都会错。用
date对比 NTP 源,看ntpd/chronyd是否在正常运行。虚拟机要特别注意——宿主机迁移、快照恢复、休眠都会导致时钟跳变。 - 查算法配置。服务端配的哈希算法和令牌实际用的算法是否一致(SM3 / SHA1 / SHA256 等)。用一把已知种子的测试令牌,逐个算法试。
- 查种子同步。用户重新扫码绑定后,旧的种子是否还在生效?有时用户扫了两次码,服务端记的是第一次的种子,而手机上是第二次的。
- 查步长单位。极少数实现里把步长配成了 60 秒或别的数值,和令牌默认的 30 秒对不上。
9.2 现象:部分用户报口令错误,且时好时坏
这是时钟漂移的典型表现,尤其集中在硬件令牌上。硬件令牌靠内部晶振计时,晶振会随温度和使用年限漂移,一年偏几十秒并不罕见,累积几年就可能超出容错窗口。
处理办法:
- 启用自动重同步(resync):当用户连续两次提交的口令分别对应当前时间的前后两个相邻步长时,服务端可以推算出该令牌的漂移量并记录,之后按漂移量校正。这是硬件令牌场景的必备功能。
- 提供手动校准入口:让用户在自助门户里连续输入两个动态口令,服务端据此计算漂移。
- 设定令牌有效期:硬件令牌的电池寿命通常是 3 到 5 年,到期必须更换。把令牌启用日期纳入台账管理,到期前主动提醒。
- 漂移超阈值告警:当某个令牌的漂移量超过预设阈值(比如 ±5 分钟),标记为"待更换",避免某天突然彻底不可用。
9.3 现象:用户抱怨"口令刚输完就过期"
通常是步长窗口设置过窄或用户输入太慢。
- 确认容错窗口是 ±1 步(即 ±30 秒,实际有效区间约 60~90 秒),而不是严格的 30 秒。完全不容错的实现体验极差,用户在口令快跳变时输入,提交时已经过期。
- 窗口也不是越大越好。容错窗口开到 ±10 步,意味着攻击者一次尝试能命中 21 个候选口令,暴力破解的成功率提升 21 倍。稳妥的取值是±1 步为默认,最多 ±2 步,再宽就要靠锁定和限流来补偿。
- 部分硬件令牌的显示周期和 TOTP 步长不完全对齐(比如令牌每 60 秒显示一次而服务端按 30 秒算),这种组合必须避免。
9.4 现象:NAS 侧提示认证失败,服务端没有收到任何请求
- 地址配错:确认 NAS 上填的是"认证服务器地址",不是计费地址,也不是某台中间件地址。
- 端口不通:在 NAS 上做基本的连通性测试;注意 UDP 的连通性测试和 TCP 不一样,很多
ping通但 UDP 1812 不通的情况,是中间防火墙只放通了 ICMP。 - 出接口地址:设备上如果没指定 RADIUS 的源接口,可能用了非预期的出接口地址,导致服务端白名单不匹配。设备侧显式指定源接口,服务端侧抓包确认。
9.5 现象:服务端收到了请求,但返回 Reject 或直接丢弃
- NAS 未登记:抓包看源地址,对比白名单。多网卡、HA 切换、NAT 是三大主因。
- 共享密钥不匹配:这是第二大主因。检查大小写、首尾空格、特殊字符。注意如果服务端丢弃了报文(因为响应校验失败),NAS 侧的表现是"超时"而不是"拒绝",这个现象很有特征性。
- 用户名的域后缀:很多设备会默认把域后缀附加到用户名上(比如配了默认域),服务端收到的是
zhangsan@corp而本地用户是zhangsan。在服务端配置用户名归一化规则,或者让设备不要附加。 - 用户名大小写:部分服务端是区分大小写的,而设备侧可能做了转换。统一约定一种形式。
- 认证协议不匹配:如果设备配成了 CHAP 或 MS-CHAP,用户口令就不再是 User-Password 明文可拆的形式,拼接的动态口令根本取不出来。OTP 的拼接校验模式必须要求设备使用 PAP。这是一个非常高频的坑:设备默认用了 CHAP,怎么调都失败。
9.6 现象:挑战模式下,第二轮提交后仍然失败
- State 未带回:NAS 没有把 State 属性原样返回。这是设备支持不完整导致的,只能换模式一。
- State 过期:两轮之间超过了挑战有效期(常见 60 秒)。用户输得太慢,或者网络往返太慢。可以适当放宽到 90 秒,但要记录挑战超时率作为体验指标。
- 第二轮的用户名被改写:有的设备第二轮不带 User-Name 或带了不同的格式,导致服务端匹配不上第一轮的挑战会话。
- NAS 把 Challenge 当 Reject 处理:部分老旧固件对 Code=11 处理不正确,直接判失败。这种情况只能升级固件或改模式一。
十、两种对接方式的选型建议
| 维度 | RADIUS 对接 | REST API 对接 |
|---|---|---|
| 适用场景 | 存量网络设备、堡垒机、远程接入网关、云桌面 | 自研业务系统、需要富交互的认证流程 |
| 业务改造量 | 极低,设备侧配置即可 | 需要开发接入,工作量按系统计 |
| 交互能力 | 弱,仅支持文本提示 + State | 强,可承载推送、扫码、多步流程 |
| 上下文传递 | 受限于属性字段 | 自由,可传设备指纹、地理位置等风控信息 |
| 传输安全 | 依赖共享密钥与 MD5,需额外补偿措施 | 可走 TLS 国密套件,表述更清晰 |
| 排障手段 | 抓包分析,门槛较高 | 接口日志与返回码,门槛较低 |
| 高可用 | 靠 NAS 的主备切换策略 | 靠调用方的重试与网关负载均衡 |
| 推荐策略 | 优先用于存量设备 | 优先用于新建与自研系统 |
实际项目里往往是两者并存:网络设备、堡垒机走 RADIUS,自研门户、移动端走 REST 接口,两者共用同一套令牌库、同一套策略引擎和同一份审计日志。这也是判断一个动态口令产品是否成熟的标志——不是支持多少种对接,而是多种对接背后是不是同一套内核。
十一、合规检查清单(等保2.0 视角)
动态口令作为第二因子,通常在等保2.0的"身份鉴别"控制点被检查。对接完成后,建议自查以下几条:
- 双因素确实生效:所有受控系统的登录路径都必须经过二次认证,不存在绕过路径(比如本地账号、应急账号、API 直连口),要做一次全面的路径梳理。
- 失败处理:具备失败锁定、超时退出、登录失败日志记录,日志包含时间、账号、来源地址、结果。
- 口令复杂度与更换:第一因子静态口令仍要满足复杂度要求并定期更换,不能因为上了 OTP 就放松。
- 传输保护:认证链路上的凭证不得明文传输。RADIUS 场景要说明共享密钥强度、源地址限制、Message-Authenticator 启用情况;接口场景要说明 TLS 版本与套件。
- 算法合规:涉及密码应用的系统优先使用 SM2/SM3/SM4,并保留算法配置记录作为证据。
- 审计留存:认证日志留存不少于六个月,且不能被业务管理员删除或篡改。
- 应急与恢复:有明确的令牌丢失、用户锁定、服务端故障的处置流程,并留有演练记录。
十二、常见问题 FAQ
Q1:RADIUS 和 LDAP 是什么关系,能互相替代吗?
不能。LDAP 是目录协议,负责"存账号、查账号";RADIUS 是认证协议,负责"这次登录通不通过"。实际部署里经常是 RADIUS 服务器背后再调 LDAP 去校验静态口令,两者是串联关系,不是替代关系。
Q2:为什么我的设备配好了却一直超时,服务端日志里什么都没有?
九成是 UDP 1812 没通,或者源地址不在白名单里被静默丢弃。在服务端抓包看有没有报文到达,是最快的定位方式。注意抓包时要看服务端网卡上"收到"的源地址,而不是你以为的设备地址。
Q3:动态口令的容错窗口开多大合适?
默认 ±1 步(约 ±30 秒)。开到 ±2 步可以缓解硬件令牌漂移,但要相应加强失败锁定和限流。超过 ±2 步不建议。
Q4:短信口令算不算动态口令,能不能用于合规?
短信属于"你拥有的"(手机号),形式上算第二因子,但存在 SIM 卡劫持、短信劫持、伪基站等风险,安全强度弱于 TOTP 和硬件令牌。在等保测评中通常可以被接受,但高安全场景建议优先用手机令牌或硬件令牌,短信作为兜底或应急通道。
Q5:接口超时了,能不能先放行再补校验?
不能。这是典型的"失败开放"设计,会让二次认证在网络抖动时整体失效。正确做法是拒绝并告警,同时提供受管控的应急通道。
Q6:一个用户能不能绑定多个令牌?
技术上可以,也建议允许(比如手机令牌 + 备用硬件令牌)。但要做好主备标识,审计日志里记录命中的是哪个令牌,便于溯源。
Q7:令牌丢了怎么办?
标准流程是:用户报告 → 管理员核实身份 → 旧令牌立即注销(注销要即时生效,不能排队)→ 发放应急口令或引导重新绑定 → 全链路留痕。关键点是注销必须即时,否则存在时间窗。
Q8:RADIUS 用 UDP 会不会丢包导致误判?
会。所以服务端要做请求幂等(相同 Identifier + 认证字的重复请求重放上次响应),NAS 侧要合理设置超时重传。服务端还应该监控重传率,重传率升高通常意味着网络质量下降或服务端处理变慢。
Q9:能不能只做动态口令,不要静态口令?
可以,但这变成了单因子(只有"你拥有的")。对大多数合规场景,要求的仍然是两种鉴别技术的组合,动态口令通常作为第二因子叠加在静态口令之上。
Q10:多个应用接同一个动态口令后台,会不会互相影响?
不会,前提是策略按"用户+应用"维度隔离。要注意的是失败锁定策略也要按维度隔离,否则一个应用被撞库会连累用户在其他系统被锁定。
方案参考
安当OTP是上海安当技术推出的动态口令产品,可作为双因素认证落地时的工程参考方案。其在对接层面的核心能力如下:
- 双通道对接:同时提供 RADIUS 服务端能力与 REST 校验接口,存量网络设备、堡垒机、远程接入网关走 RADIUS,自研系统与新业务走接口,共用同一套令牌库与策略引擎。
- 双模式支持:支持一次性口令直接校验(静态口令与动态口令拼接提交)与挑战响应两种模式,可按设备能力分别配置,并支持 State 的时效与一次性约束。
- 多形态令牌:支持手机令牌(扫码注册,兼容主流验证器 App)、硬件令牌、短信口令,可按人群与应用分别选择。
- 算法可配:支持 SHA1、SHA224、SHA256、SHA384、SHA512 与国密 SM3,可按应用维度分别配置,满足不同合规等级的要求。
- 安全防护:内置口令一次性消费、失败锁定(按用户与应用维度)、多级限流、幂等去重、源地址白名单等机制,接口不可用时的默认动作为拒绝。
- 可运维性:提供挑战超时率、失败原因分布、响应时延等可观测指标,支持时钟漂移自动校正,便于灰度期定位问题。
- 合规支撑:可作为等保2.0"身份鉴别"控制点中双因素认证的技术实现,并提供认证日志供审计留存。
如需进一步评估,建议先用本文第八节的灰度清单在单个非核心系统上跑一轮影子验证,把通过率、失败原因分布和响应时延三个数据摸清楚,再决定是否全量推广。