自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何在Spring Webflux中實(shí)現(xiàn)雙因素認(rèn)證

譯文
安全 數(shù)據(jù)安全
在本文中,我將和您討論如何在Spring Webflux中通過REST API實(shí)現(xiàn)雙因素認(rèn)證。

【51CTO.com快譯】目前,在大多情況下,尤其是對于企業(yè)級應(yīng)用、或處理敏感數(shù)據(jù)的應(yīng)用(如:財(cái)務(wù)應(yīng)用)場景中,多因素身份認(rèn)證(Multi-factor authentication,MFA)已成為了最常見的處理方法。此外,MFA也被相關(guān)法律要求在越來越多的行業(yè)中是強(qiáng)制執(zhí)行(尤其是在歐盟)。因此,如果您正在開發(fā)應(yīng)用程序,那么大概率會以某種形式啟用雙(或多)因素身份認(rèn)證。

在本文中,我將向您展示如何為使用Spring Webflux,來構(gòu)建的響應(yīng)式API,以實(shí)現(xiàn)兩因素身份認(rèn)證。該應(yīng)用主要使用電子郵件和密碼對作為第一安全因素,并采用用戶設(shè)備上應(yīng)用程序(如:Google Authenticator)所生成的一次性代碼(TOTP)作為第二安全因素。

兩因素身份認(rèn)證是如何工作的

從技術(shù)上講,兩(或多)因素身份認(rèn)證是一個(gè)安全過程,用戶必須提供兩個(gè)或更多安全因素來讓自己得到認(rèn)證。也就是說,用戶需要提供除密碼以外的另一個(gè)標(biāo)識符,例如:一次性密碼、硬件令牌、生物特征(如:指紋)等。

該安全過程涉及到如下步驟:

  • 用戶輸入電子郵件(用戶名)和密碼。
  • 除了第一憑據(jù),用戶還要提交由認(rèn)證應(yīng)用生成的一次性代碼。
  • 應(yīng)用程序在對電子郵件(用戶名)和密碼進(jìn)行身份認(rèn)證的同時(shí),也使用在注冊過程中頒發(fā)的用戶密鑰來認(rèn)證一次性代碼

