什么叫雙因子認證?
通俗的講,一般的認證方式都是用戶名/密碼的方式,也就是只有密碼這一個因子來作認證,雙因子無非是增加一個因子,增強認證的安全性。
常見解決方案
- 短信方式
- 郵件方式
- 電話語音方式
- TOTP解決方案
前三種方案,其實都大同小異。Server端通過某種算法生成一段隨機密碼,通過短信、郵件或者電話的方式傳遞給用戶,用戶把隨機密碼作為登錄的憑證傳遞給Server,Server驗證通過之后,就完成了一次雙因子認證。但是短信和電話語音對於運營公司是有一定的成本的,除此之外有些非互聯網的應用可能並不通公網,這種情況下,TOTP不失為一種好的雙因子認證的解決方案。
什么是TOTP?
是Time-based One-Time Password的簡寫,表示基於時間戳算法的一次性密碼。
如果大家玩過夢幻西游的話,那么對將軍令
應該不陌生,這個就是基於TOTP的一個產物。
OTP
介紹TOTP之前,先介紹下OTP
One-Time Password的簡寫,表示一次性密碼。
OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
其中,K代表密鑰串;C是一個數字,表示隨機數;HMAC-SHA-1表示用SHA-1做HMAC;
Truncate是一個函數,用於截取加密后的串,並取加密后串的一些字段組成一個數字。
對HMAC-SHA-1方式加密來說,Truncate實現如下:
- HMAC-SHA-1加密后的長度得到一個20字節的密串;
- 取這個20字節的密串的最后一個字節,取這字節的低4位,作為截取加密串的下標偏移量;
- 按照下標偏移量開始,獲取4個字節,以大端(把高位字節放在低位地址)的方式組成一個整數;
- 截取這個整數的后6位或者8位轉成字符串返回。
public static String generateOTP(String K,
String C,
String returnDigits,
String crypto){
int codeDigits = Integer.decode(returnDigits).intValue();
String result = null;
// K是密碼
// C是產生的隨機數
// crypto是加密算法 HMAC-SHA-1
byte[] hash = hmac_sha(crypto, K, C);
// hash為20字節的字符串
// put selected bytes into result int
// 獲取hash最后一個字節的低4位,作為選擇結果的開始下標偏移
int offset = hash[hash.length - 1] & 0xf;
// 獲取4個字節組成一個整數,其中第一個字節最高位為符號位,不獲取,使用0x7f
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
// 獲取這個整數的后6位(可以根據需要取后8位)
int otp = binary % 1000000;
// 將數字轉成字符串,不夠6位前面補0
result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
返回的結果就是看到一個數字的動態密碼。
HOTP
知道了OTP的基本原理,HOTP只是將其中的參數C變成了隨機數
公式修改一下
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
HOTP: Generates the OTP for the given count
即:C作為一個參數,獲取動態密碼。
一般規定HOTP的散列函數使用SHA2,即:基於SHA-256 or SHA-512 [SHA2] 的散列函數做事件同步驗證;
TOTP詳解
TOTP只是將其中的參數C變成了由時間戳產生的數字。
TOTP(K,C) = HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
不同點是TOTP中的C是時間戳計算得出。
C = (T - T0) / X;
T 表示當前Unix時間戳
T0一般取值為 0.
X 表示時間步數,也就是說多長時間產生一個動態密碼,這個時間間隔就是時間步數X,系統默認是30秒;
例如:
T0 = 0;
X = 30;
T = 30 ~ 59, C = 1; 表示30 ~ 59 這30秒內的動態密碼一致。
T = 60 ~ 89, C = 2; 表示30 ~ 59 這30秒內的動態密碼一致。
不同廠家使用的時間步數不同;
- 阿里巴巴的身份寶使用的時間步數是60秒;
- 寧盾令牌使用的時間步數是60秒;
- Google的 身份驗證器的時間步數是30秒;
- 騰訊的Token時間步數是60秒;
應用
客戶端的實現有很多,上面已經列出來了。而服務端的實現庫比較少,貌似也都是非官方的實現。這里推薦一個JAVA的實現庫,這是一個私人的庫,介意的朋友只能自己擼輪子了。
這里基於上述的實現庫,給出一段demo代碼,僅供參考。
package com.github.chenqimiao.util;
import java.text.MessageFormat;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorConfig;
import com.warrenstrange.googleauth.GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @Auther: chenqimiao
* @Date: 2019/8/26 22:58
* @Description: refer https://github.com/wstrange/GoogleAuth
*/
@Slf4j
public class GoogleAuthenticatorUtils {
// 前綴
private static final String DEFAULT_USER_PREFIX = "TOTP_USER:";
// 用戶名|密鑰|發行者
public static final String QRCODE_TEMPLATE = "otpauth://totp/" + DEFAULT_USER_PREFIX + "{0}?secret={1}&issuer={2}";
// 默認的發行者
public static final String DEFAULT_ISSUER = "DAS_TOTP";
private static final GoogleAuthenticatorConfig DEFAULT_CONFIG;
static {
GoogleAuthenticatorConfigBuilder builder = new GoogleAuthenticatorConfigBuilder();
// Do something here if you want to set config for GoogleAuthenticator
DEFAULT_CONFIG = builder.build();
}
public static String createQrCodeContent(String username, String secret) {
return createQrCodeContent(username, secret, DEFAULT_ISSUER);
}
public static String createQrCodeContent(String username, String secret, String issuer) {
return MessageFormat.format(QRCODE_TEMPLATE, username, secret, issuer);
}
public static String createSecret() {
return createSecret(DEFAULT_CONFIG);
}
public static String createSecret(GoogleAuthenticatorConfig config) {
GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
final GoogleAuthenticatorKey key = gAuth.createCredentials();
return key.getKey();
}
public static boolean verify(Integer totpPwd, String secret) {
return verify(totpPwd, secret, DEFAULT_CONFIG);
}
public static boolean verify(Integer totpPwd, String secret, GoogleAuthenticatorConfig config) {
GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
return gAuth.authorize(secret, totpPwd);
}
public static Integer getTotpPassword(String secret) {
return getTotpPassword(secret, DEFAULT_CONFIG);
}
public static Integer getTotpPassword(String secret, GoogleAuthenticatorConfig config) {
GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
return gAuth.getTotpPassword(secret);
}
@SneakyThrows
public static void main(String args[]) {
String secret = createSecret();
String qrcodeContent = createQrCodeContent("chenqimiao", secret);
System.out.println("qrcodeContent is " + qrcodeContent);
Integer totpPwd = getTotpPassword(secret);
System.out.println("Current totp password is " + totpPwd);
boolean result = verify(totpPwd, secret);
System.out.println("result is " + result);
}
qrcodeContent可以通過二維碼工具生成二維碼,使用Google Authenticator掃描該二維碼之后,就相當於為用戶綁定了一個認證器。