通过 Bean 的方式扩展 Spring 应用,使其同时支持多个授权服务颁发...

共 9732字,需浏览 20分钟

 ·

2023-05-02 22:34

我在 Authing.cn 中建立了一个 Brickverse 用户池:

26c4745d15ccdb234df9bddfcb6ac81a.webp


在这个用户池下创建了两个应用,一个名为 brickverse,一个名为“哈德韦的个人小程序”。

42047776560ae14ab23cfed013324be5.webp

第一个应用的访问链接是: https://brickverse.net,第二个应用,虽然是小程序,但也提供了网页访问方式: https://taro.jefftian.dev 。

之前也写了一个 user-service 服务以供 https://brickverse.net 使用,通过 spring-security-oauth2 来保护这个服务,后来将它升级到了 spring-boot-starter-oauth2-resource-server,具体细节见:《升级spring-security-oauth2到oauth2-resource-server 。这时,user-service 是资源服务,而 spring-security-oauth2 也好,spring-boot-starter-oauth2-resource-server 也好,都只是一个 Java 包,是一个具体的程序。要保护资源服务,还得需要一个授权服务,这个授权服务,我就使用 Authing.cn,具体来说,是我在 Authing.cn 上创建的 brickverse 应用。

dfeeb82b5bf8fcb5f19f8d1fbc356fe3.webp

而这个服务需要配置授权服务器,我的配置如下:

293095c3c53588ee6abce867038bd71b.webp


今天,我想让我的个人小程序,同样可以使用这个 user-service。如果直接调用,哪怕是已经登录过了,接口也会回复 401。原因很简单,我的个人小程序,在登录时,连接的是 Authing.cn 中的另一个 app,即“哈德韦的个人小程序”。

3feb49e432d098668a76a9e639b2d822.webp

因此,需要扩展 user-service 的安全组件,能够支持 uniheart(我给“哈德韦的个人小程序”app 在 Authing.cn 中起的名字) 公钥端点的 jwt token,即通过某个字段(比如 Issuer)来判断是否是受支持的,是则尝试解开 token,否则直接报错就行。

65cacd1ceb83ce4fc55b88acb73c83ec.webp


测试先行

说干就干,先加两个测试,第一个测试表明无效的令牌调用不了接口,第二个测试使用一个 uniheart token 调用接口,期待通过(现在这个用例会失败,因为还没有实现)。

      
        
          @Test
        
      
      
            public void testGraphQLEndpointsWorksWithWrongTokenForPublicAPIs() throws Exception {
      
      
                var exception = assertThrows(com.auth0.jwt.exceptions.JWTDecodeException.class, () -> mockMvc.perform(MockMvcRequestBuilders.post("/graphql").content(friendListQuery).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).header(HttpHeaders.AUTHORIZATION, "Bearer 123")).andExpect(MockMvcResultMatchers.status().isBadRequest()).andReturn());
      
      
        
          
assertEquals("The token was expected to have 3 parts, but got 1.", exception.getMessage()); }
@Test public void testGraphQLEndpointsWorksWithUniheartToken() throws Exception { var uniheartToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkNGTDZUZXN0X2xYZ0RHN1drSkphRVE3azd0Q0dzSEdua2xscy1vT3FqeUEifQ.eyJ1cGRhdGVkX2F0IjoiMjAyMy0wNC0xOVQxODoxMDo0NS4xNTRaIiwiYWRkcmVzcyI6eyJjb3VudHJ5IjoiIiwicG9zdGFsX2NvZGUiOm51bGwsInJlZ2lvbiI6bnVsbCwiZm9ybWF0dGVkIjpudWxsfSwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJwaG9uZV9udW1iZXIiOiIxMzA2MTkxMDI3MyIsImxvY2FsZSI6InpoX0NOIiwiem9uZWluZm8iOm51bGwsImJpcnRoZGF0ZSI6bnVsbCwiZ2VuZGVyIjoiTSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZW1haWwiOm51bGwsIndlYnNpdGUiOm51bGwsInBpY3R1cmUiOiJodHRwczovL3RoaXJkd3gucWxvZ28uY24vbW1vcGVuL3ZpXzMyL1g1TFE3b2tibjdqZFlMTFFmNGljaWJpYVNjMHljT01FZ29qNk5TRnVLVWJLSlcwcHVpYXpvYTI4NGZzVFBCSUdCTEhmT0tpYlcwN3h4Uk9GNHN1Q2Q1alBpYjF3LzEzMiIsInByb2ZpbGUiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6bnVsbCwibmlja25hbWUiOiLlk4jlvrfpn6bwn5SlIiwibWlkZGxlX25hbWUiOm51bGwsImZhbWlseV9uYW1lIjpudWxsLCJnaXZlbl9uYW1lIjpudWxsLCJuYW1lIjpudWxsLCJzdWIiOiI2Mzg5ZDQ4MzFkYjE2ZTU0MWRjOTQzYTMiLCJleHRlcm5hbF9pZCI6bnVsbCwidW5pb25pZCI6Im8wcHFFNkppNVhCRVJ4YkQ5XzJmWUpGZ1FXc00iLCJ1c2VybmFtZSI6IuWTiOW-t-mfpvCflKUiLCJkYXRhIjp7InR5cGUiOiJ1c2VyIiwidXNlclBvb2xJZCI6IjYyMDA5N2I2OWE5ZGFiNWU5NjdkMGM0NCIsImFwcElkIjoiNjIwMDk3YjdmN2M5NjQyMTBiOGY3NDMxIiwiaWQiOiI2Mzg5ZDQ4MzFkYjE2ZTU0MWRjOTQzYTMiLCJ1c2VySWQiOiI2Mzg5ZDQ4MzFkYjE2ZTU0MWRjOTQzYTMiLCJfaWQiOiI2Mzg5ZDQ4MzFkYjE2ZTU0MWRjOTQzYTMiLCJwaG9uZSI6IjEzMDYxOTEwMjczIiwiZW1haWwiOm51bGwsInVzZXJuYW1lIjoi5ZOI5b636Z-m8J-UpSIsInVuaW9uaWQiOiJvMHBxRTZKaTVYQkVSeGJEOV8yZllKRmdRV3NNIiwib3BlbmlkIjoibzFwOUg0MG12dUJiN18zTFJFRXE0TlZXVVdnQSIsImNsaWVudElkIjoiNjIwMDk3YjY5YTlkYWI1ZTk2N2QwYzQ0In0sInVzZXJwb29sX2lkIjoiNjIwMDk3YjY5YTlkYWI1ZTk2N2QwYzQ0IiwiYXVkIjoiNjIwMDk3YjdmN2M5NjQyMTBiOGY3NDMxIiwiZXhwIjoxNjg0MDQ4OTc2LCJpYXQiOjE2ODI4MzkzNzYsImlzcyI6Imh0dHBzOi8vdW5paGVhcnQuYXV0aGluZy5jbi9vaWRjIn0.D_UyVFD89dNzBtAgx-M9fTJlMTGxq_95cVOBN8fsD_0eQxSltr0Pktd25IdPB09JxZQBIEFb9f-maQXOq7YH2agSMk6IoNtO69TR4LNl8cGuIbkbI35jl7SGig9moK55luKnY5QjOWlUUQcTb7C0fVvBkx7K9wmuF8sVukHg8t_ComRNrhFxtqQuLWeXJSLhbLxMQzEta0Ofsu9paPzP0HPpVGmzYsS2FvkF7z4laRiz0JpyC01hw3i70qfFfOPUsxWUuSqUw_juc7heqMEFrJz2hO3c8oVovnMp48v2i-LWN8yRMbTDhsUmd9Ny0wovGdMhNFtMZ3cOhwFPp1bK6A";
mockMvc.perform(MockMvcRequestBuilders.post("/graphql").content(friendListQuery).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).header(HttpHeaders.AUTHORIZATION, "Bearer " + uniheartToken)).andExpect(MockMvcResultMatchers.status().isOk()); }

