Spring Security oauth Authentication/WebFilter/Bean-loading issues with Sping Boot ,Webflux in Kubernetes?

rdrgkggo  于 2023-08-05  发布在  Spring
关注(0)|答案(1)|浏览(127)

(请告诉我如何改进这个问题,或者我是否应该用不同的标记,第一次写关于stackoverflow的问题后,几个小时的丢失。)
我不得不删除一些网址,以绕过垃圾邮件过滤器。
我可能犯了一些很基本的错误,但我不知道接下来该怎么办,所以我很高兴听到各种各样的提示。
我有一个 Spring 启动webflux项目。这只是一个REST API,我想用keycloak来保护它。Keycloak和spring-boot服务被部署到kubernetes集群的同一个命名空间中。在添加spring.security之前,与其他服务和kubernetes-api有更多的交互。
我的实施基于以下演示:https://github.com/rbiedrawa/spring-webflux-keycloak-demo,并对其进行了简化,以便能够更好地对其进行测试。
安装程序在本地工作正常,但我就是不能让它在集群中工作。我尝试过多种设置:我先尝试了下面的设置,然后尝试实现我自己的WebFilter来解析JWT令牌。WebFilter也不会被使用,所以我实现了一个WebFilter,它应该记录请求,但也不起作用。我已经尝试过更改安全配置(如禁用cors,csrf等)。没有身份验证的/health终结点正常工作,没有问题。
我有两个档案:prod和默认值。我已经删除了个人资料,只是为了确保我在做一些奇怪的事情。我使用的是gradle,并简化了Dockerfile,使其可以在集群中构建。
pod正在接收正确的图像,我可以从日志中看到这一点(我还知道bean是创建的-我只是向构造函数添加了print)。
驳接文件:

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/"

x

@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();
    }
}
@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);
    }
/*
See https://github.com/rbiedrawa/spring-webflux-keycloak-demo
 */
@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());
    }
}
/*
See https://github.com/rbiedrawa/spring-webflux-keycloak-demo
 */
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);
    }

}

的一个或多个字符
keycloat在集群中可用,我已经尝试过从带有curl的pod进行身份验证,效果很好。令牌颁发者似乎是正确的--尽管我怀疑我的问题不是身份验证,而是spring-boot没有加载/使用bean?我尝试过简单的打印,但没有执行任何操作。
我在做什么:

  • 我使用端口转发到kleyclaok通过8081本地和使用失眠,这只是为了显示的过程:
TOKEN=$(curl --request POST \
  --url /keycloak/realms/<realm>/protocol/openid-connect/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  ...)
curl --request GET \
  --url /... \
  --header 'Authorization: Bearer $TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{...}'


结果如下所示:

2023-07-09T14:38:54.763+02:00 DEBUG 651138 --- [     parallel-2] a.DelegatingReactiveAuthorizationManager : Checking authorization on '<path>' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@64a16132
2023-07-09T14:38:54.764+02:00 DEBUG 651138 --- [     parallel-2] o.s.s.w.s.a.AuthorizationWebFilter       : Authorization successful
2023-07-09T14:38:54.766+02:00  INFO 651138 --- [     parallel-2] d.h.d.o.config.keycloak.JwtFilter        : Authorization payload: Bearerd.h.dafne.orchestrator.config.LogFilter  : REQUEST: <request>

  • 但是,在集群上,情况就不同了:我正在使用代理对代理调用服务。
curl --request POST \
  --url /keycloak/realms/<realm>/protocol/openid-connect/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  ...
curl --request GET \
  --url /api/v1/namespaces/<ns>/services/<service>:http/proxy/<path> \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '....'


群集上的结果:

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.1)

2023-07-09T13:03:01.816Z  INFO 165 --- [           main] d.h.d.o.OrchestratorApplication          : Starting OrchestratorApplication using Java 19.0.2 with PID 165 (/app/build/classes/java/main started by root in /app)
2023-07-09T13:03:01.818Z  INFO 165 --- [           main] d.h.d.o.OrchestratorApplication          : No active profile set, falling back to 1 default profile: "default" # again, simplified version, defualt contains the right paths
2023-07-09T13:03:02.414Z  INFO 165 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2023-07-09T13:03:02.417Z  INFO 165 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2023-07-09T13:03:02.463Z  INFO 165 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 28 ms. Found 0 Redis repository interfaces.
Loading LogFilter
Loading JwtFilter!
2023-07-09T13:03:03.416Z  INFO 165 --- [           main] d.h.d.orchestrator.config.RedisConfig    : Connecing to redis on url userstate-db:8082
2023-07-09T13:03:04.263Z  INFO 165 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080
2023-07-09T13:03:04.273Z  INFO 165 --- [           main] d.h.d.o.OrchestratorApplication          : Started OrchestratorApplication in 2.868 seconds (process running for 3.176)
2023-07-09T13:03:46.204Z DEBUG 165 --- [or-http-epoll-2] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Trying to match using org.springframework.security.web.server.csrf.CsrfWebFilter$DefaultRequireCsrfProtectionMatcher@256237f7
2023-07-09T13:03:46.205Z DEBUG 165 --- [or-http-epoll-2] .s.s.w.s.u.m.AndServerWebExchangeMatcher : Did not match
2023-07-09T13:03:46.225Z DEBUG 165 --- [     parallel-1] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=POST}
2023-07-09T13:03:46.228Z DEBUG 165 --- [     parallel-1] athPatternParserServerWebExchangeMatcher : Request 'GET /service/yml' doesn't match 'POST /logout'
2023-07-09T13:03:46.228Z DEBUG 165 --- [     parallel-1] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2023-07-09T13:03:46.231Z DEBUG 165 --- [     parallel-1] a.DelegatingReactiveAuthorizationManager : Checking authorization on '<path>' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@28fc745d
2023-07-09T13:03:46.234Z DEBUG 165 --- [     parallel-1] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@2f91c766'
2023-07-09T13:03:46.234Z DEBUG 165 --- [     parallel-1] o.s.s.w.s.a.AuthorizationWebFilter       : Authorization failed: Access Denied
2023-07-09T13:03:46.240Z DEBUG 165 --- [     parallel-1] ebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@2f91c766'

3zwtqj6y

3zwtqj6y1#

因此,答案与 spring security 无关。
它不工作的原因是Authentication头被kubectl proxy剥离。我认为至少有一些日志会有所帮助(我试图为所有内容设置debug,但即使这样,我也看不到指示器)。当我删除了所有的www.example.com来实现自己的安全检查时,我才注意到这一点spring.security--我试图通过@RequestHeader注解来获取头部。这最终给了我我需要的东西,因为只有在那时spring才记录没有Authentication头。

相关问题