App下載

Java 為您的 WEB 應(yīng)用程序啟用兩步驗(yàn)證

執(zhí)手不憶殤 2021-09-24 10:33:06 瀏覽數(shù) (2468)
反饋

支持雙因素身份驗(yàn)證 (2FA) 幾乎總是一個(gè)好主意,尤其是對(duì)于后臺(tái)系統(tǒng)。2FA 有許多不同的形式,其中一些包括 SMS、TOTP 甚至硬件令牌。

啟用它們需要類(lèi)似的流程:

  • 用戶(hù)轉(zhuǎn)到他們的個(gè)人資料頁(yè)面(如果您想在注冊(cè)時(shí)強(qiáng)制使用 2fa,請(qǐng)?zhí)^(guò)此頁(yè)面)
  • 單擊“啟用雙因素身份驗(yàn)證”
  • 輸入一些數(shù)據(jù)以啟用特定的 2FA 方法(電話號(hào)碼、TOTP 驗(yàn)證碼等)
  • 下次登錄時(shí),除了用戶(hù)名和密碼外,登錄表單還會(huì)請(qǐng)求第二個(gè)因素(驗(yàn)證碼)并將其與憑據(jù)一起發(fā)送

我將重點(diǎn)介紹 Google Authenticator,它使用 TOTP(基于時(shí)間的一次性密碼)來(lái)生成一系列驗(yàn)證碼。這個(gè)想法是服務(wù)器和客戶(hù)端應(yīng)用程序共享一個(gè)密鑰。根據(jù)該鍵和當(dāng)前時(shí)間,兩者都得出相同的代碼。當(dāng)然,時(shí)鐘不是完全同步的,所以有一些代碼窗口被服務(wù)器接受為有效的。請(qǐng)注意,如果您不信任 Google 的應(yīng)用程序,您可以使用下面的相同庫(kù)實(shí)現(xiàn)您自己的客戶(hù)端應(yīng)用程序。

如何使用 Java(在服務(wù)器上)實(shí)現(xiàn)它?使用?GoogleAuth? 庫(kù)。流程如下:

  • 用戶(hù)轉(zhuǎn)到他們的個(gè)人資料頁(yè)面
  • 單擊“啟用雙因素身份驗(yàn)證”
  • 服務(wù)器生成一個(gè)密鑰,將其存儲(chǔ)為用戶(hù)配置文件的一部分,并將 URL 返回到二維碼。請(qǐng)注意,最好對(duì)機(jī)密進(jìn)行加密,以使其更難被數(shù)據(jù)泄露破壞。
  • 用戶(hù)使用他們的 Google Authenticator 應(yīng)用程序掃描二維碼,從而在應(yīng)用程序中創(chuàng)建一個(gè)新的個(gè)人資料
  • 用戶(hù)在與二維碼一起出現(xiàn)的字段中輸入應(yīng)用顯示的驗(yàn)證碼,然后點(diǎn)擊“確認(rèn)”
  • 服務(wù)器在用戶(hù)配置文件中將 2FA 標(biāo)記為已啟用
  • 或者,您可以給用戶(hù)一些“臨時(shí)代碼”,他們可以寫(xiě)下這些代碼,以防他們丟失應(yīng)用程序或機(jī)密。
  • 如果用戶(hù)不掃描代碼或不驗(yàn)證過(guò)程,用戶(hù)配置文件將只包含一個(gè)孤立的密鑰,但不會(huì)被標(biāo)記為已啟用
  • 應(yīng)該有一個(gè)選項(xiàng)可以稍后從他們的用戶(hù)個(gè)人資料頁(yè)面禁用 2FA

從理論的角度來(lái)看,這里最重要的一點(diǎn)是密鑰的共享。加密是對(duì)稱(chēng)的,因此雙方(身份驗(yàn)證器應(yīng)用程序和服務(wù)器)具有相同的密鑰。它通過(guò)用戶(hù)掃描的二維碼共享。如果攻擊者在那時(shí)控制了用戶(hù)的機(jī)器,則機(jī)密可能會(huì)泄露,因此 2FA 也會(huì)被攻擊者濫用。但這不在威脅模型中——換句話說(shuō),如果攻擊者可以訪問(wèn)用戶(hù)的機(jī)器,那么損害就已經(jīng)造成了。

注意:您可能會(huì)看到此過(guò)程稱(chēng)為 2 步身份驗(yàn)證或 2 因素?!耙蛩亍笔牵骸澳阒赖臇|西”、“你擁有的東西”和“你是的東西”。您可以將 TOTP 視為“您知道”的另一件事,但您也可以將帶有安全存儲(chǔ)的密鑰的手機(jī)視為“您擁有”的東西。在這種特殊情況下,我不堅(jiān)持使用任何一個(gè)術(shù)語(yǔ)。

