Spring Boot集成Keycloak:Postman JWT认证失败,仅支持页面登录
问题描述
参考Spring Boot集成Keycloak的示例实现功能,部署完成后:
- 浏览器访问
http://localhost:8081/customers会重定向到Keycloak登录页,输入凭据后API正常返回数据,符合预期 - 用Postman获取JWT令牌后,以Bearer认证方式访问该接口,仍被重定向到Keycloak登录页
尝试过的操作:
- 修改
grant_type为client_credentials、password - 注释配置中的
http.oauth2Login() - 使用已废弃的
KeycloakWebSecurityConfigurerAdapter时可同时支持页面登录和JWT认证,但该方式不推荐
提出两个问题:
- 为何会出现这种情况?
- 如何配置才能同时支持两种认证方式?
应用调试日志如下:
12:11:49.133 [http-nio-8081-exec-2] DEBUG o.k.adapters.PreAuthActionsHandler - adminRequest http://localhost:8081/customers 12:11:49.137 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Using provider 'secret' for authentication of client 'login-app' 12:11:49.138 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider secret 12:11:49.139 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider jwt 12:11:49.139 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider secret-jwt 12:11:49.139 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider secret 12:11:49.139 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider jwt 12:11:49.139 [http-nio-8081-exec-2] DEBUG o.k.a.a.ClientCredentialsProviderUtils - Loaded clientCredentialsProvider secret-jwt 12:11:49.145 [http-nio-8081-exec-2] DEBUG o.k.a.t.AbstractAuthenticatedActionsValve - AuthenticatedActionsValve.invoke /customers 12:11:49.145 [http-nio-8081-exec-2] DEBUG o.k.a.AuthenticatedActionsHandler - AuthenticatedActionsValve.invoke http://localhost:8081/customers 12:11:49.145 [http-nio-8081-exec-2] DEBUG o.k.a.AuthenticatedActionsHandler - Policy enforcement is disabled. 12:11:49.147 [http-nio-8081-exec-2] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet' 12:11:49.147 [http-nio-8081-exec-2] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet' 12:11:49.147 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Detected StandardServletMultipartResolver 12:11:49.147 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Detected AcceptHeaderLocaleResolver 12:11:49.147 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Detected FixedThemeResolver 12:11:49.148 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@28b16193 12:11:49.148 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Detected org.springframework.web.servlet.support.SessionFlashMapManager@5d56c2d2 12:11:49.148 [http-nio-8081-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data 12:11:49.148 [http-nio-8081-exec-2] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms 12:11:49.155 [http-nio-8081-exec-2] DEBUG o.s.security.web.FilterChainProxy - Securing GET /customers 12:11:49.158 [http-nio-8081-exec-2] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext 12:11:49.160 [http-nio-8081-exec-2] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext 12:11:49.160 [http-nio-8081-exec-2] DEBUG o.s.s.w.s.SessionManagementFilter - Request requested invalid session id 2188C37ADF15140FB15235D80AAA0C77 12:11:49.165 [http-nio-8081-exec-2] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [GET /customers] with attributes [authenticated] 12:11:49.207 [http-nio-8081-exec-2] DEBUG o.s.s.w.s.HttpSessionRequestCache - Saved request http://localhost:8081/customers to session 12:11:49.207 [http-nio-8081-exec-2] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], Not [And [Or [Ant [pattern='/login'], Ant [pattern='/favicon.ico']], And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy@21fa9d8, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]]]] 12:11:49.207 [http-nio-8081-exec-2] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@681b92a9 12:11:49.207 [http-nio-8081-exec-2] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8081/oauth2/authorization/keycloak 12:11:49.208 [http-nio-8081-exec-2] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext 12:11:49.209 [http-nio-8081-exec-2] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext 12:11:49.209 [http-nio-8081-exec-2] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request 12:11:49.228 [http-nio-8081-exec-4] DEBUG o.k.adapters.PreAuthActionsHandler - adminRequest http://localhost:8081/oauth2/authorization/keycloak 12:11:49.229 [http-nio-8081-exec-4] DEBUG o.k.a.t.AbstractAuthenticatedActionsValve - AuthenticatedActionsValve.invoke /oauth2/authorization/keycloak 12:11:49.229 [http-nio-8081-exec-4] DEBUG o.k.a.AuthenticatedActionsHandler - AuthenticatedActionsValve.invoke http://localhost:8081/oauth2/authorization/keycloak 12:11:49.229 [http-nio-8081-exec-4] DEBUG o.k.a.AuthenticatedActionsHandler - Policy enforcement is disabled. 12:11:49.229 [http-nio-8081-exec-4] DEBUG o.s.security.web.FilterChainProxy - Securing GET /oauth2/authorization/keycloak 12:11:49.229 [http-nio-8081-exec-4] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext 12:11:49.234 [http-nio-8081-exec-4] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8090/auth/realms/main-authentication/protocol/openid-connect/auth?response_type=code&client_id=login-app&scope=openid&state=aoSFWCq-HdIiwwi_SB8oKFI29T2EJhHQmlz0e8h9RAY%3D&redirect_uri=http://localhost:8081/login/oauth2/code/keycloak&nonce=Dlsh8t93QaYB3oqVvAtlvssCEF_UBxdibFHfyIPGnv4 12:11:49.234 [http-nio-8081-exec-4] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext 12:11:49.234 [http-nio-8081-exec-4] DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext 12:11:49.234 [http-nio-8081-exec-4] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request
问题解答
1. 问题原因分析
从日志可见,Spring Security的FilterSecurityInterceptor判定请求未通过授权,触发LoginUrlAuthenticationEntryPoint进行重定向。核心原因是当前配置仅启用了OAuth2授权码模式的页面登录(oauth2Login()),未配置JWT Bearer令牌的认证逻辑,导致系统无法识别请求中的Bearer令牌,直接判定为未认证,进而重定向到登录页。
另外,注释http.oauth2Login()后仍失效,是因为移除页面登录逻辑后,未补充JWT认证的配置,此时系统没有有效的认证入口,依然会触发默认的重定向行为。
2. 同时支持两种认证方式的配置方案
需要在Spring Security配置中同时注册OAuth2登录过滤器和JWT Bearer认证过滤器,并配置对应的认证管理器和入口点,让系统能根据请求类型自动选择认证方式。
核心配置代码示例
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.preauth.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import java.util.List; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 配置授权规则 .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) // 启用OAuth2页面登录 .oauth2Login(oauth2 -> oauth2 .loginPage("/oauth2/authorization/keycloak") ) // 启用JWT Bearer认证 .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .decoder(jwtDecoder()) ) ) // 配置认证入口点:区分浏览器请求和API请求 .exceptionHandling(ex -> ex .authenticationEntryPoint(new DelegatingAuthenticationEntryPoint( List.of( // API请求返回401 new MediaTypeRequestMatcher(MediaType.APPLICATION_JSON), new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest") ), // 浏览器请求重定向到登录页 new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/keycloak") )) ); return http.build(); } @Bean public JwtDecoder jwtDecoder() { // 替换为你的Keycloak JWKS端点 String jwksUri = "http://localhost:8090/auth/realms/main-authentication/protocol/openid-connect/certs"; return NimbusJwtDecoder.withJwkSetUri(jwksUri).build(); } }
关键配置说明
oauth2Login():保留页面登录支持,处理浏览器端的授权码流程。oauth2ResourceServer().jwt():添加JWT Bearer认证支持,让系统能解析并验证请求头中的Authorization: Bearer <token>。- 自定义
AuthenticationEntryPoint:区分浏览器请求和API请求,浏览器请求重定向到登录页,API请求返回401 Unauthorized,避免API被错误重定向。 JwtDecoder:通过Keycloak的JWKS端点获取公钥,用于验证JWT签名的合法性。
额外注意事项
- 确保Keycloak客户端配置中,同时启用了
Authorization Code和允许使用JWT令牌访问。 - Postman请求时,需正确设置
Authorization头为Bearer <你的JWT令牌>,并确保令牌的aud(受众)包含当前应用的client_id。 - 若使用Spring Boot 3.x,需注意依赖版本适配,引入
spring-boot-starter-oauth2-resource-server和spring-boot-starter-oauth2-client依赖。
内容的提问来源于stack exchange,提问作者thahgr
相关产品推荐
相关产品推荐

