Kubernetes中Spring Boot WebFlux的OAuth认证、WebFilter及Bean加载问题
Spring Boot WebFlux + Keycloak 集群环境认证失败(本地正常)
我有一个Spring Boot WebFlux项目,需用Keycloak保护REST API。Keycloak与服务部署在Kubernetes同一命名空间,添加Spring Security前其他服务交互正常。
基于开源示例实现并简化后,本地运行正常,但在Kubernetes集群中无法完成认证。调用服务时出现无SecurityContext,授权被拒绝的日志,但无认证的/health端点可以正常访问。
已尝试的排查步骤
- 初始配置直接沿用示例结构,集群内认证失败
- 尝试自定义WebFilter手动解析JWT,未生效
- 添加日志WebFilter观察请求流转,无输出
- 修改安全配置:禁用CSRF、CORS,关闭HTTP Basic、表单登录、登出等非必要认证方式
- 移除项目中prod和default两个Profile,排除Profile配置异常
- 确认Pod已拉取正确镜像,通过构造函数打印验证相关Bean已创建
- 集群内Keycloak可正常访问:从Pod内用curl获取Token并验证有效,Token Issuer配置正确
- 本地通过端口转发Keycloak到8081,用Insomnia请求服务可正常完成认证
相关代码配置
Dockerfile
FROM gradle:7.6.0-jdk19 EXPOSE 8080 WORKDIR /app COPY . /app RUN apt update -y; apt install curl -y ENTRYPOINT ["gradle", "bootRun"]
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.1.1' id 'io.spring.dependency-management' version '1.1.0' } // .... dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.amqp:spring-amqp' implementation 'com.rabbitmq:amqp-client:5.14.2' implementation 'io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6' compileOnly 'org.projectlombok:lombok' implementation 'io.kubernetes:client-java:17.0.1' implementation 'io.kubernetes:client-java-api-fluent:17.0.1' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.24.1' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0' }
application.yaml
app: security: clientId: "<client>" logging.level: org.springframework.security: DEBUG # .... spring: security: oauth2: resourceserver: jwt: issuer-uri: "http://keycloak:8080/auth/realms/<realm>" #jwk-set-uri: "http://keycloak:8080/auth/realms/<realm>/.well-known/openid-configuration/"
SecurityConfig.java
@EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SecurityConfig { @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Qualifier("jwtFilter")WebFilter jwtFilter, @Qualifier("keycloakJwtAuthenticationConverter") Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter ) { http.csrf(csrfSpec -> csrfSpec.disable()) .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.anyExchange().authenticated()) .httpBasic(httpBasicSpec -> httpBasicSpec.disable()) .logout(logoutSpec -> logoutSpec.disable()) .formLogin(formLoginSpec -> formLoginSpec.disable()) .oauth2ResourceServer(oauth -> oauth .jwt(jwtSpec -> jwtSpec.jwtAuthenticationConverter(jwtAuthenticationConverter))) // fourth try: //.addFilterBefore(jwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); ; return http.build(); } }
KeycloakConfiguration.java
@Configuration public class KeycloakConfiguration { @Bean(name = "keycloakGrantedAuthoritiesConverter") Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter(@Value("${app.security.clientId}") String clientId) { return new KeycloakGrantedAuthoritiesConverter(clientId); } @Bean(name = "keycloakJwtAuthenticationConverter") Converter<Jwt, Mono<AbstractAuthenticationToken>> keycloakJwtAuthenticationConverter( @Qualifier("keycloakGrantedAuthoritiesConverter") Converter<Jwt, Collection<GrantedAuthority>> converter) { return new ReactiveKeycloakJwtAuthenticationConverter(converter); } }
KeycloakGrantedAuthoritiesConverter.java
@RequiredArgsConstructor public class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> { private static final String ROLES = "roles"; private static final String CLAIM_REALM_ACCESS = "realm_access"; private static final String RESOURCE_ACCESS = "resource_access"; private final Converter<Jwt, Collection<GrantedAuthority>> defaultAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); private final String clientId; @Override public Collection<GrantedAuthority> convert(Jwt jwt) { List<String> realmRoles = realmRoles(jwt); List<String> clientRoles = clientRoles(jwt, clientId); Collection<GrantedAuthority> authorities = Stream.concat(realmRoles.stream(), clientRoles.stream()) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); authorities.addAll(defaultGrantedAuthorities(jwt)); System.out.println(authorities); return authorities; } private Collection<GrantedAuthority> defaultGrantedAuthorities(Jwt jwt) { return Optional.ofNullable(defaultAuthoritiesConverter.convert(jwt)) .orElse(emptySet()); } @SuppressWarnings("unchecked") protected List<String> realmRoles(Jwt jwt) { return Optional.ofNullable(jwt.getClaimAsMap(CLAIM_REALM_ACCESS)) .map(realmAccess -> (List<String>) realmAccess.get(ROLES)) .orElse(emptyList()); } @SuppressWarnings("unchecked") protected List<String> clientRoles(Jwt jwt, String clientId) { if (ObjectUtils.isEmpty(clientId)) { return emptyList(); } return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS)) .map(resourceAccess -> (Map<String, List<String>>) resourceAccess.get(clientId)) .map(clientAccess -> clientAccess.get(ROLES)) .orElse(emptyList()); } }
ReactiveKeycloakJwtAuthenticationConverter.java
public class ReactiveKeycloakJwtAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> { private static final String USERNAME_CLAIM = "preferred_username"; private final Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter; public ReactiveKeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) { Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null"); this.jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter( jwtGrantedAuthoritiesConverter); } @Override public Mono<AbstractAuthenticationToken> convert(Jwt jwt) { // @formatter:off return this.jwtGrantedAuthoritiesConverter.convert(jwt) .collectList() .map((authorities) -> new JwtAuthenticationToken(jwt, authorities, extractUsername(jwt))); // @formatter:on } protected String extractUsername(Jwt jwt) { return Optional.ofNullable(jwt.getClaimAsString(USERNAME_CLAIM)).orElseGet(jwt::getSubject); } }
内容的提问来源于stack exchange,提问作者kuemmel
相关产品推荐
相关产品推荐

