@WithMockUser失效仍返回401,Spring安全端点测试求助
测试Spring安全端点的问题及解决方案
我正在测试Spring应用中一个为指定用户创建食谱的安全端点,想知道测试这类端点的正确方式。用@WithMockUser注解为什么不行?我还是收到401错误。
初始测试代码(使用@WithMockUser)
package com.joaogoncalves.recipes.controller; import com.joaogoncalves.recipes.model.RecipeCreate; import com.joaogoncalves.testcontainers.EnableTestContainers; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import java.util.List; import static io.restassured.RestAssured.given; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @EnableTestContainers @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class RecipeControllerIT { @LocalServerPort private Integer port; @BeforeEach void setUp() { RestAssured.baseURI = "http://localhost:" + port; } @Test @Order(1) @WithMockUser public void testRecipeCreateOk() { final RecipeCreate recipeCreate = new RecipeCreate( "tomato soup", "tomato soup with anchovies", List.of("tomato", "water", "anchovies"), List.of("peel the tomatoes", "add water", "boil for 30 minutes", "add the anchovies"), "soup" ); given() .contentType(ContentType.JSON) .body(recipeCreate) .when() .post("/api/recipe/new") .then() .statusCode(HttpStatus.OK.value()); } }
Security配置
我的SecurityFilterChain要求除/api/register外的所有端点都需要拥有USER角色的认证:
package com.joaogoncalves.recipes.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/register").permitAll() .antMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().hasRole("USER") .and() .httpBasic(Customizer.withDefaults()) .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable()); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
尝试@WithUserDetails的测试代码
我还尝试了@WithUserDetails的方式,但同样没有成功:
package com.joaogoncalves.recipes.controller; import com.joaogoncalves.recipes.model.RecipeCreate; import com.joaogoncalves.recipes.model.UserCreate; import com.joaogoncalves.recipes.service.UserService; import com.joaogoncalves.testcontainers.EnableTestContainers; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithUserDetails; import java.util.List; import static io.restassured.RestAssured.given; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @EnableTestContainers @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class RecipeControllerIT { @LocalServerPort private Integer port; @Autowired private UserService userService; @BeforeEach void setUp() { RestAssured.baseURI = "http://localhost:" + port; userService.create(new UserCreate("chef@chef.com", "chefpassword")); } @Test @Order(1) @WithUserDetails(value = "chef@chef.com", userDetailsServiceBeanName = "userService") public void testRecipeCreateOk() { final RecipeCreate recipeCreate = new RecipeCreate( "tomato soup", "tomato soup with anchovies", List.of("tomato", "water", "anchovies"), List.of("peel the tomatoes", "add water", "boil for 30 minutes", "add the anchovies"), "soup" ); given() .contentType(ContentType.JSON) .body(recipeCreate) .when() .post("/api/recipe/new") .then() .statusCode(HttpStatus.OK.value()); } }
问题原因
@WithMockUser和@WithUserDetails是Spring Security针对Spring测试上下文设计的注解,仅在使用MockMvc测试时生效。而你用的RestAssured是直接向真实服务器端口发送HTTP请求,完全独立于Spring的测试上下文,所以这些注解无法为RestAssured的请求添加认证信息,导致返回401。
正确的测试方式
方式一:RestAssured直接添加HTTP Basic认证
因为你的Security配置启用了HTTP Basic认证,直接在请求中携带用户名和密码即可:
@Test @Order(1) public void testRecipeCreateOk() { final RecipeCreate recipeCreate = new RecipeCreate( "tomato soup", "tomato soup with anchovies", List.of("tomato", "water", "anchovies"), List.of("peel the tomatoes", "add water", "boil for 30 minutes", "add the anchovies"), "soup" ); given() .contentType(ContentType.JSON) .body(recipeCreate) // 添加HTTP Basic认证信息 .auth().basic("chef@chef.com", "chefpassword") .when() .post("/api/recipe/new") .then() .statusCode(HttpStatus.OK.value()); }
注意:需确保setUp方法中已创建该测试用户。
方式二:改用MockMvc测试(支持Spring Security注解)
如果想使用@WithMockUser或@WithUserDetails,可以切换到Spring官方的MockMvc测试:
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; // 注入MockMvc和ObjectMapper @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test @WithMockUser(roles = "USER") public void testRecipeCreateOk() throws Exception { final RecipeCreate recipeCreate = new RecipeCreate( "tomato soup", "tomato soup with anchovies", List.of("tomato", "water", "anchovies"), List.of("peel the tomatoes", "add water", "boil for 30 minutes", "add the anchovies"), "soup" ); mockMvc.perform(post("/api/recipe/new") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(recipeCreate))) .andExpect(status().isOk()); }
方式三:RestAssured集成Spring Security上下文
如果坚持用RestAssured,可通过RestAssuredMockMvc集成Spring测试上下文,让注解生效:
- 确保添加
spring-security-test依赖 - 修改测试类:
import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.mockMvc.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc @EnableTestContainers @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class RecipeControllerIT { @Autowired private MockMvc mockMvc; @BeforeEach void setUp() { RestAssuredMockMvc.mockMvc(mockMvc); } @Test @Order(1) @WithMockUser(roles = "USER") public void testRecipeCreateOk() { final RecipeCreate recipeCreate = new RecipeCreate( "tomato soup", "tomato soup with anchovies", List.of("tomato", "water", "anchovies"), List.of("peel the tomatoes", "add water", "boil for 30 minutes", "add the anchovies"), "soup" ); given() .contentType(ContentType.JSON) .body(recipeCreate) .when() .post("/api/recipe/new") .then() .statusCode(HttpStatus.OK.value()); } }
内容的提问来源于stack exchange,提问作者joao-prg
相关产品推荐
相关产品推荐

