Spring Security集成Keycloak时发行者URI不匹配问题求解
问题背景
我正在使用Spring Security与Keycloak集成,配置文件相关属性:
spring.security.oauth2.client.provider.keycloak.issuer-uri = http://a.com/auth/realms/arealm
协议为http。
Keycloak公网地址是https://a.com/auth/realms/arealm,协议为https。
Keycloak启动参数:
command: - start - --optimized - --http-enabled=true - --proxy-headers - forwarded - --hostname - https://a.com/auth/ - --hostname-backchannel-dynamic=true
部署架构:Keycloak与应用同属局域网,HTTPS由前端负载均衡器终止。应用内调用以下URL可正常响应:
curl http://a.com/auth/realms/arealm/.well-known/openid-configuration
但Spring Security报错:
The Issuer https://a.com/auth/realms/arealm provided in the configuration metadata did not match the requested issuer http://a.com/auth/realms/arealm
不想把Spring配置改成https(会迫使应用走公网访问Keycloak,曾用/etc/hosts映射但方案不优雅),求解决发行者URI不匹配的方法。
解决方案
方案1:调整Keycloak后端通道hostname配置
修改Keycloak启动参数,指定后端通道使用局域网内的http地址作为hostname,这样Keycloak返回的OpenID元数据中issuer会和Spring配置的http地址一致:
command: - start - --optimized - --http-enabled=true - --proxy-headers - forwarded - --hostname - https://a.com/auth/ # 替换动态后端hostname为固定http地址 - --hostname-backchannel=http://a.com/auth/
此方案无需修改Spring代码,仅调整Keycloak配置,是最优雅的解决方案,直接从源头解决了元数据中issuer不匹配的问题。
方案2:自定义JWT Issuer验证器
通过自定义验证逻辑,允许Spring Security接受http和https两种版本的issuer:
- 编写自定义验证器类:
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; public class FlexibleIssuerValidator implements OAuth2TokenValidator<Jwt> { private final String baseIssuer; public FlexibleIssuerValidator(String baseIssuer) { this.baseIssuer = baseIssuer; } @Override public OAuth2TokenValidatorResult validate(Jwt token) { String tokenIssuer = token.getIssuer(); // 匹配http/https两种协议的issuer if (baseIssuer.equals(tokenIssuer) || baseIssuer.replace("http://", "https://").equals(tokenIssuer) || baseIssuer.replace("https://", "http://").equals(tokenIssuer)) { return OAuth2TokenValidatorResult.success(); } return OAuth2TokenValidatorResult.failure( new OAuth2Error("invalid_issuer", "Issuer does not match expected value", null) ); } }
- 在Security配置中注册该验证器:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; @Configuration public class SecurityConfig { @Bean public JwtDecoder jwtDecoder() { String issuerUri = "http://a.com/auth/realms/arealm"; NimbusJwtDecoder decoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri); // 组合默认验证器与自定义验证器 decoder.setJwtValidator( JwtValidators.createDefaultWithIssuer(issuerUri) .and(new FlexibleIssuerValidator(issuerUri)) ); return decoder; } }
方案3:修改OpenID元数据中的Issuer值
通过Spring的OAuth2ClientPropertiesCustomizer,在加载Keycloak元数据时替换issuer为配置的http地址:
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OAuth2Config { @Bean public OAuth2ClientPropertiesCustomizer clientPropertiesCustomizer() { return properties -> { // 强制替换Keycloak provider元数据中的issuer为http地址 properties.getProvider() .get("keycloak") .getMetadata() .put("issuer", "http://a.com/auth/realms/arealm"); }; } }
内容的提问来源于stack exchange,提问作者uylmz

