ResourceServer如何使用内部域名访问认证服务器

677 阅读3分钟

基于 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

image.png

(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 的流程如下:

image.png

到这里应该都不难理解,接下来重点来了。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认证流程如下:

image.png

  • ① 认证过滤器将 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

参考链接