使用Spring Security 资源服务器来保护Spring Cloud 微服务

时间:2021-10-07 02:10:41

使用Spring Security 资源服务器来保护Spring Cloud 微服务

我在上一篇对资源服务器进行了简单的阐述,让大家对资源服务器的概念有了简单的认识,今天我将用实际例子来演示单体应用改造为Spring Cloud微服务时的资源服务器实现。

资源服务器改造

Spring Security实战干货的DEMO为例子,原本它是一个单体应用,认证和授权都在一个应用中使用。改造为独立的服务后,原本的认证就要剥离出去(这个后续再讲如何实现),服务将只保留基于用户凭证(JWT)的访问控制功能。接下来我们将一步步来实现该能力。

所需依赖

在Spring Security的基础上,我们需要加入新的依赖来支持OAuth2 Resource Server和JWT。我们需要引入下面几个依赖库:

  1. org.springframework.boot
  2. spring-boot-starter-security
  3. --资源服务器-->
  4. org.springframework.security
  5. spring-security-oauth2-resource-server
  6. --jose-->
  7. org.springframework.security
  8. spring-security-oauth2-jose

Spring Security 5.x 移除了OAuth2.0授权服务器,保留了OAuth2.0资源服务器。

JWT解码

要校验JWT就必须实现对JWT的解码功能,在Spring Security OAuth2 Resource Server模块中,默认提供了解码器,这个解码器需要调用基于:

  1. spring.security.oauth2.resourceserver

配置下的元数据来生成解码配置,这里的配置大部分是调用授权服务器开放的well-known断点,包含了解析验证JWT一系列参数:

  • jwkSetUri 一般是授权服务器提供的获取JWK配置的well-known端点,用来校验JWT Token。
  • jwsAlgorithm 指定jwt使用的算法,默认 RSA-256。
  • issuerUri 获取OAuth2.0 授权服务器元数据的端点。
  • publicKeyLocation 用于解码的公钥路径,作为资源服务器来说将只能持有公钥,不应该持有私钥。

为了实现平滑过渡,默认的配置肯定不能用了,需要定制化一个JWT解码器。接下来我们一步步来实现它。

分离公私钥

资源服务器只能保存公钥,所以需要从之前的jks文件中导出一个公钥。

  1. keytool-export-aliasfelordcn-keystore-file<导出cer的全路径>

例如:

  1. keytool-export-aliasfelordcn-keystoreD:\keystores\felordcn.jks-filed:\keystores\publickey.cer

把分离的cer公钥文件放到原来jks文件的路径下面,资源服务器不再保存jks。

自定义jwt解码器

spring-security-oauth2-jose是Spring Security的jose规范依赖。我将根据该类库来实现自定义的JWT解码器。

  1. /**
  2. *基于Nimbus的jwt解码器,并增加了一些自定义校验策略
  3. *
  4. *@paramvalidatorthevalidator
  5. *@returnthejwtdecoder
  6. */
  7. @SneakyThrows
  8. @Bean
  9. publicJwtDecoderjwtDecoder(@Qualifier("delegatingTokenValidator")DelegatingOAuth2TokenValidatorvalidator){
  10. CertificateFactorycertificateFactory=CertificateFactory.getInstance("X.509");
  11. //从classpath路径读取cer公钥证书来配置解码器
  12. ClassPathResourceresource=newClassPathResource(this.jwtProperties.getCertInfo().getPublicKeyLocation());
  13. Certificatecertificate=certificateFactory.generateCertificate(resource.getInputStream());
  14. PublicKeypublicKey=certificate.getPublicKey();
  15. NimbusJwtDecodernimbusJwtDecoder=NimbusJwtDecoder.withPublicKey((RSAPublicKey)publicKey).build();
  16. nimbusJwtDecoder.setJwtValidator(validator);
  17. returnnimbusJwtDecoder;
  18. }

上面的解码器基于我们的公钥证书,同时我还自定义了一些校验策略。不得不说Nimbus的jwt类库比jjwt要好用的多。

自定义资源服务器配置

接下来配置资源服务器。

核心流程和概念

资源服务器其实也就是配置了一个过滤器BearerTokenAuthenticationFilter来拦截并验证Bearer Token。验证通过而且权限符合要求就放行,不通过就不放行。