登錄后,流程如下:

  • 用戶(hù)輸入用戶(hù)名和密碼,點(diǎn)擊“登錄”
  • 頁(yè)面使用 AJAX 請(qǐng)求詢(xún)問(wèn)服務(wù)器此電子郵件是否啟用了 2FA
  • 如果未啟用 2FA,只需提交用戶(hù)名和密碼表格
  • 如果啟用了 2FA,則不會(huì)提交登錄表單,而是會(huì)顯示一個(gè)附加字段,讓用戶(hù)從身份驗(yàn)證器應(yīng)用程序輸入驗(yàn)證碼
  • 用戶(hù)輸入代碼并按登錄后,即可提交表單。使用相同的登錄按鈕,或新的“驗(yàn)證”按鈕,或者驗(yàn)證輸入 + 按鈕可以是一個(gè)全新的屏幕(隱藏用戶(hù)名/密碼輸入)。
  • 然后服務(wù)器再次檢查用戶(hù)是否啟用了 2FA,如果是,則驗(yàn)證驗(yàn)證碼。如果匹配,則登錄成功。如果不是,則登錄失敗,并且允許用戶(hù)重新輸入憑據(jù)和驗(yàn)證碼。請(qǐng)注意,根據(jù)用戶(hù)名/密碼是否錯(cuò)誤或代碼錯(cuò)誤,您可以有不同的響應(yīng)。您也可以在顯示驗(yàn)證碼輸入之前嘗試登錄。這種方式可以說(shuō)更好,因?yàn)檫@樣您就不會(huì)向潛在攻擊者透露用戶(hù)使用 2FA。

雖然我說(shuō)的是用戶(hù)名和密碼,但它可以適用于任何其他身份驗(yàn)證方法。從 ?OAuth/OpenID Connect/SAML? 提供程序獲得成功確認(rèn)后,或者在獲得來(lái)自?SecureLogin?的令牌后,您可以請(qǐng)求第二個(gè)因素(代碼)。

在代碼中,上述流程如下所示(使用 Spring MVC;為了簡(jiǎn)潔起見(jiàn),我合并了控制器和服務(wù)層。您可以將 ?@AuthenticatedPrincipal? 位替換為您將當(dāng)前登錄的用戶(hù)詳細(xì)信息提供給控制器的方式)。假設(shè)方法在映射到“/user/”的控制器中:

@RequestMapping(value ="/init2fa", method = RequestMethod.POST)
@ResponseBody
public String initTwoFactorAuth(@AuthenticationPrincipal LoginAuthenticationToken token) {
    User user = getLoggedInUser(token);
    GoogleAuthenticatorKey googleAuthenticatorKey = googleAuthenticator.createCredentials();
    // note - preferably encrypt it with an externally stored (or even HSM) key
    user.setTwoFactorAuthKey(googleAuthenticatorKey.getKey());
    dao.update(user);
    return GoogleAuthenticatorQRGenerator.getOtpAuthURL(GOOGLE_AUTH_ISSUER, email, googleAuthenticatorKey);
}
 
@RequestMapping(value ="/confirm2fa", method = RequestMethod.POST)
@ResponseBody
public boolean confirmTwoFactorAuth(@AuthenticationPrincipal LoginAuthenticationToken token,@RequestParam("code")int code) {
    User user = getLoggedInUser(token);
    boolean result = googleAuthenticator.authorize(user.getTwoFactorAuthKey(), code);
    user.setTwoFactorAuthEnabled(result);
    dao.update(user);
    return result;
}
 
@RequestMapping(value ="/disable2fa", method = RequestMethod.GET)
@ResponseBody
public void disableTwoFactorAuth(@AuthenticationPrincipal LoginAuthenticationToken token) {
    User user = getLoggedInUser(token);
    user.setTwoFactorAuthKey(null);
    user.setTwoFactorAuthEnabled(false);
    dao.update(user);
}
 
@RequestMapping(value ="/requires2fa", method = RequestMethod.POST)
@ResponseBody
public boolean login(@RequestParam("email") String email) {
    // TODO consider verifying the password here in order not to reveal that a given user uses 2FA
    return userService.getUserDetailsByEmail(email).isTwoFactorAuthEnabled();
}

二維碼生成使用 Google 的服務(wù),從技術(shù)上講,該服務(wù)也為 Google 提供了密鑰。我懷疑他們除了生成二維碼之外還存儲(chǔ)它,但是如果您不信任他們,您可以實(shí)現(xiàn)自己的二維碼生成器,自己生成二維碼應(yīng)該不難。

在客戶(hù)端,它是對(duì)上述方法的簡(jiǎn)單 AJAX 請(qǐng)求(旁注:我有點(diǎn)覺(jué)得 AJAX 一詞不再流行,但我不知道如何調(diào)用它們。異步?背景?Javascript?)。

$("#two-fa-init").click(function() {
    $.post("/user/init2fa",function(qrImage) {
    $("#two-fa-verification").show();
    $("#two-fa-qr").prepend($('<img>',{id:'qr',src:qrImage}));
    $("#two-fa-init").hide();
    });
});
 
$("#two-fa-confirm").click(function() {
    var verificationCode = $("#verificationCode").val().replace(/ /g,'')
    $.post("/user/confirm2fa?code=" + verificationCode,function() {
       $("#two-fa-verification").hide();
       $("#two-fa-qr").hide();
       $.notify("Successfully enabled two-factor authentication","success");
       $("#two-fa-message").html("Successfully enabled");
    });
});
 
$("#two-fa-disable").click(function() {
    $.post("/user/disable2fa",function(qrImage) {
       window.location.reload();
    });
});

登錄表單代碼在很大程度上取決于您正在使用的現(xiàn)有登錄表單,但重點(diǎn)是使用電子郵件(和密碼)調(diào)用 /requires2fa 以檢查是否啟用了 2FA,然后顯示驗(yàn)證碼輸入。

總的來(lái)說(shuō),如果雙因素身份驗(yàn)證的實(shí)現(xiàn)很簡(jiǎn)單,我建議將它用于大多數(shù)系統(tǒng),在這些系統(tǒng)中,安全性比用戶(hù)體驗(yàn)的簡(jiǎn)單性更重要。


0 人點(diǎn)贊