在这篇文章中,我将演示如何为SpringBoot 2.5OAuthJWT服务器配置一个双因素身份验证功能,这是一个更大的SpringCloud系统的一部分。
我们构建了一个分布式CRM系统。该系统具有专用的基于SpringBoot的身份验证/授权服务和许多安全和不安全的微服务。我们选择授权服务器的JWT安全机制来验证每个安全的微服务的任何请求,这是微服务的工作。另外,我们希望在授权服务发出有效的JWT之前为授权服务启用2FA身份验证,以达到另一级别的保护。
该员额安排如下。首先,我描述了系统的各个部分。其次,我演示了系统的体系结构,并描述了如何配置授权服务器来启用2FA。提供了整个系统的源代码。这里. 第二部分这篇文章演示了授权服务器如何在幕后工作。
系统
我们系统的组成图如图1所示。用户通过Zuul网关与系统进行交互。用户也可以直接调用单个的微服务RESTAPI。
Config服务器为微服务提供配置文件。全局配置文件包含所有微服务的信息。还有一些配置文件是针对单个微服务的.为了使其自动工作,所有服务都在Eureka服务器上注册。一些微型服务通过Kafka消息代理相互连接。Kafka的主机、端口和主题列在全局配置文件中。
本系统是以起动机为基础的。电码的伊斯克伦·伊万诺夫;见他的岗有关此系统如何工作的详细信息。我添加了一个双因素授权,并将授权服务器从1.5迁移到SpringBoot2.5。此外,我还分析了授权服务器如何在第二部分.
在这个分布式系统中,安全工作流比单块系统更复杂,用户只需要提供用户名和密码,可能还需要提供2FA代码。我们的系统使用隐式授权流。要获得有效的JWT,用户需要提供一个客户机ID(客户(为了简洁起见)和客户秘密(秘密为了简洁起见,这些都是微服务的“凭据”。此外,他或她需要提供一个有效的用户名和密码-这些是我们习惯的常规凭证。此外,如果用户启用了他/她的2FA,则该用户需要发送一个具有相同内容的额外请求。客户:秘密并提供必要的2FA代码。最后,要刷新JWT,用户必须提供客户:秘密以及有效的刷新令牌。
客户端ID、客户端秘密、用户名和密码存储在单独的数据库中,仅连接到授权服务器。此外,授权服务器保留一个私钥来签名JWT;所有安全服务都保留相应公钥的副本以验证签名。让我们看看如何配置这个授权服务器。
授权服务器体系结构
3种方案的实际工作流,由岗位上的阿纳尔·苏丹诺夫,如下所示。 如果用户有他/她2FA残疾人,用户可在1步内获得授权。用户进行以下调用:
curl trusted-app:secret@localhost:9999/oauth/token -d grant_type=password -d username=user -d password=password
这里trusted-app:secret 是客户:秘密在这个系统中,localhost:9999是东道主:港口在部署授权服务器的地方,/oauth/token是令牌端点。这个grant_type=password是2FA授权的第一步的授予类型,username和password是通常的用户凭据。系统返回一个有效的JWT和一个具有用户角色编码的刷新令牌。
如果用户有他/她2FA启用,用户打两个电话。第一个调用与上面的调用相同;但是,这一次,系统返回一个带有“preauth”角色编码的有效访问令牌。然后,用户再打一个电话:
curl trusted-app:secret@localhost:9999/oauth/token -d grant_type=tfa -d tfa_token=$token -d tfa_code=123456
在那里$token是上一步中的“preauth”访问令牌,tfa_code是2FA算法工作所必需的一次代码。系统返回一个有效的JWT和一个具有用户角色编码的刷新令牌。
注意,这个带有访问令牌(而不是JWT)的中间步骤防止了微服务错误地对用户进行身份验证,其中JWT是用JWT的公钥进行验证的,但是服务有一个“All以外的<角色>”过滤器。在这种情况下,“preauth”角色也是可以接受的。
最后,到刷新一个JTW(不一定过期),用户调用:
curl trusted-app:secret@localhost:9999/oauth/token -d grant_type=refresh_token -d refresh_token=$token
在我们使用的地方grant_type=refresh_token以及在前面步骤中获得的实际刷新令牌。
在引擎盖下,我们的系统如下(图2)。
图2.我们安全服务器的工作流程。
对于传入的令牌请求,系统首先验证客户:秘密对系统的基本身份验证过滤器,这反过来调用客户端和秘密身份验证管理器。如果为肯定,则请求将到达授权端点。/oauth/token.然后,系统根据请求类型调用用户名和密码验证器、2FA验证器或刷新令牌验证器;这些验证器在适当的令牌授予器中用于实际发出JWT。反过来,用户名和密码验证器需要用户名和密码身份验证管理器来完成其工作。让我们看看如何配置这些管理器。
认证管理器
这个客户端和秘密认证管理器由Spring授权服务器自动配置。在……里面第二部分在这篇文章中,我详细地展示了这是如何工作的。现在,我们只需要为客户:秘密对(见全文)电码详细情况)。
@Configuration@EnableAuthorizationServerpublic class AuthorizationConfig extends AuthorizationServerConfigurerAdapter { @Autowired @Qualifier("dataSource") private DataSource dataSource; {.....} @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } {.....}}
接下来,客户端和秘密认证管理器是以下列方式创建的:
@Configuration@EnableWebSecurity(debug = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Bean("securityConfigAuthManager") @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder); } @Bean("securityConfigUserService") @Primary @Qualifier("userDetail") public UserDetailsService userDetailsService() { return new AccountServiceImpl(); }}
在这里,我们首先userDetailsService()验证用户名和密码的AccountServiceImpl()。那我们就喂这个userDetailsService()到configure(AuthenticationManagerBuilder auth)使服务可用于authenticationManagerBean()。是这个身份验证管理器自动进入AuthorizationConfig.
正如我在第二部分中所演示的,@EnableWebSecurity的每个子代生成一个筛选链。WebSecurityConfigurerAdapter在授权服务和SecurityConfig也不例外。但是,对于我们的应用程序,我们不需要这个特定的过滤器链接。我们只使用提供的基础设施来创建用户名和密码身份验证管理器。让我们看看如何对令牌授权程序进行编程。
令牌授予者
我们需要创建必要的辅助类,使令牌授予器能够正常工作,然后将授予程序提供给令牌端点:
@Configuration@EnableAuthorizationServerpublic class AuthorizationConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private TfaService tfaService; {......} @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenGranter(tokenGranter(endpoints)); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( new ClassPathResource("ms-auth.jks"), "ms-auth-pass".toCharArray()); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("ms-auth")); return converter; } @Bean public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenEnhancer(accessTokenConverter()); return defaultTokenServices; } public class RefreshTokenConverter extends JwtAccessTokenConverter{ public MyAccessTokenConverter(){ super(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( new ClassPathResource("ms-auth.jks"), "ms-auth-pass".toCharArray()); super.setKeyPair(keyStoreKeyFactory.getKeyPair("ms-auth")); } public Map<String, Object> decode(String token){return super.decode(token);} } private TokenGranter tokenGranter(final AuthorizationServerEndpointsConfigurer endpoints) { List<TokenGranter> granters =new ArrayList<>(); granters.add(new PasswordTokenGranter(endpoints, authenticationManager, tfaService, tokenServices())); granters.add(new TfaTokenGranter(endpoints, authenticationManager, tfaService, tokenServices())); granters.add(new JWTRefreshTokenGranter(endpoints, authenticationManager, tfaService, tokenServices(), new RefreshTokenConverter())); return new CompositeTokenGranter(granters); }}
在这里,我们将私钥设置为在RefreshTokenConverter此令牌转换器用于解码刷新令牌。接下来,我们设置另一个令牌转换器。accessTokenConverter()具有相同私钥的TokenStore签署JWTS。然后,我们给TokenStore到TokenGranter。最后,TokenGranter提供给端点配置程序:configure(AuthorizationServerEndpointsConfigurer endpoints).
每个令牌授予者都必须实现grant(String grantType, TokenRequest tokenRequest)方法。这个密码令牌授权器是grant(...)方法的实现如下:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters()); String username = parameters.get("username"); String password = parameters.get("password"); parameters.remove("password"); Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken) userAuth).setDetails(parameters); String clientId = tokenRequest.getClientId(); ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId); this.validateGrantType(grantType, client); try { userAuth = this.authenticationManager.authenticate(userAuth); } catch (AccountStatusException | BadCredentialsException e) { throw new InvalidGrantException(e.getMessage()); } if (userAuth != null && userAuth.isAuthenticated()) { OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest); if (tfaService.isEnabled(username)) { userAuth = new UsernamePasswordAuthenticationToken(username, password, Collections.singleton(PRE_AUTH)); OAuth2AccessToken accessToken = this.endpointsConfigurer.getTokenServices().createAccessToken(new OAuth2Authentication(storedOAuth2Request, userAuth)); return accessToken; } OAuth2AccessToken jwtToken = this.jwtService.createAccessToken(new OAuth2Authentication(storedOAuth2Request, userAuth)); return jwtToken; } else { throw new InvalidGrantException("Could not authenticate user: " + username); } }
在这里,我们首先提取用户名:密码和客户(叫clientId在这里,从客户:秘密(对)从tokenRequest。然后,系统调用clientDetailService若要加载客户端详细信息,请执行以下操作客户并验证授予类型。接下来,身份验证管理器将验证用户名:密码一对。然后系统调用tfaService若要检查此用户名是否启用了2FA,请执行以下操作。如果为正,系统将调用tokenService若要创建一个OAuth2Access使用“preauth”角色编码的令牌,并返回令牌。如果为否定,系统将调用jwtService(那是tokenService从…AuthorizationConfig)来创建一个JWT,然后返回令牌。
这个TFA权杖氏grant()方法的实现如下:
@Override public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest){ return super.grant(grantType, tokenRequest); } @Override protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { {extracts "tfa_token" parameters} OAuth2Authentication authentication = loadAuthentication(tfaToken); if (parameters.containsKey("tfa_code")) { int code = parseCode(parameters.get("tfa_code")); if (tfaService.verifyCode(username, code)) { return getAuthentication(tokenRequest, authentication); } } {elses and throw exceptions} } private OAuth2Authentication loadAuthentication(String accessTokenValue) { OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue); {checks if the accessToken is not null or expired} OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken); return result; } } private OAuth2Authentication getAuthentication(TokenRequest tokenRequest, OAuth2Authentication authentication) { {authManager authenticates the user; clientDetailsService verifies clientId} return refreshAuthentication(authentication, tokenRequest); } private OAuth2Authentication refreshAuthentication(OAuth2Authentication authentication, TokenRequest request) { {verifies the request scope} return new OAuth2Authentication(clientAuth, authentication.getUserAuthentication()); }
它很长,因此为了简洁起见,省略了细节(请参阅电码)。此令牌授予器的工作方式与前一个不同。在这里,我们重写父方法来调用tfaService来验证2FA代码。如果是肯定的,系统将再次检查用户和客户端凭据,以返回JWT。
所述TfaService是:
@Autowired private AccountRepository accountRepository; private GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(); public boolean isEnabled(String username) { Optional<Account> account = accountRepository.findByUsername(username); if(account.isPresent()) { return accountRepository.findByUsername(username).get().isTwoFa(); } else return false; } public boolean verifyCode(String username, int code) { Optional<Account> account = accountRepository.findByUsername(username); if(account.isPresent()) { System.out.println("TFA code is OK"); return code == googleAuthenticator.getTotpPassword(account.get().getSecret()); } else return false; }
该服务检查用户是否启用了他/她的2FA,以及所提供的代码是否与Google身份验证者从用户的秘密中获得的代码相匹配。启用的2FA和用户的秘密都从accountRepository.
最后,让我们看看刷新令牌授权器:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest){ String refrToken = tokenRequest.getRequestParameters().get("refresh_token"); //decode(refrtoken) verifies the token as well Map<String,Object> map = this.jwtConverter.decode(refrToken); OAuth2Authentication auth = this.jwtConverter.extractAuthentication(map); OAuth2AccessToken result = this.jwtTokenService.createAccessToken(auth); return result; }
这个很简单。系统从请求中提取刷新令牌。然后RefreshConverter(()jwtConverter)解码刷新令牌。最后,对系统进行了提取。OAuth2Authentication auth并从AUTH创建一个JWT。
我想指出的是,这个权证授予者的实现方式与原授权程序中的授予者不同。岗。在那里,在第一个2FA步骤中,用户(启用了2FA)使用客户:秘密,输入“密码”,用户名:密码。系统返回一个带有“preauth”角色编码的“mfa”(多因素授权)访问令牌。在第二个2FA步骤中,用户用客户:秘密,授予类型“mfa”、“mfa”访问令牌和2FA代码。如果验证,系统将返回一个带有用户角色编码的访问令牌。
我们的系统被修改为返回一个JWT,而不是访问令牌。另外,在2FA过程的第一步,我们的系统返回一个访问令牌,这样就不可能错误地授权一个“preauth”JWT作为“除了用户角色”的令牌。最后,该系统在第一个2FA步骤中执行2FA身份验证,而不需要计算上昂贵的“2FA_Required”异常。
结果
为了测试2FA功能,我们使用了3个容器测试,每个请求类型一个。为了简洁起见,我演示了当用户第一次提供他/她时如何测试第二个场景用户名:密码,然后是访问令牌和必要的2FA代码。
MvcResult result = mockMvc.perform(post("/oauth/token") .header("Authorization","Basic dHJ1c3RlZC1hcHA6c2VjcmV0") .param("username","admin") .param("password","password") .param("grant_type","password") .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) ) .andExpect(status().isOk()) .andReturn(); String resp = result.getResponse().getContentAsString(); String token = getTokenString("tfa_token",resp); result = mockMvc.perform(post("/oauth/token") .header("Authorization","Basic dHJ1c3RlZC1hcHA6c2VjcmV0") .param("tfa_code","123456") .param("tfa_token",token) .param("grant_type","tfa") .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) ) .andExpect(status().isOk()) .andReturn(); resp = result.getResponse().getContentAsString(); String tokenBody = getTokenString("access_token",resp); String user = getUserName(tokenBody); String auth = getAuthorities(tokenBody); assertEquals(user, "admin"); assertTrue(auth.contains("ROLE_USER")); assertTrue(auth.contains("ROLE_ADMIN"));
这里模拟了第二个场景的curl调用。这个tfaService.verify(String username, int code)对每个代码返回true,以便测试自动运行。这个测试通过了。见到其他两次测试。
结论
在这篇文章中,我演示了如何配置Spring授权服务器以启用2FA功能。为此,我们需要编程3个令牌授予者和2个身份验证管理器,对于其中的一个管理器,我们只需要设置一个客户端详细信息数据源。希望能见到你第二部分,在这里,我演示了Spring授权服务器是如何在幕后运行所有这些的。
最后
谢谢大家的观看,小伙伴们如果觉得我写得不错,可以点赞+收藏!更多Java进阶学习资料、2022大厂面试真题,完整资料已经给大家打包完毕,需要资料的小伙伴关注博主+私信“学习”或者“面经”即可获得免费资料!!
版权声明:内容来源于互联网和用户投稿 如有侵权请联系删除