You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

已配置放行的/keycloak端点为何仍被Spring Security要求认证?

问题:Spring Security已配置permitAll的端点仍返回401 Unauthorized

我在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会尝试对这个已配置放行的请求进行认证?


问题原因及解决方案

核心原因

  1. 匿名认证被禁用:你配置了.anonymous().disable(),但permitAll的本质是允许匿名用户访问该端点。禁用匿名认证后,Spring Security无法识别未认证的请求为合法的匿名身份,会强制触发认证流程。
  2. 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();
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 05:01:59