Spring Boot3返回StreamingResponseBody时AccessDeniedException问题求助
问题排查:流式音频API的Spring Security异常问题
问题场景
开发了流式返回MP3的REST API,Postman调用时响应正常、音频可播放,但服务器终端持续抛出AccessDeniedException,根因提示响应已提交无法处理Spring Security异常。
API代码实现
@GetMapping(value = "/api/user/stream/{songId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity<StreamingResponseBody> playSong(@PathVariable Long songId) throws IOException { File file = new File(projectPath + "\assets\audio\DummySong" + ".mp3"); FileInputStream in = new FileInputStream(file); StreamingResponseBody songStream = out -> { try { Thread.sleep(10); IOUtils.copy(in, out); } catch (InterruptedException e) { LOGGER.error("Streaming Thread Interrupted - {}", e.getMessage()); } }; return ResponseEntity.ok() .header(HttpHeaders.ACCEPT_RANGES, "128") .header(HttpHeaders.CONTENT_TYPE, "audio/mpeg") .contentLength(file.length()) .body(songStream); }
Spring Security配置
@Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests .requestMatchers("/auth/signin", "/auth/signup", "/docs/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/artist/**").hasAnyRole("ARTIST", "ADMIN") .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ); http.exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint((req, resp, e) -> { LOGGER.error("Error during auth: {}", e.getMessage()); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); })); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }
异常信息
org.springframework.security.access.AccessDeniedException: Access Denied at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:98) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:91) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:85) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.2.jar:6.1.2] at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.1.2.jar:6.1.2]
根因错误:
2023-08-17T18:35:48.233+05:30 ERROR 10528 --- [0.0-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet] : 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.
问题原因分析
- 流式响应与过滤器链的时序冲突:
StreamingResponseBody是异步写入响应的,控制器返回ResponseEntity后响应立即开始提交(甚至部分音频已发送给客户端),但Spring Security的后续过滤器仍在执行权限校验。此时若校验失败,尝试修改响应(返回403)时,会发现响应已提交,从而抛出错误。 - HTTP头配置错误:
ACCEPT_RANGES头设置为"128"不符合HTTP规范,该头应设置为"bytes"表示支持字节范围请求。 - 资源泄漏风险:
FileInputStream未自动关闭,可能导致资源泄漏。 - 路径转义问题:Java字符串中反斜杠需转义,原代码路径
"\assets\audio\DummySong"存在解析风险。
修复方案
1. 修正HTTP头与路径问题
// 修正路径为正斜杠,避免转义错误 File file = new File(projectPath + "/assets/audio/DummySong.mp3"); // 使用try-with-resources自动关闭输入流 try (FileInputStream in = new FileInputStream(file)) { StreamingResponseBody songStream = out -> { try { IOUtils.copy(in, out); } catch (IOException e) { LOGGER.error("Error streaming audio: {}", e.getMessage()); } }; return ResponseEntity.ok() // 修正ACCEPT_RANGES头为标准值 .header(HttpHeaders.ACCEPT_RANGES, "bytes") .header(HttpHeaders.CONTENT_TYPE, "audio/mpeg") .contentLength(file.length()) .body(songStream); }
2. 提前校验权限,避免响应提交后触发校验
在控制器方法开头手动校验权限,确保响应提交前完成权限检查:
@GetMapping(value = "/api/user/stream/{songId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity<StreamingResponseBody> playSong(@PathVariable Long songId, Authentication authentication) throws IOException { // 手动校验用户角色 boolean hasPermission = authentication.getAuthorities().stream() .anyMatch(auth -> auth.getAuthority().equals("ROLE_USER") || auth.getAuthority().equals("ROLE_ADMIN")); if (!hasPermission) { throw new AccessDeniedException("Access Denied"); } // 后续文件与流式响应逻辑... }
3. 优化Spring Security异常处理
自定义异常处理器,检查响应是否已提交,避免重复修改响应:
http.exceptionHandling((exceptionHandling) -> exceptionHandling .authenticationEntryPoint((req, resp, e) -> { if (!resp.isCommitted()) { LOGGER.error("Error during auth: {}", e.getMessage()); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); } else { LOGGER.warn("Response already committed, skip sending unauthorized error"); } }) .accessDeniedHandler((req, resp, e) -> { if (!resp.isCommitted()) { LOGGER.error("Access denied: {}", e.getMessage()); resp.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); } else { LOGGER.warn("Response already committed, skip sending forbidden error"); } }) );
4. 检查JWT过滤器逻辑
确保jwtTokenFilter正确解析所有/api/user/**请求的令牌,并将Authentication对象设置到SecurityContextHolder中。若令牌缺失或解析失败,需在响应提交前返回401,避免后续权限校验冲突。
内容的提问来源于stack exchange,提问作者Kalpadiptya Roy
相关产品推荐
相关产品推荐

