当前位置: 首页 > 科技观察

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

时间:2023-03-21 14:40:49 科技观察

如何在SpringWebflux中实现双因素认证(Multi-factorauthentication,MFA)成为最常见的处理方式。此外,MFA也被相关法律要求在越来越多的行业(尤其是欧盟)强制执行。因此,如果您正在开发应用程序,您很可能已经以某种形式启用了双(或多)因素身份验证。在本文中,我将向您展示如何为使用SpringWebflux构建的反应式API实现双因素身份验证。该应用程序主要使用电子邮件和密码对作为第一安全因素,并使用用户设备上的应用程序(例如GoogleAuthenticator)生成的一次性代码(TOTP)作为第二安全因素。双因素身份验证的工作原理从技术上讲,双(或更多)因素身份验证是一个安全过程,在该过程中,用户必须提供两个或更多安全因素才能通过身份验证。也就是说,用户需要提供除密码之外的另一种标识,例如:一次性密码、硬件令牌、生物特征(如:指纹)等。安全过程涉及以下步骤:用户输入电子邮件(用户名)和密码。除了第一个凭证之外,用户还提交由身份验证应用程序生成的一次性代码。在应用程序验证电子邮件(用户名)和密码的同时,它还使用在注册过程中颁发的用户密钥来验证一次性代码。可以看出,相较于使用短信传递密码,Authenticator、MicrosoftAuthenticator、FreeOTP等认证应用可以防止SIM卡被攻击(见--https://www.theverge.com/2017/6/17/15772142/how-to-set-up-two-factor-authentication),并且可以在没有蜂窝网络或Internet连接的情况下正常进行身份验证。应用程序示例下面,我们将逐步构建一个使用双因素身份验证技术的简单RESTAPI。API要求用户提供电子邮件密码对,以及应用程序生成的简码。在这里,我使用GoogleAuthenticatorforAndroid来生成TOTP。其源代码的github库链接是--https://github.com/mednikoviurii/spring-twofactor-example。该应用程序将使用JDK11、Maven和MongoDB来存储用户个人信息。其项目组织结构如下图所示:应用示例的项目结构这里,我不会遍历介绍各个组件,只重点介绍AuthService、TokenManager和TotpManager。这些部分主要负责身份的认证过程。它们分别提供以下功能:AuthService——该组件主要用于存储、验证和授权所有业务逻辑,包括:注册、登录和令牌验证。TokenManager–该组件抽象代码以生成和验证JWT令牌。它可以使主要业务逻辑的实现独立于具体的JWT库。在这里,我使用NimbusJOSE-JWT(参见--https://connect2id.com/products/nimbus-jose-jwt/examples)。TotpManager–将实现与底层逻辑隔离开来的另一种抽象。TotpManager可以用来生成用户的密钥,并给出短代码断言(assert,可以立即验证)。这里,我使用的是TOTPJava库(https://github.com/samdjstevens/java-totp),当然你也可以选择其他库。由于我们在这里只关注身份验证组件,因此我们将从用户创建过程(注册)开始,这涉及密钥生成和令牌颁发。接下来,我们将进入登录流程,其中涉及断言用户提供的简码。实现注册流程接下来,我们将完成一个注册流程,包括以下步骤:从客户端获取注册请求。检查用户是否存在。散列密码。生成密钥。将用户存储在数据库中。发布JWT。返回包含用户ID、私钥和令牌的响应。我将主要业务逻辑(AuthServiceImpl)从令牌生成和密钥生成中分离出来。一般步骤主要组件AuthServiceImpl将接受SignupRequest并返回SignupResponse。在幕后,它负责整个注册逻辑。下面是具体的代码:Java1.@Override2.publicMonosignup(SignupRequestrequest){3.//generatinganewuserentityparams4.//step15.Stringemail=request.getEmail().trim().toLowerCase();6.Stringpassword=request.getPassword();7.Stringsalt=BCrypt.gensalt();8.Stringhash=BCrypt.hashpw(密码,盐);9.Stringsecret=totpManager.generateSecret();10.Useruser=newUser(null,email,hash,salt,secret);11.//preparingaMono12.Monoresponse=repository.findByEmail(email)13..defaultIfEmpty(user)//step214..flatMap(result->{15.//assert,thatuserdoesnotexist16.//step317.if(result.getUserId()==null){18.//step419.returnrepository.save(result).flatMap(result2->{20.//preparetoken21.//step522.StringuserId=result2.getUserId();23.Stringtoken=tokenManager.issueToken(userId);24.SignupResponsesignupResponse=newSignupResponse();25.signupResponse.setUserId(userId);26.signupResponse.setSecretKey(秘密);27.signupResponse.setToken(令牌);28.signupResponse.setSuccess(true);29.30.returnMono.just(signupResponse);31.});32.}else{33.//step634.//场景用户已经存在35.SignupResponsesignupResponse=newSignupResponse();36.signupResponse.setSuccess(false);37.38.returnMono.just(signupResponse);39.}40.});41.returnresponse;接下来,让我们一步一步来逻辑解释一下上面实现的过程:如果当前用户是新用户,我们就注册;如果用户已经存在于数据库中,那么我们必须拒绝该请求。具体步骤是:我们根据请求数据创建一个新的用户实体,并生成对应的key。如果用户过去不存在,则给定的新实体将用作其默认实体。检查调用存储库的结果。将用户保存在数据库中,并获取其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.@Override2.publicStringgenerateSecret(){3.SecretGeneratorgenerator=newDefaultSecretGenerator();4.returgenerator.generate();5.}测试实现注册逻辑后,我们需要测试是否可以按需要身份验证。首先,让我们调用注册端点来创建一个新用户。结果对象应包含我们需要添加到应用程序构建器(例如:GoogleAuthenticator)的userId、token和secret:成功注册但是,我们应该禁止同一电子邮件注册两次。在这里,我们使用断言来确保应用程序在创建新用户之前检查现有的电子邮件列表:登录响应对象登录接下来,让我们讨论登录流程。该过程包括两个主要部分:验证电子邮件的密码凭据,以及验证用户提供的一次性代码。和上一节一样,我们首先描述登录涉及的步骤:获取客户端的登录请求。在数据库中查找用户。使用请求中提供的密码进行断言。断言一次性代码。返回带有令牌的登录响应。JWT生成过程类似于注册过程。一般步骤作为本例的功能重点,AuthServiceImpl.login将实现主要的业务逻辑。首先,我们需要通过请求数据库中的电子邮件来查找用户;否则,我们需要提供带有空字段的默认值。即让user.getUserId()==null表示用户不存在,立即终止登录流程。接下来,我们需要断言密码匹配。当我们将密码的散列存储在数据库中时,我们需要使用存储的盐对请求中的密码进行散列,然后断言这两个值。如果密码匹配,我们需要使用先前存储的密钥值来验证提交的代码。生成JWT并创建LoginResponse对象后会得到认证成功或失败的结果。下面便为这部分的最终源代码:Java1.@Override2.publicMonologin(LoginRequestrequest){3.Stringemail=request.getEmail().trim().toLowerCase();4.Stringpassword=request.getPassword();5.Stringcode=request.getCode();6.Monoresponse=repository.findByEmail(email)7.//step18..defaultIfEmpty(newUser())9..flatMap(user->{10.//step211.if(user.getUserId()==null){12.//nouser13.LoginResponseloginResponse=newLoginResponse();14.loginResponse.setSuccess(false);15.16.returnMono.just(loginResponse);17.}else{18.//step319.//userexists20.Stringsalt=user.getSalt();21.Stringsecret=user.getSecretKey();22.booleanpasswordMatch=BCrypt.hashpw(password,salt).equalsIgnoreCase(user.getHash());23.if(passwordMatch){24.//step425.//passwordmatched26.booleancodeMatched=totpManager.validateCode(code,secret);27.if(codeMatched){28.//step529.Stringtoken=tokenManager.issueToken(user.getUserId());30.LoginResponseloginResponse=newLoginResponse();31.loginResponse.setS成功(真);32.loginResponse.setToken(令牌);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.}可以看出,幕后的逻辑步骤是:提供一个默认的用户实体,字段为空,以检查用户是否实际存在。密码的哈希值是根据请求和盐生成的,并存储在数据库中。断言密钥是否可以实际匹配。验证一次性代码,并发布JWT。断言一次性代码为了验证应用程序生成的一次性代码,我们必须向TOTP库提供相应的代码和密钥,并将它们保存为用户实体的一部分。具体代码如下:Java1.@Override2.publicbooleanvalidateCode(Stringcode,Stringsecret){3.TimeProvidertimeProvider=newSystemTimeProvider();4.CodeGeneratorcodeGenerator=newDefaultCodeGenerator();5.CodeVerifierverifier=newDefaultCodeVerifier(codeGenerator,timeProvider);6.returnverifier。isValidCode(secret,code);7.}测试最后,我们可以通过测试来验证登录过程是否按预期工作。我们使用GoogleAuthenticator生成的代码作为登录请求的有效负载来调用登录端点。如下图所示,为了检查密码错误的情况,我们需要在密码断言阶段终止进程:Logindeniedduetowrongpassword此时,我们创建了一个简单的RESTAPI,可以访问通过SpringWebflux(参见--https://www.mednikov.tech/two-factor-authentication-for-spring-webflux-apis/)TOTP提供双因素身份验证。如前所述,为了更加关注身份验证逻辑,我们省略了所有其他部分。如果您对此示例的完整代码感兴趣,请参阅--https://github.com/mednikoviurii/spring-twofactor-example。DhirajRay的参考文献《使用jBCrypt实现密钥的加、解密》(2017)——https://dzone.com/articles/password-encryption-and-decryption-using-bcrypt。《如何在Spring应用中使用Nimbus JOSE和JWT》自然程序员博客(2018),作者SanjayPatel--https://www.naturalprogrammer.com/blog/17852/spring-framework-nimbus-jose-jwt。《使用Nimbus JOSE和JWT创建带有签名的JWT》作者:ScottBrady(2019年)——https://www.scottbrady91.com/Kotlin/Creating-Signed-JWTs-using-Nimbus-JOSE-JWT。原标题:Two-FactorAuthenticationinSpringWebfluxRESTAPI,作者:YuriMednikov