英文:
Spring Boot application always redirect to login despite request having valid access token
问题
我有一个使用JHipster
生成的Spring Boot
微服务应用程序,集成了Keycloak
。以下是该应用程序的版本信息:
- JHipster - 7.9.3
- Spring Boot - 3.0.2
- Spring Cloud - 2022.0.1
- Keycloak - 20.0.3
我手动更新了Spring Boot
版本,而不是使用JHipster
生成的版本。
安全配置如下所示:
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {
private final JHipsterProperties jHipsterProperties;
@Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
private String issuerUri;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(JHipsterProperties jHipsterProperties, SecurityProblemSupport problemSupport) {
this.problemSupport = problemSupport;
this.jHipsterProperties = jHipsterProperties;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/api/authenticate").permitAll()
.requestMatchers("/api/auth-info").permitAll()
.requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers("/api/**").authenticated()
.requestMatchers("/management/health").permitAll()
.requestMatchers("/management/health/**").permitAll()
.requestMatchers("/management/info").permitAll()
.requestMatchers("/management/prometheus").permitAll()
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(authenticationConverter())
.and()
.and()
.oauth2Client();
return http.build();
// @formatter:on
}
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());
return jwtAuthenticationConverter;
}
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
安全相关的应用程序属性如下:
spring:
security:
oauth2:
resource:
filter-order: 3
client:
provider:
oidc:
issuer-uri: http://localhost:8080/realms/samplerealm
registration:
oidc:
authorization-grant-type: client_credentials
client-id: microservice-client
client-secret: <VALID_CLIENT_SECRET>
scope: openid, profile, email, offline_access # 最后一个用于刷新令牌
使用这些配置,应用程序监听localhost:8087
以接收HTTP
请求。
我在Keycloak中创建了另一个客户端dev-client
,并使用Postman
测试应用程序的API。我使用此客户端从Keycloak获取了访问令牌,并在Postman
的Authorization
标头中使用了访问令牌(Bearer ----access token----
)。即使使用有效令牌,API也将我重定向到localhost:8087/login
,并返回一个HTML页面响应。
这是Postman
控制台的快照(由于访问令牌的长度,该快照已被裁剪)。
我不确定为什么请求会被重定向/转发到localhost:8087/login
,即使我提供了有效的访问令牌。我尝试使用不同客户端获取的使用password
授权的访问令牌,但结果仍然相同。
应用程序的任何HTTP
请求都被重定向到localhost:8087/login
,到目前为止,我尝试了GET
请求,但都遇到了这个问题。
英文:
I have a Spring Boot
microservice application generated using JHipster
with Keycloak
. Below are the versions for the application:
- JHipster - 7.9.3
- Spring Boot - 3.0.2
- Spring Cloud - 2022.0.1
- Keycloak - 20.0.3
I had manually updated the Spring Boot
version from the one generated by JHipster
.
The security configuration is as follows:
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {
private final JHipsterProperties jHipsterProperties;
@Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
private String issuerUri;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(JHipsterProperties jHipsterProperties, SecurityProblemSupport problemSupport) {
this.problemSupport = problemSupport;
this.jHipsterProperties = jHipsterProperties;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/api/authenticate").permitAll()
.requestMatchers("/api/auth-info").permitAll()
.requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers("/api/**").authenticated()
.requestMatchers("/management/health").permitAll()
.requestMatchers("/management/health/**").permitAll()
.requestMatchers("/management/info").permitAll()
.requestMatchers("/management/prometheus").permitAll()
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(authenticationConverter())
.and()
.and()
.oauth2Client();
return http.build();
// @formatter:on
}
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());
return jwtAuthenticationConverter;
}
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
The security related application properties are:
spring:
security:
oauth2:
resource:
filter-order: 3
client:
provider:
oidc:
issuer-uri: http://localhost:8080/realms/samplerealm
registration:
oidc:
authorization-grant-type: client_credentials
client-id: microservice-client
client-secret: <VALID_CLIENT_SECRET>
scope: openid, profile, email, offline_access # last one for refresh tokens
With these configurations, the application is listening on localhost:8087
for HTTP
requests.
I created another client in Keycloak dev-client
and using Postman
to test the application API
. I acquired an access token from Keycloak
using this client and used the access token in Postman
in the Authorization
header (Bearer ----access token----
). Even with this valid token, the API
forwards me to localhost:8087/login
with an HTML
page response:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet"
crossorigin="anonymous" />
</head>
<body>
<div class="container">
<h2 class="form-signin-heading">Login with OAuth 2.0</h2>
<table class="table table-striped">
</table>
</div>
</body>
</html>
Here is a snapshot of Postman
console (the snapshot is cropped because of the length of the access token)
I am not sure why are the requests being redirected/forwarded to localhost:8087/login
even if I have provided a valid access token. I have tried poviding an access token which is acquired using password
grant with a different client but it still gave me the same result.
Any HTTP
requests to the application gets forwarded to localhost:8087/login
, so far I tried GET
request and it is throwing me this issue.
答案1
得分: 2
保留默认的 JwtDecoder
使用Spring Boot。不要覆盖 JwtDecoder
只是为了验证 issuer
和 audience
。相反,应该定义Spring Boot属性。
spring:
security:
oauth2:
resourceserver:
jwt:
audiences:
- http://localhost:8087
issuer-uri: http://localhost:8080/realms/samplerealm
有几个很好的原因:
- 如果Spring团队确定应该更改要使用的实现或添加一些配置(出于性能、安全性或其他原因),仅仅升级版本不会使你受益。
- 配置将更容易阅读和理解(例如:“JWT解码器没有特殊之处,只是标准推荐的内容”)。
- 如果你定义了一个新的 bean,你应该对其进行单元测试。你确定你已经掌握了JWT解码器应该做什么?
UI客户端与资源服务器
作为提醒,当资源服务器提供资源时,客户端会消耗这些资源。
在这里,我区分了“UI”(或类似 BFF 的 spring-cloud-gateway
配置为OAuth2客户端)和“REST”客户端。前者提供用户看到的内容,并触发认证服务器上的登录和登出。REST客户端也可能被用于“UI”客户端和资源服务器,以从资源服务器消耗资源。第一种(UI)需要一些特定的安全过滤器链配置,但第二种(REST客户端)通常要么使用已经存在于安全上下文中的访问令牌来代表用户发出请求,要么获取一个新的访问令牌(使用客户端凭据)以自己的名义发出请求。
对于Spring来说,安全方面的考虑不同,因此它提供了不同的库。例如,资源服务器通常可以是无状态的(无会话,状态与令牌相关),而UI客户端通常需要用户会话。
不要在同一安全过滤器链中混合UI客户端和资源服务器配置。如果你需要两者在同一个应用程序中,应创建具有顺序和 securityMatcher
的单独的过滤器链,让第一个仅拦截应该拦截的路由,让第二个作为后备。关于该主题的更多细节可参见“在Spring Boot 3中使用Keycloak Spring Adapter”。
在这里,你似乎只有REST端点 => 从你的安全过滤器链中移除客户端配置(.oauth2Client()
),并确保没有 oauth2Login
,因为它属于客户端问题。
精简化
除非你使用由Spring Boot自动配置的REST客户端(如 WebClient
、@FeignClient
、RestTemplate
等)从另一个微服务获取资源,否则移除 spring.security.oauth2.resource.client
属性和 spring-boot-starter-oauth2-client
。
删除无用的依赖、属性和Java配置将使你的项目更易于维护、更易于调试,在硬件上占用空间更小,并且对新开发人员来说更容易理解。
各种OAuth2使用案例的教程
在我的这个仓库上:https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials
英文:
Keep the Default JwtDecoder
You are using boot. Don't override JwtDecoder
for just validating issuer
and audience
. Define boot properties instead.
spring:
security:
oauth2:
resourceserver:
jwt:
audiences:
- http://localhost:8087
issuer-uri: http://localhost:8080/realms/samplerealm
There are a few good reasons for that:
- if spring team figures out that the implementation to use should be switched or that some configuration should be added, (for performance, security or whatever reason) you won't benefit it by just bumping versions.
- configuration will be easier to read and understand (like: "nothing special regarding JWT decoder, just the standard recommended stuff")
- if you define a new bean you should unit-test it. Are you sure you master enough the specs of what a JWT decoder should do for that?
UI Client V.S. Resource-Server
As a reminder, clients consume resources when resource-servers serve it.
Here I make a distinction between "UI" (or BFF like spring-cloud-gateway
configured as OAuth2 client) and "REST" clients. the first serve what users see and trigger login and logout on the authization-server. REST clients might be used as well on "UI" clients and resource-server to consume a resource from a resource-server. Some specific security filter-chain config is required for the first (UI) but not for the second (REST client) which usually either use the access-token already in the security context to issue a request on behalf of the user or acquires a new access-token (with client credentials) to issue a request in its own name.
Security concerns are different enough for spring to provide WIth different libs. For instance, resource-server can frequently be stateless (no session, the state is associated to the token) when UI clients generally need a session for the user.
Don't mix UI client and resource-server config in the same security filter-chain.
If you need the two in the same application, create separate filter chains with order and securityMatcher
for the first to intercept only the routes it should and let the second act as fallback. More details on that subject in "Use Keycloak Spring Adapter with Spring Boot 3"
Here you seem to have only REST endpoints => remove client configuration from your security filter-chain (.oauth2Client()
and also be sure you don't have oauth2Login
which is a client concern).
Be Lean
Unless you use a REST client autoconfigured by spring-boot (WebClient
, @FeignClient
, RestTemplate
, ...) to fetch resources from another micro-service, also remove spring.security.oauth2.resource.client
properties and spring-boot-starter-oauth2-client
.
Removing useless dependencies, properties and Java configuration will make your project easier to maintain, easier to debug, have a smaller footprint on the hardware and easier to grasp for new developers.
Tutorials for Various OAuth2 Use-Cases
On this repo of mine: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论