通过 Bean 的方式扩展 Spring 应用,使其同时支持多个授权服务颁发...
我在 Authing.cn 中建立了一个 Brickverse 用户池:
在这个用户池下创建了两个应用,一个名为 brickverse,一个名为“哈德韦的个人小程序”。
第一个应用的访问链接是: 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 应用。
而这个服务需要配置授权服务器,我的配置如下:
今天,我想让我的个人小程序,同样可以使用这个 user-service。如果直接调用,哪怕是已经登录过了,接口也会回复 401。原因很简单,我的个人小程序,在登录时,连接的是 Authing.cn 中的另一个 app,即“哈德韦的个人小程序”。
因此,需要扩展 user-service 的安全组件,能够支持 uniheart(我给“哈德韦的个人小程序”app 在 Authing.cn 中起的名字) 公钥端点的 jwt token,即通过某个字段(比如 Issuer)来判断是否是受支持的,是则尝试解开 token,否则直接报错就行。
说干就干,先加两个测试,第一个测试表明无效的令牌调用不了接口,第二个测试使用一个 uniheart token 调用接口,期待通过(现在这个用例会失败,因为还没有实现)。
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());
}
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,后续可以替换成一个活的。
要让这两个测试都通过,需要让安全组件能够针对 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 {
"${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") (
private String jwkSetUri;
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 了!
如果有收获,请帮忙点赞点在看!
没什么能够给你,欢迎长按识别以下二维码,领取“哈德韦”出品的微信表情!