(请告诉我如何改进这个问题,或者我是否应该用不同的标记,第一次写关于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'
型
1条答案
按热度按时间3zwtqj6y1#
因此,答案与 spring security 无关。
它不工作的原因是
Authentication
头被kubectl proxy
剥离。我认为至少有一些日志会有所帮助(我试图为所有内容设置debug
,但即使这样,我也看不到指示器)。当我删除了所有的www.example.com来实现自己的安全检查时,我才注意到这一点spring.security--我试图通过@RequestHeader
注解来获取头部。这最终给了我我需要的东西,因为只有在那时spring才记录没有Authentication
头。