1. 程式人生 > >雙因子認證解決方案

雙因子認證解決方案

什麼叫雙因子認證?

通俗的講,一般的認證方式都是使用者名稱/密碼的方式,也就是隻有密碼這一個因子來作認證,雙因子無非是增加一個因子,增強認證的安全性。

常見解決方案

  • 簡訊方式
  • 郵件方式
  • 電話語音方式
  • 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掃描該二維碼之後,就相當於為使用者綁定了一個認證器