黑客24小时在线接单网站

黑客24小时在线接单网站,黑客接单,接单网站,黑客入口

如何在Spring Webflux中实现双因素认证

【51CTO.com快译】目前,在大多情况下,尤其是对于企业级应用、或处理敏感数据的应用(如:财务应用)场景中,多因素身份认证(Multi-factor authentication,MFA)已成为了最常见的处理方法。此外,MFA也被相关法律要求在越来越多的行业中是强制执行(尤其是在欧盟)。因此,如果您正在开发应用程序,那么大概率会以某种形式启用双(或多)因素身份认证。

在本文中,我将向您展示如何为使用Spring Webflux,来构建的响应式API,以实现两因素身份认证。该应用主要使用电子邮件和密码对作为第一安全因素,并采用用户设备上应用程序(如:Google Authenticator)所生成的一次性代码(TOTP)作为第二安全因素。

两因素身份认证是如何工作的

从技术上讲,两(或多)因素身份认证是一个安全过程,用户必须提供两个或更多安全因素来让自己得到认证。也就是说,用户需要提供除密码以外的另一个标识符,例如:一次性密码、硬件令牌、生物特征(如:指纹)等。

该安全过程涉及到如下步骤:

  • 用户输入电子邮件(用户名)和密码。
  • 除了第一凭据,用户还要提交由认证应用生成的一次性代码。
  • 应用程序在对电子邮件(用户名)和密码进行身份认证的同时,也使用在注册过程中颁发的用户密钥来认证一次性代码
