Spring WebMvcTest使用post方法返回403错误

12
我想知道我的代码出了什么问题,在我运行一个post测试时(不管是针对哪个控制器或方法),我都会返回403错误,但有些情况下我期望返回401错误,而在其他情况下则需要进行身份认证才能获得200的响应。
以下是我的控制器代码片段:
@RestController
@CrossOrigin("*")
@RequestMapping("/user")
class UserController @Autowired constructor(val userRepository: UserRepository) {
    @PostMapping("/create")
    fun addUser(@RequestBody user: User): ResponseEntity<User> {
        return ResponseEntity.ok(userRepository.save(user))
    }
}

我的单元测试针对这个控制器

@RunWith(SpringRunner::class)
@WebMvcTest(UserController::class)
class UserControllerTests {
    @Autowired
    val mvc: MockMvc? = null

    @MockBean
    val repository: UserRepository? = null

    val userCollection = mutableListOf<BioRiskUser>()

    @Test
    fun testAddUserNoAuth() {
        val user = BioRiskUser(
                0L,
                "user",
                "password",
                mutableListOf(Role(
                    0L,
                    "administrator"
                )))
        repository!!
        `when`(repository.save(user)).thenReturn(createUser(user))
        mvc!!
        mvc.perform(post("/create"))
                .andExpect(status().isUnauthorized)
    }

    private fun createUser(user: BioRiskUser): BioRiskUser? {
        user.id=userCollection.count().toLong()
        userCollection.add(user)
        return user
    }
}

我缺少什么?

按要求,以下是我的安全配置...

@Configuration
@EnableWebSecurity
class SecurityConfig(private val userRepository: UserRepository, private val userDetailsService: UserDetailsService) : WebSecurityConfigurerAdapter() {
    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    override fun configure(auth: AuthenticationManagerBuilder) {
        auth.authenticationProvider(authProvider())
    }

    override fun configure(http: HttpSecurity) {
        http
            .csrf().disable()
            .cors()
            .and()
            .httpBasic()
            .realmName("App Realm")
            .and()
            .authorizeRequests()
            .antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
            .anonymous()
            .anyRequest().authenticated()
            .and()
            .logout()
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .logoutSuccessUrl("/user")
            .permitAll()
    }

    @Bean
    fun authProvider(): DaoAuthenticationProvider {
        val authProvider = CustomAuthProvider(userRepository)
        authProvider.setUserDetailsService(userDetailsService)
        authProvider.setPasswordEncoder(encoder())
        return authProvider
    }
}

并且身份验证提供者

class CustomAuthProvider constructor(val userRepository: UserRepository) : DaoAuthenticationProvider() {
    override fun authenticate(authentication: Authentication?): Authentication {
        authentication!!
        val user = userRepository.findByUsername(authentication.name)
        if (!user.isPresent) {
            throw BadCredentialsException("Invalid username or password")
        }
        val result = super.authenticate(authentication)
        return UsernamePasswordAuthenticationToken(user, result.credentials, result.authorities)
    }


    override fun supports(authentication: Class<*>?): Boolean {
        return authentication?.equals(UsernamePasswordAuthenticationToken::class.java) ?: false
    }
}

请更新您的问题,加入处理身份验证和授权的代码。 - Branislav Lazic
你能从Postman调用这个端点吗? - Chirdeep Tomar
从Postman中测试正常(我第一时间就检查了)- 起初我没有进行单元测试,但我已经开始使用rest文档了(更多是为了养成测试所有东西的习惯,而不是懒惰)。 - Dave Roberts
4个回答

19

在我的情况下,即使在您的配置中禁用了csrf保护,它似乎仍然在我的WebMvcTest中处于活动状态。

因此,为了解决这个问题,我简单地将我的WebMvcTest更改为:

    @Test
    public void testFoo() throws Exception {

        MvcResult result = mvc.perform(
                    post("/foo").with(csrf()))
                .andExpect(status().isOk())
                .andReturn();

        // ...
    }

所以在我的情况下,缺少的 .with(csrf()) 才是问题所在。