由此可見,與使用短信傳遞口令代碼相比,使用諸如Google Authenticator、Microsoft Authenticator、以及FreeOTP等身份認(rèn)證應(yīng)用,既能夠避免SIM卡遭受攻擊(請參見--

https://www.theverge.com/2017/6/17/15772142/how-to-set-up-two-factor-authentication),又能夠無需蜂窩網(wǎng)絡(luò)或互聯(lián)網(wǎng)連接,進(jìn)行正常認(rèn)證。

應(yīng)用示例

下面,我們將逐步構(gòu)建一個(gè)使用兩因素身份認(rèn)證技術(shù)的簡單REST API。該API要求用戶提供電子郵件密碼對,和由應(yīng)用生成的短代碼。在此,我使用Android版的Google Authenticator來生成TOTP。其源代碼的github庫鏈接為--

https://github.com/mednikoviurii/spring-twofactor-example。該應(yīng)用會用到JDK 11、Maven、以及用于存儲用戶個(gè)人信息的MongoDB。其項(xiàng)目組織結(jié)構(gòu)如下圖所示:

應(yīng)用示例的項(xiàng)目結(jié)構(gòu)

在此,我不會遍歷地介紹每一個(gè)組成部分,而只會專注于AuthService、TokenManager和TotpManager。這些部分主要負(fù)責(zé)身份的認(rèn)證流程。它們分別提供了以下功能:

  • AuthService –該組件主要用于存儲、認(rèn)證和授權(quán)所有的業(yè)務(wù)邏輯,其中包括:注冊、登錄和令牌認(rèn)證。
  • TokenManager–該組件通過抽象代碼,以生成和認(rèn)證JWT令牌。它能夠使得主要業(yè)務(wù)邏輯的實(shí)現(xiàn)與具體的JWT庫相互獨(dú)立。在此,我使用是Nimbus JOSE-JWT(請參見--https://connect2id.com/products/nimbus-jose-jwt/examples)。
  • TotpManager–作為另一種抽象,它能夠?qū)?shí)現(xiàn)與基本邏輯相隔離。TotpManager既可被用于生成用戶的密鑰,又可以斷言(assert,可以立即為驗(yàn)證)給出的短代碼。在此,我使用的是TOTP Java庫(https://github.com/samdjstevens/java-totp)來實(shí)現(xiàn),當(dāng)然您也可以選用其他的庫。

由于在此僅關(guān)注認(rèn)證組件,因此我們將從用戶的創(chuàng)建過程(注冊)開始,同時(shí)涉及到密鑰的生成和令牌的頒發(fā)。接著,我們將進(jìn)入登錄流程,涉及一個(gè)由用戶提供的短代碼的斷言。

實(shí)現(xiàn)注冊流程

下面,我們將完成一個(gè)注冊的過程,其中涉及以下步驟:

  • 從客戶端獲取注冊請求。
  • 檢查該用戶是否存在。
  • 對密碼進(jìn)行哈希。
  • 生成一個(gè)密鑰。
  • 將用戶存儲到數(shù)據(jù)庫中。
  • 頒發(fā)JWT。
  • 返回帶有用戶ID、私鑰和令牌的響應(yīng)。

我將主要的業(yè)務(wù)邏輯(AuthServiceImpl)與令牌的生成,以及密鑰的產(chǎn)生分離開來。

一般步驟

主要組件AuthServiceImpl會接受SignupRequest,并返回SignupResponse。在后臺,它負(fù)責(zé)整個(gè)注冊的邏輯。下面是具體的實(shí)現(xiàn)代碼:

Java

  1. 1.  @Override 
  2. 2.  public Mono<SignupResponse> signup(SignupRequest request) { 
  3. 3.  // generating a new user entity params 
  4. 4.  // step 1 
  5. 5.  String email = request.getEmail().trim().toLowerCase(); 
  6. 6.  String password = request.getPassword(); 
  7. 7.  String salt = BCrypt.gensalt(); 
  8. 8.  String hash = BCrypt.hashpw(password, salt); 
  9. 9.  String secret = totpManager.generateSecret(); 
  10. 10. User user = new User(null, email, hash, salt, secret); 
  11. 11. // preparing a Mono 
  12. 12. Mono<SignupResponse> response = repository.findByEmail(email) 
  13. 13.         .defaultIfEmpty(user) // step 2 
  14. 14.         .flatMap(result -> { 
  15. 15.             // assert, that user does not exist 
  16. 16.             // step 3 
  17. 17.             if (result.getUserId() == null) { 
  18. 18.                 // step 4 
  19. 19.                 return repository.save(result).flatMap(result2 -> { 
  20. 20.                     // prepare token 
  21. 21.                     // step 5 
  22. 22.                     String userId = result2.getUserId(); 
  23. 23.                     String token = tokenManager.issueToken(userId); 
  24. 24.                     SignupResponse signupResponse = new SignupResponse(); 
  25. 25.                     signupResponse.setUserId(userId); 
  26. 26.                     signupResponse.setSecretKey(secret); 
  27. 27.                     signupResponse.setToken(token); 
  28. 28.                     signupResponse.setSuccess(true); 
  29. 29.                    
  30. 30.                     return Mono.just(signupResponse); 
  31. 31.                 }); 
  32. 32.             } else { 
  33. 33.                 // step 6 
  34. 34.                 // scenario - user already exists 
  35. 35.                 SignupResponse signupResponse = new SignupResponse(); 
  36. 36.                 signupResponse.setSuccess(false); 
  37. 37.                
  38. 38.                 return Mono.just(signupResponse); 
  39. 39.             } 
  40. 40.         }); 
  41. 41. return response; 

下面,讓我們逐步解讀上述實(shí)現(xiàn)的過程。在邏輯判讀中:如果當(dāng)前用戶是新用戶,我們將對其進(jìn)行注冊;如果該用戶已經(jīng)存在于數(shù)據(jù)庫之中,那么我們就必須拒絕該請求。具體步驟為:

  • 我們根據(jù)請求數(shù)據(jù)創(chuàng)建一個(gè)新的用戶實(shí)體,并生成一個(gè)相應(yīng)的密鑰。
  • 如果該用戶過去不存在,則將給出的新實(shí)體作為其默認(rèn)實(shí)體。
  • 檢查存儲庫的調(diào)用結(jié)果。
  • 將用戶保存在數(shù)據(jù)庫中,并獲取其userId。
  • 頒發(fā)JWT。
  • 如果用戶已經(jīng)存在,則返回一個(gè)拒絕響應(yīng)。

相比以漏洞和安全問題而聞名的SHA函數(shù),我在此選用jBcrypt庫(請參見-- https://www.mindrot.org/projects/jBCrypt/),來產(chǎn)生各種安全的哈希和salt(鹽)。如不你不太熟悉jBcrypt的話,請參見教程--

https://dzone.com/articles/password-encryption-and-decryption-using-bcrypt,以獲取更多信息。

生成密鑰

接下來,我們需要實(shí)現(xiàn)一個(gè)用來生成新的密鑰的函數(shù)。它是由TotpManager.generateSecret()內(nèi)部抽象而來。下面是它的代碼:

Java:

  1. 1.  @Override 
  2. 2.  public String generateSecret() { 
  3. 3.    SecretGenerator generator = new DefaultSecretGenerator(); 
  4. 4.    return generator.generate(); 
  5. 5.  } 

測試

實(shí)現(xiàn)了注冊邏輯之后,我們需要測試它是否能夠按預(yù)期進(jìn)行認(rèn)證。首先,讓我們調(diào)用signup端點(diǎn)以創(chuàng)建一個(gè)新的用戶。其結(jié)果對象應(yīng)當(dāng)包含我們需要添加到應(yīng)用生成器(如:Google Authenticator)的userId、令牌和密鑰:

成功注冊

不過,我們應(yīng)當(dāng)禁止同一封電子郵件兩次進(jìn)行注冊。在此,我們通過斷言,以保證應(yīng)用在創(chuàng)建新用戶之前,去檢查現(xiàn)有的電子郵件列表:

登錄響應(yīng)對象

登錄

下面,我們來討論登錄流程。該流程包括兩個(gè)主要部分:認(rèn)證電子郵件的密碼憑據(jù),以及認(rèn)證由用戶提供的一次性代碼。和上一節(jié)一樣,我們首先介紹登錄所涉及的步驟:

  • 從客戶端獲取登錄請求。
  • 在數(shù)據(jù)庫中找到該用戶。
  • 使用請求中提供的密碼進(jìn)行斷言。
  • 斷言一次性代碼。
  • 返回帶有令牌的登錄響應(yīng)。

而JWT的生成過程與注冊的過程比較類似。

一般步驟

作為該示例的功能重點(diǎn),AuthServiceImpl.login將實(shí)現(xiàn)主要的業(yè)務(wù)邏輯。首先,我們需要通過在數(shù)據(jù)庫中請求電子郵件,來查找用戶;否則,我們需要提供帶有空字段的默認(rèn)值。也就是說,讓user.getUserId() == null,以表示該用戶并不存在,登錄流程隨即中止。

接著,我們需要斷言密碼的匹配。當(dāng)我們將密碼的哈希值存儲在數(shù)據(jù)庫中時(shí),就需要使用存儲的salt對請求中的密碼進(jìn)行哈希處理,進(jìn)而斷言這兩個(gè)值。

如果密碼匹配,我們需要使用之前存儲的密鑰值來認(rèn)證提交的代碼。認(rèn)證成功與否的結(jié)果,將在產(chǎn)生JWT和創(chuàng)建LoginResponse對象后得出。以下便是此部分的最終源代碼:

Java

  1. 1.  @Override 
  2. 2.  public Mono<LoginResponse> login(LoginRequest request) { 
  3. 3.    String email = request.getEmail().trim().toLowerCase(); 
  4. 4.    String password = request.getPassword(); 
  5. 5.    String code = request.getCode(); 
  6. 6.    Mono<LoginResponse> response = repository.findByEmail(email) 
  7. 7.    // step 1 
  8. 8.            .defaultIfEmpty(new User()) 
  9. 9.            .flatMap(user -> { 
  10. 10.               // step 2 
  11. 11.               if (user.getUserId() == null) { 
  12. 12.                   // no user 
  13. 13.                   LoginResponse loginResponse = new LoginResponse(); 
  14. 14.                   loginResponse.setSuccess(false); 
  15. 15.                  
  16. 16.                   return Mono.just(loginResponse); 
  17. 17.               } else { 
  18. 18.                   // step 3 
  19. 19.                   // user exists 
  20. 20.                   String salt = user.getSalt(); 
  21. 21.                   String secret = user.getSecretKey(); 
  22. 22.                   boolean passwordMatch = BCrypt.hashpw(password, salt).equalsIgnoreCase(user.getHash()); 
  23. 23.                  if (passwordMatch) { 
  24. 24.                      // step 4 
  25. 25.                      // password matched 
  26. 26.                      boolean codeMatched = totpManager.validateCode(code, secret); 
  27. 27.                      if (codeMatched) { 
  28. 28.                          // step 5 
  29. 29.                          String token = tokenManager.issueToken(user.getUserId()); 
  30. 30.                          LoginResponse loginResponse = new LoginResponse(); 
  31. 31.                          loginResponse.setSuccess(true); 
  32. 32.                          loginResponse.setToken(token); 
  33. 33.                          loginResponse.setUserId(user.getUserId()); 
  34. 34.                         
  35. 35.                          return Mono.just(loginResponse); 
  36. 36.                      } else { 
  37. 37.                          LoginResponse loginResponse = new LoginResponse(); 
  38. 38.                          loginResponse.setSuccess(false); 
  39. 39.                          return Mono.just(loginResponse); 
  40. 40.                      } 
  41. 41.                  } else { 
  42. 42.                      LoginResponse loginResponse = new LoginResponse(); 
  43. 43.                      loginResponse.setSuccess(false); 
  44. 44.                     
  45. 45.                      return Mono.just(loginResponse); 
  46. 46.                  } 
  47. 47.              } 
  48. 48.          }); 
  49. 49.  return response; 
  50. 50. } 

可見,后臺的邏輯步驟為:

  • 提供具有空字段的默認(rèn)用戶實(shí)體。
  • 檢查該用戶是否確實(shí)存在。
  • 從請求和salt處生成密碼的哈希,并存儲在數(shù)據(jù)庫中。
  • 斷言密鑰是否能夠確實(shí)匹配。
  • 認(rèn)證一次性代碼,并頒發(fā)JWT。

斷言一次性代碼

為了認(rèn)證由應(yīng)用生成的一次性代碼,我們必須向TOTP庫提供相應(yīng)的代碼和密鑰,并將它們保存為用戶實(shí)體的一部分。具體代碼如下:

Java

  1. 1.  @Override 
  2. 2.  public boolean validateCode(String code, String secret) { 
  3. 3.    TimeProvider timeProvider = new SystemTimeProvider(); 
  4. 4.    CodeGenerator codeGenerator = new DefaultCodeGenerator(); 
  5. 5.    CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); 
  6. 6.    return verifier.isValidCode(secret, code); 
  7. 7.  } 

測試

最后,我們可以通過測試,以認(rèn)證登錄的過程是否如期運(yùn)行。我們將由Google Authenticator生成的代碼作為登錄請求的負(fù)載,去調(diào)用login端點(diǎn)。

如下圖所示,為了檢查處密碼錯誤的情況,我們需要將進(jìn)程終止在密碼斷言階段:

由于密碼錯誤,登錄被拒絕

至此,我們已經(jīng)創(chuàng)建了一個(gè)簡單的REST API,它可以通過Spring Webflux(請參見--

https://www.mednikov.tech/two-factor-authentication-for-spring-webflux-apis/)的TOTP來提供兩因素身份認(rèn)證。如前文所述,為了更專注于身份認(rèn)證的邏輯,我們省略了所有的其他部分。

如果您對該示例的完整代碼感興趣,請參見--https://github.com/mednikoviurii/spring-twofactor-example。

參考文獻(xiàn)

  • Dhiraj Ray的《使用jBCrypt實(shí)現(xiàn)密鑰的加、解密》(2017)--https://dzone.com/articles/password-encryption-and-decryption-using-bcrypt。
  • Sanjay Patel的《如何在Spring應(yīng)用中使用Nimbus JOSE和JWT》Natural Programmer Blog(2018)--https://www.naturalprogrammer.com/blog/17852/spring-framework-nimbus-jose-jwt。
  • Scott Brady的《使用Nimbus JOSE和JWT創(chuàng)建帶有簽名的JWT》(2019)--https://www.scottbrady91.com/Kotlin/Creating-Signed-JWTs-using-Nimbus-JOSE-JWT。

原標(biāo)題:Two-Factor Authentication in Spring Webflux REST API ,作者:Yuri Mednikov

【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請注明原文譯者和出處為51CTO.com】

責(zé)任編輯:趙寧寧 來源: 51CTO
相關(guān)推薦

2022-08-01 00:08:03

雙因素認(rèn)證2FA

2024-10-17 11:24:04

2020-09-30 11:22:16

帳戶安全

2016-10-12 15:11:56

2022-03-17 08:17:45

SpringGemFire緩存數(shù)據(jù)

2017-01-19 10:47:46

公有云云計(jì)算MFA

2011-08-15 09:31:55

2016-01-15 10:58:02

2013-05-23 18:03:25

2020-04-27 09:00:00

雙因素認(rèn)證身份認(rèn)證生物識別

2015-03-02 14:48:07

2015-03-09 11:20:25

雙因素認(rèn)證寧盾DKEY

2020-07-07 07:00:00

Spring WebFREST APIReactive AP

2010-04-20 10:00:29

2024-01-05 15:10:29

2015-03-12 13:44:44

DKEY雙因素認(rèn)證寧盾

2013-03-15 14:47:13

YubiKeyUSB密鑰雙因素驗(yàn)證

2022-07-08 07:02:10

Python動態(tài)碼OTP

2023-02-09 08:01:12

核心組件非阻塞

2022-07-04 09:15:10

Spring請求處理流程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號