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

@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测试上下文,让注解生效:

  1. 确保添加spring-security-test依赖
  2. 修改测试类:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 10:20:53