这是唯一对我有效的修复方法。否则,当我进行调试时,我会看到它在我的配置中跨越HttpSecurity.csrf().disable(),但然后我会看到它在CrsfFilter中失败。 - whistling_marmot
1
csrf() 方法来自于这个导入:static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; - David Bradley

9

@WebMvcTest(UserController::class)注释之后,您需要将@ContextConfiguration(classes=SecurityConfig.class)添加到您的UserControllerTests类顶部。


1
这个问题有点愚蠢,这不是废除了单元测试的意义吗?我开始把它变成一个集成测试了(除非我的理解有误,我很乐意接受指正)。 - Dave Roberts
2
使用MockMvc和方法mvc.perform()时,实际上你并没有编写单元测试。你是在直接进行API调用,因此需要所有的配置。如果你要为控制器编写单元测试,你应该对更深层次进行存根,并直接调用控制器的方法,例如在你的情况下是addUser()方法,而不是使用MockMvc - Kushagra Goyal

4
您的问题来自CSRF,如果启用调试日志,问题将变得明显,并且问题源于@WebMvcTest仅加载Web层而不是整个上下文,因此KeycloakWebSecurityConfigurerAdapter未加载。
加载的配置来自org.springframework.boot.autoconfigure.security.servlet.DefaultConfigurerAdapter(等同于org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter)。 WebSecurityConfigurerAdapter包含crsf()
截至今天,您有3个选项来解决此问题:
选项1
在测试类中创建一个WebSecurityConfigurerAdapter
如果您的项目中只有少量@WebMvcTest注释的类,则此解决方案适合您。
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {

    @TestConfiguration
    static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            super.configure(http);
            http.csrf().disable();
        }
    }
    ...
}

选项2

创建一个WebSecurityConfigurerAdapter超类,并使您的测试类继承它。

如果您的项目中有多个带有@WebMvcTest注释的类,则此解决方案适合您。

@Import(WebMvcTestWithoutCsrf.DefaultConfigWithoutCsrf.class)
public interface WebMvcCsrfDisabler {

    static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            super.configure(http);
            http.csrf().disable();
        }
    }
}

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyControllerTest .class})
public class MyControllerTest implements WebMvcCsrfDisabler {
    ...
}

选项3

使用spring-security csrf SecurityMockMvcRequestPostProcessors

这种解决方案很笨重,容易出错,检查权限拒绝和忘记使用(csrf())将导致测试结果误报。

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
    ...
    @Test
    public void myTest() {
        mvc.perform(post("/path")
                .with(csrf()) // <=== THIS IS THE PART THAT FIX CSRF ISSUE
                .content(...)
                
        )
                .andExpect(...);
    }
}

你提出的所有选项都有一个很大的缺点:它们在与生产设置不同的设置下执行测试。我希望你提出从https://stackoverflow.com/a/52211616/2365727复制的第四个选项。 - michaldo

0

这里有一个问题:

override fun configure(http: HttpSecurity) {
    http
        .csrf().disable()
        .cors()
        .and()
        .httpBasic()
        .realmName("App Realm")
        .and()
        .authorizeRequests()
        .antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
        .anonymous()
        .anyRequest().authenticated()
        .and()
        .logout()
        .invalidateHttpSession(true)
        .clearAuthentication(true)
        .logoutSuccessUrl("/user")
        .permitAll()
}

更具体地说,在这里:

.anyRequest().authenticated()

您需要对每个请求进行身份验证,因此会收到403错误。

这篇教程很好地解释了如何使用模拟用户进行测试。

简单的方法是像这样:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SecuredControllerRestTemplateIntegrationTest {

    @Autowired
    private val template: TestRestTemplate

    @Test
    fun createUser(): Unit {
        val result = template.withBasicAuth("username", "password")
          .postForObject("/user/create", HttpEntity(User(...)), User.class)
        assertEquals(HttpStatus.OK, result.getStatusCode())
    }
}

但是我不想启动一个“完整”的Servlet容器,如果我正确使用测试,我就不需要它(?)或者说Spring Boot WebMvcTest有缺陷吗?(虽然我也可以接受“新锤子”问题) - Dave Roberts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接