由此可见,与使用短信传递口令代码相比,使用诸如Google Authenticator、Microsoft Authenticator、以及FreeOTP等身份认证应用,既能够避免SIM卡遭受攻击(请参见--

https://www.theverge.com/2017/6/17/15772142/how-to-set-up-two-factor-authentication),又能够无需蜂窝网络或互联网连接,进行正常认证。

应用示例

下面,我们将逐步构建一个使用两因素身份认证技术的简单REST API。该API要求用户提供电子邮件密码对,和由应用生成的短代码。在此,我使用Android版的Google Authenticator来生成TOTP。其源代码的github库链接为--

https://github.com/mednikoviurii/spring-twofactor-example。该应用会用到JDK 11、Maven、以及用于存储用户个人信息的MongoDB。其项目组织结构如下图所示:

应用示例的项目结构

在此,我不会遍历地介绍每一个组成部分,而只会专注于AuthService、TokenManager和TotpManager。这些部分主要负责身份的认证流程。它们分别提供了以下功能:

  • AuthService –该组件主要用于存储、认证和授权所有的业务逻辑,其中包括:注册、登录和令牌认证。
  • TokenManager–该组件通过抽象代码,以生成和认证JWT令牌。它能够使得主要业务逻辑的实现与具体的JWT库相互独立。在此,我使用是Nimbus JOSE-JWT(请参见--https://connect2id.com/products/nimbus-jose-jwt/examples)。
  • TotpManager–作为另一种抽象,它能够将实现与基本逻辑相隔离。TotpManager既可被用于生成用户的密钥,又可以断言(assert,可以立即为验证)给出的短代码。在此,我使用的是TOTP Java库(https://github.com/samdjstevens/java-totp)来实现,当然您也可以选用其他的库。
由于在此仅关注认证组件,因此我们将从用户的创建过程(注册)开始,同时涉及到密钥的生成和令牌的颁发。接着,我们将进入登录流程,涉及一个由用户提供的短代码的断言。

实现注册流程

下面,我们将完成一个注册的过程,其中涉及以下步骤:

  • 从客户端获取注册请求。
  • 检查该用户是否存在。
  • 对密码进行哈希。
  • 生成一个密钥。
  • 将用户存储到数据库中。
  • 颁发JWT。
  • 返回带有用户ID、私钥和令牌的响应。
我将主要的业务逻辑(AuthServiceImpl)与令牌的生成,以及密钥的产生分离开来。

一般步骤

主要组件AuthServiceImpl会接受SignupRequest,并返回SignupResponse。在后台,它负责整个注册的逻辑。下面是具体的实现代码:

Java

  • 1.@Override
  • 2.publicMono<SignupResponse>signup(SignupRequestrequest){
  • 3.//generatinganewuserentityparams
  • 4.//step1
  • 5.Stringemail=request.getEmail().trim().toLowerCase();
  • 6.Stringpassword=request.getPassword();
  • 7.Stringsalt=BCrypt.gensalt();
  • 8.Stringhash=BCrypt.hashpw(password,salt);
  • 9.Stringsecret=totpManager.generateSecret();
  • 10.Useruser=newUser(null,email,hash,salt,secret);
  • 11.//preparingaMono
  • 12.Mono<SignupResponse>response=repository.findByEmail(email)
  • 13..defaultIfEmpty(user)//step2
  • 14..flatMap(result->{
  • 15.//assert,thatuserdoesnotexist
  • 16.//step3
  • 17.if(result.getUserId()==null){
  • 18.//step4
  • 19.returnrepository.save(result).flatMap(result2->{
  • 20.//preparetoken
  • 21.//step5
  • 22.StringuserId=result2.getUserId();
  • 23.Stringtoken=tokenManager.issueToken(userId);
  • 24.SignupResponsesignupResponse=newSignupResponse();
  • 25.signupResponse.setUserId(userId);
  • 26.signupResponse.setSecretKey(secret);
  • 27.signupResponse.setToken(token);
  • 28.signupResponse.setSuccess(true);
  • 29.
  • 30.returnMono.just(signupResponse);
  • 31.});
  • 32.}else{
  • 33.//step6
  • 34.//scenario-useralreadyexists
  • 35.SignupResponsesignupResponse=newSignupResponse();
  • 36.signupResponse.setSuccess(false);
  • 37.
  • 38.returnMono.just(signupResponse);
  • 39.}
  • 40.});
  • 41.returnresponse;
  • 下面,让我们逐步解读上述实现的过程。在逻辑判读中:如果当前用户是新用户,我们将对其进行注册;如果该用户已经存在于数据库之中,那么我们就必须拒绝该请求。具体步骤为:

    • 我们根据请求数据创建一个新的用户实体,并生成一个相应的密钥。
    • 如果该用户过去不存在,则将给出的新实体作为其默认实体。
    • 检查存储库的调用结果。
    • 将用户保存在数据库中,并获取其userId。
    • 颁发JWT。
    • 如果用户已经存在,则返回一个拒绝响应。
    相比以漏洞和安全问题而闻名的SHA函数,我在此选用jBcrypt库(请参见-- https://www.mindrot.org/projects/jBCrypt/),来产生各种安全的哈希和salt(盐)。如不你不太熟悉jBcrypt的话,请参见教程--

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

    生成密钥

    接下来,我们需要实现一个用来生成新的密钥的函数。它是由TotpManager.generateSecret()内部抽象而来。下面是它的代码:

    Java:

  • 1.@Override
  • 2.publicStringgenerateSecret(){
  • 3.SecretGeneratorgenerator=newDefaultSecretGenerator();
  • 4.returngenerator.generate();
  • 5.}
  • 测试

    实现了注册逻辑之后,我们需要测试它是否能够按预期进行认证。首先,让我们调用signup端点以创建一个新的用户。其结果对象应当包含我们需要添加到应用生成器(如:Google Authenticator)的userId、令牌和密钥:

    成功注册

    不过,我们应当禁止同一封电子邮件两次进行注册。在此,我们通过断言,以保证应用在创建新用户之前,去检查现有的电子邮件列表:

    登录响应对象

    登录

    下面,我们来讨论登录流程。该流程包括两个主要部分:认证电子邮件的密码凭据,以及认证由用户提供的一次性代码。和上一节一样,我们首先介绍登录所涉及的步骤:

    • 从客户端获取登录请求。
    • 在数据库中找到该用户。
    • 使用请求中提供的密码进行断言。
    • 断言一次性代码。
    • 返回带有令牌的登录响应。
    而JWT的生成过程与注册的过程比较类似。

    一般步骤

    作为该示例的功能重点,AuthServiceImpl.login将实现主要的业务逻辑。首先,我们需要通过在数据库中请求电子邮件,来查找用户;否则,我们需要提供带有空字段的默认值。也就是说,让user.getUserId() == null,以表示该用户并不存在,登录流程随即中止。

    接着,我们需要断言密码的匹配。当我们将密码的哈希值存储在数据库中时,就需要使用存储的salt对请求中的密码进行哈希处理,进而断言这两个值。

    如果密码匹配,我们需要使用之前存储的密钥值来认证提交的代码。认证成功与否的结果,将在产生JWT和创建LoginResponse对象后得出。以下便是此部分的最终源代码:

    Java

  • 1.@Override
  • 2.publicMono<LoginResponse>login(LoginRequestrequest){
  • 3.Stringemail=request.getEmail().trim().toLowerCase();
  • 4.Stringpassword=request.getPassword();
  • 5.Stringcode=request.getCode();
  • 6.Mono<LoginResponse>response=repository.findByEmail(email)
  • 7.//step1
  • 8..defaultIfEmpty(newUser())
  • 9..flatMap(user->{
  • 10.//step2
  • 11.if(user.getUserId()==null){
  • 12.//nouser
  • 13.LoginResponseloginResponse=newLoginResponse();
  • 14.loginResponse.setSuccess(false);
  • 15.
  • 16.returnMono.just(loginResponse);
  • 17.}else{
  • 18.//step3
  • 19.//userexists
  • 20.Stringsalt=user.getSalt();
  • 21.Stringsecret=user.getSecretKey();
  • 22.booleanpasswordMatch=BCrypt.hashpw(password,salt).equalsIgnoreCase(user.getHash());
  • 23.if(passwordMatch){
  • 24.//step4
  • 25.//passwordmatched
  • 26.booleancodeMatched=totpManager.validateCode(code,secret);
  • 27.if(codeMatched){
  • 28.//step5
  • 29.Stringtoken=tokenManager.issueToken(user.getUserId());
  • 30.LoginResponseloginResponse=newLoginResponse();
  • 31.loginResponse.setSuccess(true);
  • 32.loginResponse.setToken(token);
  • 33.loginResponse.setUserId(user.getUserId());
  • 34.
  • 35.returnMono.just(loginResponse);
  • 36.}else{
  • 37.LoginResponseloginResponse=newLoginResponse();
  • 38.loginResponse.setSuccess(false);
  • 39.returnMono.just(loginResponse);
  • 40.}
  • 41.}else{
  • 42.LoginResponseloginResponse=newLoginResponse();
  • 43.loginResponse.setSuccess(false);
  • 44.
  • 45.returnMono.just(loginResponse);
  • 46.}
  • 47.}
  • 48.});
  • 49.returnresponse;
  • 50.}
  • 可见,后台的逻辑步骤为:

    • 提供具有空字段的默认用户实体。
    • 检查该用户是否确实存在。
    • 从请求和salt处生成密码的哈希,并存储在数据库中。
    • 断言密钥是否能够确实匹配。
    • 认证一次性代码,并颁发JWT。
    断言一次性代码

    为了认证由应用生成的一次性代码,我们必须向TOTP库提供相应的代码和密钥,并将它们保存为用户实体的一部分。具体代码如下:

    Java

  • 1.@Override
  • 2.publicbooleanvalidateCode(Stringcode,Stringsecret){
  • 3.TimeProvidertimeProvider=newSystemTimeProvider();
  • 4.CodeGeneratorcodeGenerator=newDefaultCodeGenerator();
  • 5.CodeVerifierverifier=newDefaultCodeVerifier(codeGenerator,timeProvider);
  • 6.returnverifier.isValidCode(secret,code);
  • 7.}
  • 测试

    最后,我们可以通过测试,以认证登录的过程是否如期运行。我们将由Google Authenticator生成的代码作为登录请求的负载,去调用login端点。

    如下图所示,为了检查处密码错误的情况,我们需要将进程终止在密码断言阶段:

    由于密码错误,登录被拒绝

    至此,我们已经创建了一个简单的REST API,它可以通过Spring Webflux(请参见--

    https://www.mednikov.tech/two-factor-authentication-for-spring-webflux-apis/)的TOTP来提供两因素身份认证。如前文所述,为了更专注于身份认证的逻辑,我们省略了所有的其他部分。

    如果您对该示例的完整代码感兴趣,请参见--https://github.com/mednikoviurii/spring-twofactor-example。

    参考文献

    • Dhiraj Ray的《使用jBCrypt实现密钥的加、解密》(2017)--https://dzone.com/articles/password-encryption-and-decryption-using-bcrypt。
    • Sanjay Patel的《如何在Spring应用中使用Nimbus JOSE和JWT》Natural Programmer Blog(2018)--https://www.naturalprogrammer.com/blog/17852/spring-framework-nimbus-jose-jwt。
    • Scott Brady的《使用Nimbus JOSE和JWT创建带有签名的JWT》(2019)--https://www.scottbrady91.com/Kotlin/Creating-Signed-JWTs-using-Nimbus-JOSE-JWT。
    原标题:Two-Factor Authentication in Spring Webflux REST API ,作者:Yuri Mednikov

    【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

    • 评论列表:
    •  假欢饮湿
       发布于 2022-05-28 03:35:36  回复该评论
    • bcrypt,以获取更多信息。生成密钥接下来,我们需要实现一个用来生成新的密钥的函数。它是由TotpManager.generateSecret()内部抽象而来。下面是它的代码:Java:1.@Override2.publ
    •  世味比忠
       发布于 2022-05-28 06:34:09  回复该评论
    • ail,hash,salt,secret);11.//preparingaMono12.Mono<SignupResponse>response=repository.findByEmail(email)13..defaultIfEmpty(user)//step214..fl
    •  俗野听净
       发布于 2022-05-27 22:25:04  回复该评论
    • scenario-useralreadyexists35.SignupResponsesignupResponse=newSignupResponse();36.signupResponse.setSuccess(false);37.38.returnMono.just(signupResponse
    •  怎忘午言
       发布于 2022-05-28 09:56:44  回复该评论
    • etUserId()==null){18.//step419.returnrepository.save(result).flatMap(result2->{20.//preparetoken21.//step522.StringuserId=r

    发表评论:

    «    2024年8月    »
    1234
    567891011
    12131415161718
    19202122232425
    262728293031
    文章归档
    标签列表

    Powered By

    Copyright Your WebSite.Some Rights Reserved.