基于 Spring Security 5 构筑的 Resource Server 在使用内部域名访问认证服务器时因验证 Token 失败而报错:IllegalStateException: The Issuer "https://my.public.com" provided in the configuration did not match the requested issuer "http://my.private.dev"
。谷歌了一圈没有现成的解决方案,只能从源码中寻找思路。结论说在前面,解决方案很简单,不需要定制 Java 代码,只需要调整 Yaml 的配置即可。
背景
先来复习一下OAuth 2.0的运行流程,下图摘自RFC 6749。
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
我们的 Resource Server 是基于 Spring Security 构筑的。依赖配置如下:
implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server:5.6.1"
Authorization Server 使用的是 KeyCloak。前端访问 Resource Server 的流程如下:
到这里应该都不难理解,接下来重点来了。KeyCloak 可以通过两个域名访问,一个是对外的 https://my.public.com
,一个是对内的 http://my.private.dev
。 前端的访问则需要通过外部域名访问。由于 Resource Server 和 KeyCloak 都处于同一个 AWS VPC 中,所以可通过内部域名访问,不需要SSL加密,Yaml 中配置如下:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://my.private.dev/auth/realms/my-realm
问题
时序图中的 1
~ 3
都没问题,在第 4
步时因验证 Token 失败而报错:
Caused by: java.lang.IllegalStateException: The Issuer "https://my.public.com" provided in the configuration did not match the requested issuer "http://my.private.dev"
at org.springframework.util.Assert.state(Assert.java:97)
at org.springframework.security.oauth2.jwt.JwtDecoderProviderConfigurationUtils.validateIssuer(JwtDecoderProviderConfigurationUtils.java:84)
at org.springframework.security.oauth2.jwt.ReactiveJwtDecoders.withProviderConfiguration(ReactiveJwtDecoders.java:107)
at org.springframework.security.oauth2.jwt.ReactiveJwtDecoders.fromIssuerLocation(ReactiveJwtDecoders.java:92)
at org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerJwkConfiguration$JwtConfiguration.lambda$jwtDecoderByIssuerUri$0(ReactiveOAuth2ResourceServerJwkConfiguration.java:97)
at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86)
at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:227)
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68)
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28)
at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264)
原因
从字面上不难看出,应该是 Issuer
校验失败。KeyCloak 对前端发行的 Token 中包含 iss
属性,值对应的是外部域名即 https://my.public.com
,而 Resource Server 是通过内部域名与 KeyCloak 连接的,因而在使用 JwtDecoderProviderConfigurationUtils.validateIssuer
进行校验时失败了。
根据 Spring Security 官方文档 JWT 章节的描述,Jwt认证流程如下:
- ① 认证过滤器将
BearerTokenAuthenticationToken
传递给由 ProviderManager 实现的 AuthenticationManager。- ② ProviderManager 将使用一个 JwtAuthenticationProvider 类型的AuthenticationProvider。
- ③ JwtAuthenticationProvider使用JwtDecoder对
Jwt
进行解码、验证和确认。- ④ 然后JwtAuthenticationProvider使用JwtAuthenticationConverter将
Jwt
转换为权限的集合。- ⑤ 当认证成功时,返回的认证是JwtAuthenticationToken类型,并且包含由JwtDecoder返回的Jwt。最终,返回的JwtAuthenticationToken将被认证过滤器设置在SecurityContextHolder上。
根据错误日志,可以判断问题处在上述第③步 JwtDecoder
上。那么这个 JwtDecoder 是如何创建的呢?从下面这行日志可以看出,是在 ReactiveOAuth2ResourceServerJwkConfiguration
中配置的。
at org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerJwkConfiguration$JwtConfiguration.lambda$jwtDecoderByIssuerUri$0(ReactiveOAuth2ResourceServerJwkConfiguration.java:97)
打开 ReactiveOAuth2ResourceServerJwkConfiguration
代码发现,只有在 IssuerUriCondition
中定义的条件成立,才会创建对应的 Bean 。而在创建 JwtDecoder
Bean 时会校验 iss
,若发现 Token 中的issuer 和在Yaml配置的 issuer 不匹配时,就报错了。
@Bean
@Conditional(IssuerUriCondition.class)
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() {
return new SupplierReactiveJwtDecoder(
() -> ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri()));
}
以下是 IssuerUriCondition
的代码。可以看到,只有当 application.yaml
中设置了 issuer-uri
,且没有设置 jwk-set-uri
时,条件才会成立。
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OpenID Connect Issuer URI Condition");
Environment environment = context.getEnvironment();
String issuerUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.issuer-uri");
String jwkSetUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri");
if (!StringUtils.hasText(issuerUri)) {
return ConditionOutcome.noMatch(message.didNotFind("issuer-uri property").atAll());
}
if (StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.found("jwk-set-uri property").items(jwkSetUri));
}
return ConditionOutcome.match(message.foundExactly("issuer-uri property"));
}
看到上面这段代码,我突然发现应该有办法绕过对 issuer 的校验。那就是单独使用 jwk-set-uri
。在ReactiveOAuth2ResourceServerJwkConfiguration
看中找到如下代码,如果在 application.yaml
中设置了 jwk-set-uri
,则会创建 NimbusReactiveJwtDecoder
,如果同时也设置了 issuer-uri
, 则会对 issuer
进行校验,如果没有设置,就不校验 issuer
了。 😃
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder
.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
String issuerUri = this.properties.getIssuerUri();
if (issuerUri != null) {
nimbusReactiveJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
}
return nimbusReactiveJwtDecoder;
}
解决办法
在 application.yaml
中进行如下配置:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://my.private.dev/auth/realms/my-realm/protocol/openid-connect/certs