和之前不太一样的是验证成功后凭据不再是UsernamePasswordAuthenticationToken而是JwtAuthenticationToken:

  1. @Transient
  2. publicclassJwtAuthenticationTokenextendsAbstractOAuth2TokenAuthenticationToken{
  3. privatestaticfinallongserialVersionUID=SpringSecurityCoreVersion.SERIAL_VERSION_UID;
  4. privatefinalStringname;
  5. /**
  6. *Constructsa{@codeJwtAuthenticationToken}usingtheprovidedparameters.
  7. *@paramjwttheJWT
  8. */
  9. publicJwtAuthenticationToken(Jwtjwt){
  10. super(jwt);
  11. this.name=jwt.getSubject();
  12. }
  13. /**
  14. *Constructsa{@codeJwtAuthenticationToken}usingtheprovidedparameters.
  15. *@paramjwttheJWT
  16. *@paramauthoritiestheauthoritiesassignedtotheJWT
  17. */
  18. publicJwtAuthenticationToken(Jwtjwt,Collectionauthorities){
  19. super(jwt,authorities);
  20. this.setAuthenticated(true);
  21. this.name=jwt.getSubject();
  22. }
  23. /**
  24. *Constructsa{@codeJwtAuthenticationToken}usingtheprovidedparameters.
  25. *@paramjwttheJWT
  26. *@paramauthoritiestheauthoritiesassignedtotheJWT
  27. *@paramnametheprincipalname
  28. */
  29. publicJwtAuthenticationToken(Jwtjwt,Collectionauthorities,Stringname){
  30. super(jwt,authorities);
  31. this.setAuthenticated(true);
  32. this.name=name;
  33. }
  34. @Override
  35. publicMapgetTokenAttributes(){
  36. returnthis.getToken().getClaims();
  37. }
  38. /**
  39. *jwt中的sub值用户名比较合适
  40. */
  41. @Override
  42. publicStringgetName(){
  43. returnthis.name;
  44. }
  45. }

这个我们改造的时候要特别注意,尤其是从SecurityContext获取的时候用户凭证信息的时候。

资源管理器配置

从Spring Security 5的某版本开始不需要再集成适配类了,只需要这样就能配置Spring Security,资源管理器也是这样:

  1. @Bean
  2. SecurityFilterChainjwtSecurityFilterChain(HttpSecurityhttp)throwsException{
  3. returnhttp.authorizeRequests(request->request.anyRequest()
  4. .access("@checker.check(authentication,request)"))
  5. .exceptionHandling()
  6. .accessDeniedHandler(newSimpleAccessDeniedHandler())
  7. .authenticationEntryPoint(newSimpleAuthenticationEntryPoint())
  8. .and()
  9. .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
  10. .build();
  11. }

这里只需要声明使用JWT校验的资源服务器,同时配置好定义的401端点和403处理器即可。这里我加了基于SpEL的动态权限控制,这个再以往都讲过了,这里不再赘述。

JWT个性化解析

从JWT Token中解析数据并生成JwtAuthenticationToken的操作是由JwtAuthenticationConverter来完成的。你可以定制这个转换器来实现一些个性化功能。比如默认情况下解析出来的权限都是带SCOPE_前缀的,而项目用ROLE_,你就可以通过这个类兼容一下老项目。

  1. @Bean
  2. JwtAuthenticationConverterjwtAuthenticationConverter(){
  3. JwtAuthenticationConverterjwtAuthenticationConverter=newJwtAuthenticationConverter();
  4. JwtGrantedAuthoritiesConverterjwtGrantedAuthoritiesConverter=newJwtGrantedAuthoritiesConverter();
  5. //如果不按照规范解析权限集合Authorities就需要自定义key
  6. //jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
  7. //OAuth2默认前缀是SCOPE_SpringSecurity是ROLE_
  8. jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
  9. jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
  10. //设置jwt中用户名的key默认就是sub你可以自定义
  11. jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);
  12. returnjwtAuthenticationConverter;
  13. }

这里基本上就改造完成了。你受保护的资源API将由Bearer Token来保护。

在实际生产中建议把资源服务器封装为依赖集成到需要保护资源的的服务中即可。

附加说明

为了测试资源服务器,假设我们有一个颁发令牌的授权服务器。这里简单模拟了一个发令牌的方法用来获取Token:

  1. /**
  2. *资源服务器不应该生成JWT但是为了测试假设这是个认证服务器
  3. */
  4. @SneakyThrows
  5. @Test
  6. publicvoidimitateAuthServer(){
  7. JwtEncoderjwsEncoder=newNimbusJwsEncoder(jwkSource());
  8. JwtTokenGeneratorjwtTokenGenerator=newJwtTokenGenerator(jwsEncoder);
  9. OAuth2AccessTokenResponseoAuth2AccessTokenResponse=jwtTokenGenerator.tokenResponse();
  10. System.out.println("oAuth2AccessTokenResponse="+oAuth2AccessTokenResponse.getAccessToken().getTokenValue());
  11. }
  12. @SneakyThrows
  13. privateJWKSourcejwkSource(){
  14. ClassPathResourceresource=newClassPathResource("felordcn.jks");
  15. KeyStorejks=KeyStore.getInstance("jks");
  16. Stringpass="123456";
  17. char[]pem=pass.toCharArray();
  18. jks.load(resource.getInputStream(),pem);
  19. RSAKeyrsaKey=RSAKey.load(jks,"felordcn",pem);
  20. JWKSetjwkSet=newJWKSet(rsaKey);
  21. returnnewImmutableJWKSet<>(jwkSet);
  22. }

相关的DEMO已经上传,你可以通过关注公众号“码农小胖哥” 回复 resourceserver获取资源服务器实现的DEMO。

原文链接:https://mp.weixin.qq.com/s/T_90tcyLQ6_a7IvaH6muYg