使用越來越廣泛的2FA雙因素認證,緣何越發受到推崇?

是Vzn呀發表於2024-11-22

大家好,我是vzn呀,又見面了。

隨著網際網路在生活方方面面的應用,日常少不了要登入各個網站或者應用、或者是銀行轉賬等需要驗證自己身份的場景。從早期的輸入賬號密碼來登入,到後來普遍開始透過手機驗證碼進行登入、或者APP掃碼進行登入,身份校驗的操作方式經歷了一輪又一輪的迭代演進。

近年來,有越來越多的網站開始推廣並引導使用者啟用所謂的2FA雙重登入驗證,比如Github就早在2022年的時候就開始引導使用者啟用2FA,近期還發出警告若使用者在2024年10月22日前未啟用2FA將被限制部分功能。而科技巨頭Apple在最近釋出的IOS18中,也亮相了全新的密碼APP並提供對於2FA場景的臨時鑑權碼(IOS中稱為驗證碼)生成的能力支援。

這個2FA究竟是何方神聖?為什麼越發得到各大公司的青睞、甚至是Apple也要親自入局?本篇文章我們就來一探究竟。

初識2FA

所謂2FA,也即雙因素認證(Two-Factor Authentication,簡稱2FA),是一種身份校驗的策略。顧名思義,所謂雙因素認證,就是你需要同時提供2方面的證據、來證明你就是你。

想要驗證一個人是否為本人,使用者提供的證據(認證因素)大體可分為3類:

分類 說明 舉例
私有秘密 一個私密的資訊,僅本人可知曉的內容,知曉此內容即認為是合法的目標身份 最為常見,比如密碼、金鑰等
生理特徵 基於人的各種獨一無二的生理特徵,來判斷並確定是否為本人 比如人臉、指紋、聲紋、虹膜等
專有物件 一個私人專屬的物理裝置或物件,持有此物件的人就是本人 身份證、手機、U盾等

任一認證因素都可以作為個人身份的認證,但又都有各自的缺點,無法做到100%可靠精準。

  1. 使用賬號密碼登入: 存在密碼洩露的風險。尤其是很多人喜歡所有賬號都同一個密碼,一旦洩露,後果不堪設想。
  2. 使用人臉識別: 應用門檻高,需要硬體層面支援,web類應用難以應用。此外,生物特徵屬於不可變更型別,如果生物特徵洩露將無法變更,後果比密碼洩露更嚴重。
  3. 使用U盾等專有物件:成本高、便捷度差,不僅要攜帶裝置,一旦丟失還很麻煩。

所以,為了儘可能的提升認證結果的可信度,彌補單一認證因素存在的弊端,2FA認證方案應用而生。透過同時使用2種認證因素進行綜合識別,來提升識別結果的可信度,保障身份認證的安全性。

話說到這裡,既然單一認證有風險,2FA可以將風險降低,那為啥不直接搞個3FA呢?豈不是更加安全嗎?這其實是軟體實現中常見的一種取捨,畢竟軟體最終是要服務於使用者使用的,還是需要關注下使用者使用的便捷度與使用體驗,所以2FA相對而言,就是在安全和便捷之間取了個折中。

2FA的形態演進

2FA並非是一個新鮮玩意,它很早就已經開始廣泛應用在各種場景中了。隨著時間的推移,其呈現形式也經歷了數次的演進,服務接入門檻降低、使用者使用的繁瑣度也大幅下降。

下面舉幾個2FA的實際應用,感受下這些年2FA技術的變革。

  • 早期網上銀行的U盾

早些年的時候,開通網上銀行的時候,銀行會提供一個類似隨身碟形狀的U盾,或者是一個密碼生成器(估計很多年輕小朋友都沒見過,也算是時代的眼淚吧~)。在需要轉賬的時候,除了要輸入自己的銀行卡號和取款密碼,還需要將U盾插入到電腦上,或者用密碼生成器生成一串數字,並將數字填入到網頁中進行雙重校驗之後,才會允許轉賬操作。

這種2FA的應用場景中,分別使用了密碼和獨立物理裝置進行綜合認證。有效的規避了密碼洩露或者U盾丟失帶來的風險(當然,U盾和密碼同時被另一個人拿到的話,就回天乏術了),保障個人資金的安全。

這種方式,雖然達到了賬號安全性的要求,但是弊端也很明顯:

  1. 實施門檻高,需要生產配套物理裝置,所以僅在銀行這種財大氣粗的行業領域中使用,很難在各行業中普遍推廣。
  2. 使用者使用繁瑣,如果裝置不在身邊或者丟失,則無法使用。而且,不同銀行之間、甚至同一個銀行的不同銀行卡之間都有配套獨立的裝置,儲存並區分也是一件很頭疼的事情。

也是由於上述的原因,現在幾乎已經看不到U盾的身影了。

  • 網站登入採用賬密+手機驗證碼方式

這個是目前比較常見的一種2FA的應用形態。比如某度網盤,輸入賬號和密碼驗證透過後,還會要求向手機傳送驗證碼,基於驗證碼進行二次身份認證透過之後,方可正常登入到系統中。

