前言
对外网开放的后台管理系统,只用静态口令认证有几个先天问题:用户为了好记会选有特征的密码、明文传输时可被截获、内部人员能拿到密码冒用。OTP(One-Time Password,一次性密码/动态口令)是最容易落地的双因素认证增强手段,这篇整理它的原理和一套 java 实现方案。
一、静态口令的风险
- 便于记忆的密码往往有特征,容易被猜测和爆破
- 非加密传输时认证信息可被直接截获
- 合法授权者(包括内部人员)可以复制密码冒用,事后无法追责
静态口令从根本上无法确定「现在操作的人就是账户本人」。
二、OTP 的三种技术形式
OTP 是客户端与服务器通过共享秘密实现的强认证技术,由生成口令的令牌(硬件或 APP)和管理认证的服务端组成。按同步方式分三类:
| 形式 |
原理 |
典型载体 |
| 时间同步 |
令牌与服务器比对时间,每 60 秒生成一个新口令,对时钟精度和晶振频率要求高 |
硬件令牌、Google Authenticator |
| 事件同步 |
以事件次序 + 相同种子值作为输入,Hash 算出一致密码 |
计数型动态令牌 |
| 挑战/应答 |
服务端下发挑战码,令牌内置算法生成 6/8 位随机数,一次有效 |
刮刮卡、短信密码、部分动态令牌 |
时间同步实现最简单、终端普及度最高(手机 APP 即可),下面以它为主线。
三、时间同步(TOTP)的原理
客户端和服务器持有相同的密钥,基于时间基数用相同的 Hash 算法各自算出 6 位校验码。两边算出的码一致则验证通过——密钥从不上网传输,每分钟变化的只是校验码。
四、完整认证流程
三个角色:用户、手机端 APP(密钥载体+计算器)、服务端管理系统。注册时服务端生成密钥并以二维码形式交付给 APP,之后每次登录用「密码 + 6 位动态码」双因素验证。
五、java 实现
1. 用户注册:生成密钥并交付
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 1.1 生成 OTP 密钥
String secretBase32 = TotpUtil.getRandomSecretBase32(64);
// 1.2 生成供 APP 扫描的字符串,约定格式:
// otpauth://totp/[客户端显示的账户信息]?secret=[secretBase32]
String totpProtocalString = TotpUtil.generateTotpString(operCode, host, secretBase32);
// 1.3 生成二维码图片并通过邮件发送给用户
String host = "noreply@example.com";
String filePath = f_temp;
String fileName = Long.toString(System.currentTimeMillis()) + ".png";
try {
QRUtil.generateMatrixPic(totpProtocalString, 150, 150, filePath, fileName);
} catch (Exception e) {
throw new RuntimeException("生成二维码图片失败:" + e.getMessage());
}
String content = "用户名:" + operCode + "</br>"
+ "系统使用密码 + 动态口令双因素认证的方式登录。</br>"
+ "请按以下方式激活动态口令:</br>"
+ "安装 Google Authenticator 后,扫描以下二维码激活。</br>"
+ "<img src=\"cid:image\">";
emailBaseLogic.sendWithPic(email, "账户开立通知", content, filePath + "/" + fileName);
// 1.4 将用户注册信息与 OTP 密钥一起入库
|
2. 客户端:扫码获取密钥
用户用 APP 扫描邮件中的二维码,客户端获取密钥,之后基于时间每分钟算出一个 6 位校验码:
3. 登录验证:服务端算码比对
用户输入用户名、密码和 APP 上的 6 位码。服务端按用户名取出密钥,用同样的算法算出此刻的校验码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
String secretHex = "";
try {
secretHex = HexEncoding.encode(Base32String.decode(secretBase32));
} catch (Base32String.DecodingException e) {
LOGGER.error("解码" + secretBase32 + "出错,", e);
throw new RuntimeException("解码Base32出错");
}
long X = 30; // 30 秒一个周期
String steps = "0";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC")); // 必须用 UTC 时间计算
long currentTime = System.currentTimeMillis() / 1000L;
try {
long t = currentTime / X;
steps = Long.toHexString(t).toUpperCase();
while (steps.length() < 16) steps = "0" + steps;
// HmacSHA1 生成 6 位 TOTP
return generateTOTP(secretHex, steps, "6", "HmacSHA1");
} catch (final Exception e) {
LOGGER.error("生成动态口令出错:" + secretBase32, e);
throw new RuntimeException("生成动态口令出错");
}
|
比对客户端提交的码与服务端算出的码是否一致,一致则放行。生产上一般还会允许 ±1 个时间窗口的码(照顾手机时钟偏差),并对同一码做防重放标记。
总结
OTP 的安全性来自两点:密钥只通过二维码离线交付、口令一分钟一换且一次有效。落地时注意三点:服务端时间必须准(NTP)、验证要考虑时钟偏移窗口、同一动态码要用过即焚防重放。完整 demo 代码可参考开源实现 otp-demo。
参考