通俗的講,通常的認證方式都是用戶名/密碼的方式,也就是隻有密碼這一個因子來做認證,雙因子無非是增長一個因子,加強認證的安全性。java
前三種方案,其實都大同小異。Server端經過某種算法生成一段隨機密碼,經過短信、郵件或者電話的方式傳遞給用戶,用戶把隨機密碼做爲登陸的憑證傳遞給Server,Server驗證經過以後,就完成了一次雙因子認證。可是短信和電話語音對於運營公司是有必定的成本的,除此以外有些非互聯網的應用可能並不通公網,這種狀況下,TOTP不失爲一種好的雙因子認證的解決方案。git
是Time-based One-Time Password的簡寫,表示基於時間戳算法的一次性密碼。github
若是你們玩過夢幻西遊的話,那麼對將軍令
應該不陌生,這個就是基於TOTP的一個產物。算法
介紹TOTP以前,先介紹下OTP
安全
One-Time Password的簡寫,表示一次性密碼。app
OTP(K,C) = Truncate(HMAC-SHA-1(K,C))
函數
其中,K表明密鑰串;C是一個數字,表示隨機數;HMAC-SHA-1表示用SHA-1作HMAC;工具
Truncate是一個函數,用於截取加密後的串,並取加密後串的一些字段組成一個數字。ui
對HMAC-SHA-1方式加密來講,Truncate實現以下:google
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; }
返回的結果就是看到一個數字的動態密碼。
知道了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只是將其中的參數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秒內的動態密碼一致。
不一樣廠家使用的時間步數不一樣;
客戶端的實現有不少,上面已經列出來了。而服務端的實現庫比較少,貌似也都是非官方的實現。這裏推薦一個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掃描該二維碼以後,就至關於爲用戶綁定了一個認證器。