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

Spring Boot MockMvc单元测试返回403 Forbidden求助

问题:POST接口单元测试返回403 Forbidden排查

我为PDFTaggerController的POST接口编写了单元测试,但测试返回403 Forbidden,以下是相关代码、日志及Spring Security配置,请帮忙排查问题。

控制器代码

接口通过@RequestPart("FileContent")接收PDF文件:

@RequestMapping(value = "/v1/getFillableFormElements", method = RequestMethod.POST)
public @ResponseBody HttpEntity getFillableFormElements(@RequestPart("FileContent") @Valid 
MultipartFile FileContent, @RequestParam int pageNo) throws IOException {
    FormElementsService formElementsService = new FormElementsServiceImpl();         
    formElementsService.process(pdfFilePath, returnFilePath);
    return setReturnHttpHeaders(returnFilePath);
}

单元测试代码

@WebMvcTest
@ContextConfiguration(classes = PDFTaggerController.class) 
public class PDFTaggerControllerTests {
    @MockBean
    PDFTaggerController controller;
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before()
    public void setup()
    {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }
    
    @Test
    public void getFillableFormElements() throws Exception {
        File file = new File("C:\\Downloads\\sample.pdf");
        FileInputStream fileInputStream = new FileInputStream(file);
        MockMultipartFile firstFile = new MockMultipartFile("file", file.getName(), 
        "multipart/.pdf", fileInputStream);
        
        doNothing().when(retrieveFillableFormElementsService).process(anyString(), anyString());             
        mockMvc.perform(MockMvcRequestBuilders.multipart("/api/v1/getFillableFormElements")
                 .file(firstFile)
                 .param("pageNo", "0"))
                 .andExpect(status().isOk());
    }
}

测试输出日志

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/v1/getFillableFormElements
       Parameters = {pageNo=[0]}
          Headers = [Content-Type:"multipart/form-data;charset=UTF-8"]
             Body = null
    Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@5d08976a}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 403
    Error message = Forbidden
          Headers = [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 type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Spring Security配置代码

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/v1/status");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().
        authorizeRequests().antMatchers("*").permitAll()
        .antMatchers("/pingnot").anonymous()
        .antMatchers("/**").authenticated().and().exceptionHandling().and().httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint).and().sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.authorizeRequests().antMatchers("/swagger-ui.html").authenticated().and()
        .formLogin().permitAll().and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        http.addFilterBefore(new BasicAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
    }
}

问题排查及解决方案

1. 测试用例的核心问题

  • Mock了控制器本身:@MockBean PDFTaggerController controller会替换掉Spring容器中真实的控制器实例,导致请求找不到处理器(日志中Handler: Type = null)。测试控制器时,应该Mock控制器依赖的服务(如FormElementsService),而非控制器本身。
  • 请求参数名不匹配:测试中MockMultipartFile的参数名是"file",但控制器中@RequestPart指定的是"FileContent",参数名不匹配会导致请求无法正确绑定参数。
  • 未正确Mock依赖服务:测试中retrieveFillableFormElementsService未定义,应该Mock控制器中使用的FormElementsService。

修正后的测试用例:

@WebMvcTest(PDFTaggerController.class)
public class PDFTaggerControllerTests {
    @Autowired
    private MockMvc mockMvc;
    
    // Mock控制器依赖的服务,而非控制器本身
    @MockBean
    private FormElementsService formElementsService;

    @Test
    public void getFillableFormElements() throws Exception {
        File file = new File("C:\\Downloads\\sample.pdf");
        FileInputStream fileInputStream = new FileInputStream(file);
        // 修正参数名为"FileContent",与控制器一致
        MockMultipartFile pdfFile = new MockMultipartFile("FileContent", file.getName(), 
                                                          "application/pdf", fileInputStream);
        
        // Stub服务方法
        doNothing().when(formElementsService).process(anyString(), anyString());             
        
        mockMvc.perform(MockMvcRequestBuilders.multipart("/api/v1/getFillableFormElements")
                 .file(pdfFile)
                 .param("pageNo", "0"))
                 .andExpect(status().isOk());
    }
}

2. Spring Security配置的冲突问题

  • 规则顺序与冲突:Security规则是从上到下匹配,先匹配的生效。你先配置了antMatchers("*").permitAll()(匹配一级路径,如/api/v1),随后又配置antMatchers("/**").authenticated()(匹配所有路径),导致/api/v1/getFillableFormElements会匹配到"/**"规则,要求认证,返回403。
  • 重复配置导致的混乱:多次调用http.authorizeRequests(),且同时设置了SessionCreationPolicy.STATELESS(无状态,适合REST)和SessionCreationPolicy.IF_REQUIRED(适合表单登录),配置冲突。
  • 冗余过滤器:手动添加BasicAuthenticationFilter是多余的,httpBasic()已经会自动添加该过滤器。

修正后的Security配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 放行静态资源或无需认证的接口
        web.ignoring().antMatchers("/api/v1/status");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            // 统一配置授权规则,避免重复调用authorizeRequests()
            .authorizeRequests()
                // 放行测试的PDF接口(根据需求调整)
                .antMatchers("/api/v1/getFillableFormElements").permitAll()
                .antMatchers("/pingnot").anonymous()
                .antMatchers("/swagger-ui.html").authenticated()
                // 其他所有路径需要认证
                .anyRequest().authenticated()
            .and()
            .httpBasic()
                .authenticationEntryPoint(authenticationEntryPoint)
            .and()
            // 统一设置会话策略,REST接口建议用STATELESS
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

3. 测试时的认证处理

如果不想修改Security配置放行接口,可以在测试方法上添加@WithMockUser注解模拟认证用户:

@Test
@WithMockUser(username = "test", roles = "USER")
public void getFillableFormElements() throws Exception {
    // 测试代码不变
}

内容的提问来源于stack exchange,提问作者Nitish Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 05:52:03