已配置放行的/keycloak端点为何仍被Spring Security要求认证?
我在Spring Security的SecurityFilterChain中配置了/keycloak端点为permitAll,允许匿名访问,但调用该端点时却返回401 Unauthorized响应,服务端日志显示Spring Security仍在尝试对该请求进行认证。
我的SecurityFilterChain配置
@Configuration @RequiredArgsConstructor @EnableWebSecurity(debug = true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { final CorsConfigurationSource configurationSource = serverWebExchange -> { final var cc = new CorsConfiguration(); cc.addAllowedOriginPattern(CorsConfiguration.ALL); cc.addAllowedMethod(CorsConfiguration.ALL); cc.addAllowedHeader(CorsConfiguration.ALL); return cc; }; Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2Customizer = (config) -> config.jwt(); return http .httpBasic().disable() .csrf().disable() .formLogin().disable() .anonymous().disable() .logout().disable() .cors().configurationSource(configurationSource).and() .authorizeRequests((authorize) -> authorize .antMatchers( "/keycloak", ).permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2Customizer) .build(); } }
请求及响应
$ http -v GET $API_SERVER/keycloak clientId==$OAUTH2_CLIENT_ID clientSecret==$OAUTH2_CLIENT_SECRET GET /keycloak?clientId=RDOCUI&clientSecret=Lsx8nSOxKfeSYPBIDgDMjMtD2KTPvma5 HTTP/1.1 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive User-Agent: HTTPie/3.2.1 Host: api-espaidoc-transversal.apps.ocpdes.t-systems.es HTTP/1.1 401 vary: Origin vary: Access-Control-Request-Method vary: Access-Control-Request-Headers set-cookie: JSESSIONID=AD9C22513404F342B07B828878BDBDD1; Path=/; HttpOnly set-cookie: 81d19e38397896573410aee2d8d4fe76=43a159f8dcfa8179b6ba9fae52676052; path=/; HttpOnly www-authenticate: Bearer x-content-type-options: nosniff x-xss-protection: 1; mode=block cache-control: no-cache, no-store, max-age=0, must-revalidate pragma: no-cache expires: 0 x-frame-options: DENY content-length: 0 date: Tue, 25 Apr 2023 14:05:21 GMT
服务端日志
2023-04-25 14:10:08.599 INFO 1 --- [nio-8080-exec-5] Spring Security Debugger : ************************************************************ Request received for GET '/keycloak?clientId=RDOCUI&clientSecret=Lsx8nSOxKfeSYPBIDgDMjMtD2KTPvma5': org.apache.catalina.connector.RequestFacade@56215287 servletPath:/keycloak pathInfo:null headers: accept-encoding: gzip, deflate accept: */* user-agent: HTTPie/3.2.1 host: api-espaidoc-transversal.apps.ocpdes.t-systems.es x-forwarded-host: api-espaidoc-transversal.apps.ocpdes.t-systems.es x-forwarded-port: 80 x-forwarded-proto: http forwarded: for=10.49.88.118;host=api-espaidoc-transversal.apps.ocpdes.t-systems.es;proto=http Security filter chain: [ WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CorsFilter BearerTokenAuthenticationFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ] ************************************************************ 2023-04-25 14:10:08.600 DEBUG 1 --- [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Securing GET /keycloak?clientId=RDOCUI&clientSecret=Lsx8nSOxKfeSYPBIDgDMjMtD2KTPvma5 2023-04-25 14:10:08.600 DEBUG 1 --- [nio-8080-exec-5] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext 2023-04-25 14:10:08.602 INFO 1 --- [nio-8080-exec-5] Spring Security Debugger : ************************************************************ New HTTP session created: 34CAE9608DEE0A209767B7F95B3C1071 Call stack: at org.springframework.security.web.debug.Logger.info(Logger.java:46) at org.springframework.security.web.debug.DebugFilter$DebugRequestWrapper.getSession(DebugFilter.java:167) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:253) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:253) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:253) at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:253) at org.springframework.security.web.savedrequest.HttpSessionRequestCache.saveRequest(HttpSessionRequestCache.java:73) at org.springframework.security.web.access.ExceptionTranslationFilter.sendStartAuthentication(ExceptionTranslationFilter.java:210) at org.springframework.security.web.access.ExceptionTranslationFilter.handleAuthenticationException(ExceptionTranslationFilter.java:178) at org.springframework.security.web.access.ExceptionTranslationFilter.handleSpringSecurityException(ExceptionTranslationFilter.java:168) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:140) at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126) at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:115) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110) at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90) at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:78) at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:67) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:764) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.base/java.lang.Thread.run(Unknown Source) ************************************************************ 2023-04-25 14:10:08.602 DEBUG 1 --- [nio-8080-exec-5] o.s.s.w.s.HttpSessionRequestCache : Saved request http://api-espaidoc-transversal.apps.ocpdes.t-systems.es/keycloak?clientId=RDOCUI&clientSecret=Lsx8nSOxKfeSYPBIDgDMjMtD2KTPvma5 to session 2023-04-25 14:10:08.602 DEBUG 1 --- [nio-8080-exec-5] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext 2023-04-25 14:10:08.602 DEBUG 1 --- [nio-8080-exec-5] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
控制器代码
@RestController @RequiredArgsConstructor public class TokenController { private final OAuth2TokenProvider oauth2TokenProvider; @GetMapping("/keycloak") public String getToken(@RequestParam String clientId, @RequestParam String clientSecret) { return this.oauth2TokenProvider.getToken(clientId, clientSecret); } }
请问为何Spring Security会尝试对这个已配置放行的请求进行认证?
问题原因及解决方案
核心原因
- 匿名认证被禁用:你配置了
.anonymous().disable(),但permitAll的本质是允许匿名用户访问该端点。禁用匿名认证后,Spring Security无法识别未认证的请求为合法的匿名身份,会强制触发认证流程。 - BearerTokenAuthenticationFilter执行顺序问题:该过滤器在
FilterSecurityInterceptor(负责权限判断的过滤器)之前执行。它会尝试从请求中提取Bearer Token,当请求没有携带Token时,会直接抛出AuthenticationException,此时权限判断逻辑还未执行,ExceptionTranslationFilter会直接返回401响应。
解决步骤
步骤1:恢复匿名认证支持
移除.anonymous().disable(),Spring Security默认启用匿名认证,这是permitAll生效的前提。
步骤2:跳过指定端点的BearerToken检查
为/keycloak端点配置过滤器忽略规则,避免BearerTokenAuthenticationFilter对该端点的请求进行Token校验。
修改后的SecurityFilterChain配置
@Configuration @RequiredArgsConstructor @EnableWebSecurity(debug = true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { final CorsConfigurationSource configurationSource = serverWebExchange -> { final var cc = new CorsConfiguration(); cc.addAllowedOriginPattern(CorsConfiguration.ALL); cc.addAllowedMethod(CorsConfiguration.ALL); cc.addAllowedHeader(CorsConfiguration.ALL); return cc; }; Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2Customizer = (config) -> config.jwt(); // 为除/keycloak外的请求配置安全规则 http.securityMatcher(negate(new AntPathRequestMatcher("/keycloak"))) .httpBasic().disable() .csrf().disable() .formLogin().disable() .logout().disable() .cors().configurationSource(configurationSource).and() .authorizeRequests((authorize) -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2Customizer); // 单独为/keycloak配置放行规则,不应用OAuth2相关过滤器 http.securityMatcher("/keycloak") .authorizeRequests(authorize -> authorize.anyRequest().permitAll()) .csrf().disable() .cors().configurationSource(configurationSource); return http.build();
相关产品推荐
相关产品推荐