你可能会问,还应有一个测试,传入 brickverse token 时,接口可以调通。但是这个测试在之前添加 brickverse 应用时,就已经写了,我们只需要在稍后改完代码后,保证它仍然是通过的就行了。

第二个测试中先使用了一个硬编码的 uniheart token,后续可以替换成一个活的。

ab5e3a24617dedad70a978c581e89ef3.webp


要让这两个测试都通过,需要让安全组件能够针对 token 的 iss 做分支判断,不同的 iss,走不同的逻辑。最简单的想法,是给 spring-boot-starter-oauth2-resource-server 配置多个 jwk-set-uri,从而不写一行代码。遗憾的是,spring-boot-starter-oauth2-resource-server 并不支持这样做,只能自己写一点代码了。

添加自定义的 jwt decoder

首先添加一个新文件: src/main/java/com/brickpets/user/security/BrickverseUniHeartJwtDecoder.java:

      
        package com.brickpets.user.security;
      
      
        
          
import com.auth0.jwt.JWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
public class BrickverseUniHeartJwtDecoder implements JwtDecoder { @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") private String jwkSetUri;
@Override public Jwt decode(String token) throws JwtException { var jwt = JWT.decode(token); String issuer = jwt.getIssuer();
if ("https://brickverse.authing.cn/oidc".equals(issuer)) { return new NimbusJwtDecoderJwkSupport(jwkSetUri).decode(token); }
return new NimbusJwtDecoderJwkSupport("https://uniheart.authing.cn/oidc/.well-known/jwks.json").decode(token); } }

这里主要就是从 token 中取出 issuer,如果是 brickverse 来的,就直接调用之前的配置。如果不是,就当做 uniheart token 来尝试(目前先写死 uniheart 的 jwks url)。

使用 Bean 将自定义 jwt decoder 注入

接着改造一下之前的 src/main/java/com/brickpets/user/config/CustomWebSecurityConfigurerAdapter.java 文件,将新增的 jwt decoder 放进去:

      
        package com.brickpets.user.config;
      
      
        
          
import com.brickpets.user.filters.TokenRemover; + import com.brickpets.user.security.BrickverseUniHeartJwtDecoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; - import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; + import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

@Configuration public class CustomWebSecurityConfigurerAdapter { + @Bean + public JwtDecoder jwtDecoder() { + return new BrickverseUniHeartJwtDecoder(); + }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/user/preference/**").authenticated() .anyRequest().permitAll() .and() .csrf().disable() - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt).addFilterBefore(new TokenRemover(), AbstractPreAuthenticatedProcessingFilter.class); + .addFilterBefore(new TokenRemover(), AbstractPreAuthenticatedProcessingFilter.class) + .oauth2ResourceServer().jwt().decoder(jwtDecoder()) ;

return http.build();

以上就是通过 Bean 的方式将我们新加的 jwt decoder 注入到了应用中去。

有了以上的改动,测试就全通过了。提供代码上线,现在 https://taro.jefftian.dev 也可以使用 user-service 了!







如果有收获,请帮忙点赞点在看!


没什么能够给你,欢迎长按识别以下二维码,领取“哈德韦”出品的微信表情!







浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报