這種形態,其實算是早期的密碼+U盾裝置的一種升級方案。前面也說過了基於U盾等物理裝置進行認證的成本與使用繁瑣問題,而當前手機已經成為使用者必備且幾乎形影不離的物件,它便是U盾等裝置的最佳替代品。

基於密碼+手機驗證碼的方式,在提升認證安全性的同時,打破了對特定配套物理裝置的依賴,降低了2FA方案的落地成本與使用者使用體驗,被廣泛的應用到了各種線上身份認證的場景中,成為了當前最為主流的一種2FA認證方式。

但是利用手機驗證碼進行驗證,依舊會存在一個成本問題,畢竟傳送簡訊也是要錢的,尤其是對於一些幾億使用者體量的系統而言,即使每個使用者1個月只傳送一條簡訊,算下來也是一筆不菲的費用啊。

  • iCloud網頁版登入

apple使用者如果登入過網頁版iCloud,應該都有見過iCloud的2FA實現思路,它在驗證完使用者的賬號和密碼之後,並非是傳送簡訊驗證碼,而是向登入了此賬號的iphone裝置推送了一條彈窗通知,裡面顯示了一串隨機碼,使用者輸入iPhone接收到的隨機碼,完成身份驗證並進入到系統重。

蘋果的這種實現,藉助自身iPhone裝置的廣泛應用,構建了服務端與iPhone裝置之間的專有推送通道,完美的省掉了簡訊驗證碼傳送的費用。但,這種方案,就像網上很多人調侃蘋果的那句Only Apple Can Do一樣,還真的只有Apple等手機裝置廠商可以實現。對於普通的系統服務提供者,想要透過非簡訊途徑推送驗證碼給使用者,也至少得要使用者在手機中安裝個自己產品的APP應用,才有可能實現利用自己的通道進行點對點訊息推送,但這一條件顯然限制了該方案的推行。

總體而言,iCloud的這種做法,是一種更加經濟的2FA方案,但是技術門檻與推廣門檻極高,不具備普遍性。

  • GitHub網站登入

並不是所有公司都是手機廠商。所以是否有一種通用的、成本更低廉的2FA實現方案呢?在這個背景下,一種基於TOTP協議的2FA方案進入大眾視野中。當前很多啟用2FA的網站,使用的都是這一方案。

看下GitHub的2FA登入實現。在開啟2FA功能的時候,需要在提前在手機上安裝一個APP並繫結到GitHub賬號上。這樣後續在輸入賬號密碼之後,還需要開啟APP並將APP中生成的鑑權碼填入到介面上進行二次驗證,驗證透過之後方可進入系統。

這裡的手機上安裝的APP,也是基於TOTP協議進行開發的金鑰生成器。這一方案接入成本相對較低、更容易推廣,目前正在逐步被各類系統所支援。值得一提的是,正如本文開頭提及的訊息,在9月中旬剛剛釋出的IOS18系統中自帶了一款名為密碼的APP,其中提供了一個驗證碼功能,也正是基於TOTP協議的鑑權碼生成器,使用它生成的驗證碼也可以正常完成2FA認證。

基於TOTP的2FA

在上面介紹GitHub的2FA方案的時候,有提過其採用的是基於TOTP演算法的2FA方案。所謂TOTP,即基於時間的一次性密碼(Time-based One-Time Password,簡稱TOTP),它是一種國際標準協議(RFC6238)。其本質上就是取當前時間戳以及當前賬號的一個唯一標識(類似金鑰,服務端頒發、並提供給客戶端儲存使用),透過固定演算法加工生成一個6位數的鑑權碼,一定時間範圍內生成的鑑權碼是固定的(所以這個驗證碼會有個有效期的概念,一般是30s)。這樣只要服務端和APP端各自計算出一個驗證碼,然後比對下兩個驗證碼是否一致,即可完成校驗。


def generate_totp(secret, interval=30, digits=6, timestamp=None):
    """
    生成基於時間的一次性密碼(TOTP)。

    :param secret: 金鑰(base32 編碼的字串)
    :param interval: 時間間隔(秒)
    :param digits: 生成的 OTP 的位數
    :param timestamp: 當前時間戳(秒),預設為當前時間
    :return: 生成的 OTP 字串
    """
    if timestamp is None:
        timestamp = int(time.time())

    # 計算時間步長(時間戳除以時間間隔並取整)
    counter = timestamp // interval

    # 將時間步長轉換為位元組序列
    counter_bytes = struct.pack('>Q', counter)

    # 使用 HMAC-SHA1 生成雜湊值
    hmac_result = hmac.new(base64.b32decode(secret), counter_bytes, hashlib.sha1).digest()

    # 動態截斷以生成 OTP
    offset = hmac_result[-1] & 0xf
    binary_otp = hmac_result[offset:offset + 4]
    binary_otp = struct.unpack('>I', b'\x00' + binary_otp[1:3] + b'\x00')[0]

    # 將 OTP 格式化為指定位數的字串
    otp = str(binary_otp % 10**digits).zfill(digits)

    return otp

