Spring Boot中设置响应状态触发AccessDeniedException的解决咨询
问题场景
我在Spring Boot项目中实现了基于Cookie令牌的自定义认证过滤器CookieAuthenticationFilter,但在过滤器中设置响应状态码和自定义错误消息后,访问需要认证的接口会触发如下错误:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause org.springframework.security.access.AccessDeniedException: Access Denied
调试后发现,无论是否设置响应内容,都会抛出AccessDeniedException,但设置响应后由于response已提交,ExceptionTranslationFilter无法处理该异常,进而抛出上述Servlet异常。
相关代码
安全配置类SecurityConfig
@Configuration @EnableWebSecurity public class SecurityConfig { private final CookieAuthenticationFilter cookieAuthenticationFilter; public SecurityConfig(CookieAuthenticationFilter customFilter) { this.cookieAuthenticationFilter = customFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement( s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests( a -> a .requestMatchers("/un/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(cookieAuthenticationFilter, BasicAuthenticationFilter.class); return http.build(); } }
自定义认证过滤器CookieAuthenticationFilter
@Component public class CookieAuthenticationFilter extends OncePerRequestFilter { private final AuthService authService; private final ObjectMapper objectMapper; public CookieAuthenticationFilter( AuthService authService, ObjectMapper objectMapper) { this.authService = authService; this.objectMapper = objectMapper; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 提取认证Cookie的代码(省略) // 通过令牌获取用户信息 UserDto user = null; try { user = authService.getUserFromAuthenticationToken( new AuthenticationTokenValueDto(authCookie.getValue()) ); } catch (CustomAuthException e) { // 写入自定义错误响应 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getOutputStream(), CustomAuthException.MESSAGE); // 原代码调用了filterChain,导致后续过滤器处理异常 filterChain.doFilter(request, response); return; } // 将认证信息存入上下文 Authentication authentication = new PreAuthenticatedAuthenticationToken( user, authCookie.getValue(), List.of() ); SecurityContextHolder.getContext().setAuthentication(authentication); // 执行后续过滤器 filterChain.doFilter(request, response); } }
解决方案
方案1:停止后续过滤器执行(快速修复)
当你已经向response写入自定义错误内容后,不要继续调用filterChain.doFilter(request, response),直接return终止过滤器链即可。因为此时response已提交,后续过滤器(如ExceptionTranslationFilter)无法再修改响应,继续执行只会引发冲突。
修改过滤器的catch块代码:
catch (CustomAuthException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getOutputStream(), CustomAuthException.MESSAGE); // 直接返回,不再执行后续过滤器 return; }
方案2:统一异常处理(更规范的实践)
将认证异常的响应处理委托给Spring Security的异常处理机制,让过滤器只专注于认证逻辑,职责更清晰:
- 让
CustomAuthException继承Spring Security的AuthenticationException(确保被Security的异常处理器识别):
public class CustomAuthException extends AuthenticationException { public static final String MESSAGE = "自定义认证错误信息"; public CustomAuthException(String msg, Throwable t) { super(msg, t); } }
- 在
SecurityConfig中配置自定义的AuthenticationEntryPoint,处理未认证异常:
@Configuration @EnableWebSecurity public class SecurityConfig { private final CookieAuthenticationFilter cookieAuthenticationFilter; private final ObjectMapper objectMapper; public SecurityConfig(CookieAuthenticationFilter customFilter, ObjectMapper objectMapper) { this.cookieAuthenticationFilter = customFilter; this.objectMapper = objectMapper; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(a -> a .requestMatchers("/un/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(cookieAuthenticationFilter, BasicAuthenticationFilter.class) // 配置自定义认证异常处理器 .exceptionHandling(e -> e.authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getOutputStream(), CustomAuthException.MESSAGE); })); return http.build(); } }
- 修改过滤器的catch块,直接抛出异常,交给Security处理:
catch (CustomAuthException e) { // 抛出AuthenticationException,由配置的authenticationEntryPoint处理响应 throw e; }
方案3:关闭ExceptionTranslationFilter(不推荐)
不建议直接关闭该过滤器,因为它负责处理Spring Security的核心异常(如认证失败、权限不足),关闭后会丢失默认的异常处理能力,除非你完全自定义了所有异常处理逻辑。
问题根源
当你在过滤器中调用response.setStatus()或写入响应体后,response的committed状态会被标记为true(响应已提交)。后续的ExceptionTranslationFilter尝试处理AccessDeniedException时,发现无法再修改响应,因此抛出"Unable to handle the Spring Security Exception because the response is already committed"的异常。
内容的提问来源于stack exchange,提问作者the thinker

