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

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.

问题原因分析

  1. 流式响应与过滤器链的时序冲突:StreamingResponseBody是异步写入响应的,控制器返回ResponseEntity后响应立即开始提交(甚至部分音频已发送给客户端),但Spring Security的后续过滤器仍在执行权限校验。此时若校验失败,尝试修改响应(返回403)时,会发现响应已提交,从而抛出错误。
  2. HTTP头配置错误:ACCEPT_RANGES头设置为"128"不符合HTTP规范,该头应设置为"bytes"表示支持字节范围请求。
  3. 资源泄漏风险:FileInputStream未自动关闭,可能导致资源泄漏。
  4. 路径转义问题: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 04:57:10