def main():
    # 示例金鑰(base32 編碼)
    secret = 'JBSWY3DPEHPK3PXP'

    # 生成當前時間的 TOTP
    otp = generate_totp(secret)
    print(f"Generated TOTP: {otp}")

    # 驗證生成的 OTP(假設客戶端和伺服器的時間同步)
    verified_otp = input("Enter the OTP you received: ")
    if otp == verified_otp:
        print("Verification successful!")
    else:
        print("Verification failed!")

if __name__ == "__main__":
    main()

基於上面介紹,可以看出,基於TOTP演算法生成驗證碼有兩個輸入因子:時間和使用者金鑰。服務端和客戶端除了需使用相同的加密演算法,還需要保證傳入相同的時間戳和使用者金鑰,才能保證生成的校驗碼相同。如何保證服務端和客戶端裝置,可以獲取到相同的引數值呢?下面簡單介紹下。

  • 時間因子

服務端和客戶端在計算生成驗證碼的時候,各自取自身裝置本地當前時間作為時間引數。因為如今的智慧手機和伺服器都支援基於網路的時鐘校準能力,所以可以很輕鬆的保證手機終端與應用服務端本地時間的基本一致。

  • 使用者金鑰因子

使用者金鑰因子是服務端為使用者生成的授權金鑰,計算生成驗證碼的時候,服務端和客戶端都要使用同一個金鑰進行計算,所以服務端為使用者生成金鑰後,除了服務端要儲存該金鑰與使用者的繫結關係,還需要將此金鑰提供給使用者、由使用者將其繫結到手機上的TOTP軟體中(所謂繫結,本質上就是將服務端生成的金鑰儲存到手機的TOTP軟體中)。繫結完成後,服務端和客戶端就都擁有相同的使用者金鑰資訊了。

至此,TOTP演算法所需的2個關鍵引數都已具備,就可以使用TOTP應用生成的驗證碼進行身份二次認證咯。

講到這裡,小夥伴們可能會有個疑惑,假如使用者更換了新手機,新手機上安裝的TOTP軟體並沒有繫結對應的使用者金鑰資訊,那不就沒法登入了嗎?

這就要再回到開啟2FA認證的時候,服務端除了會生成一個金鑰(類似公鑰)提供給客戶端進行繫結,還會同時提供一份Recovery Code,會提示使用者將其可靠儲存起來。

目前市面上主流的基於TOTP的客戶端軟體,主要有2個:

  • Google Authenticator
  • MicroSoft Authenticator

當然,現在又多了個Apple Password,以後可能會形成三足鼎立的局面。

服務中整合2FA能力

如果需要在服務端開啟2FA能力,需要整合實現對應的TOTP金鑰演算法即可。以JAVA為例,可以透過整合現有的第三方庫來快捷實現,常用的有com.warrenstrange.googleauth庫:

<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>{version}</version>
</dependency>

程式碼中直接使用其提供的api介面即可,下面程式碼演示下api介面的使用方式:

import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.IKey;
import com.warrenstrange.googleauth.KeyGenerator;

import java.time.Instant;

public class TOTPExample {
    public static void main(String[] args) {
        // 生成一個金鑰、可以指定金鑰長度,例如20位元組
        IKey key = KeyGenerator.getKey(20); 
        String secretKey = key.getKey();
        // 計算當前時間的時間視窗
        long currentTime = Instant.now().getEpochSecond();
        int timeStep = 30; 
        long timeWindow = currentTime / timeStep;
        // 生成TOTP
        GoogleAuthenticator ga = new GoogleAuthenticator();
        String totp = ga.getTotpPassword(secretKey, timeWindow);
        System.out.println("Generated TOTP: " + totp);

        // 校驗驗證碼是否符合預期
        boolean isValid = ga.authorize(totp, secretKey, timeWindow);
        System.out.println("TOTP Validation: " + (isValid ? "Success!" : "Failed!"));

        // 考慮到網路延遲等因素,可以允許一定的時間誤差。例如,驗證時允許前後一個時間視窗的誤差
        boolean isValidWithTolerance = false;
        for (int i = -1; i <= 1; i++) {
            long toleranceWindow = timeWindow + i;
            if (ga.authorize(totp, secretKey, toleranceWindow)) {
                isValidWithTolerance = true;
                break;
            }
        }
        System.out.println("TOTP Validation with Tolerance: " + (isValidWithTolerance ? "Success!" : "Failed!"));
    }
}

總結

好咯,關於雙因素認證的內容,就聊到這裡,相信小夥伴們對2FA已經有所瞭解了吧。隨著網際網路時代對個人資訊與隱私的日益重視、以及現今網路安全形勢的日益嚴峻,使用者身份認證的技術方案與實現形態也在不斷地發展與最佳化,相信2FA在後續也會得到更大的普及。關於2FA或者使用者身份驗證的方案,小夥伴們是否有自己的見解呢,歡迎留言交流。

我是vzn呀,聊技術、又不僅僅聊技術~

如果覺得有用,請點個關注,也可以關注下我的公眾號【是vzn呀】,獲取更及時的更新。

期待與你一起探討,一起成長為更好的自己。

相關文章