Java框架快速入门: Spring Security+OAuth2之自动化集成测试

纲要

  • 使用 @SpringBootTest 构建 Web 集成测试的基础环境
  • 通过 MockMvc 发起 HTTP 请求并断言响应
  • 控制测试中是否启用 Spring Security 安全检查
  • 使用 @WithMockUser 模拟已认证用户,简化安全测试
  • 完整可运行的测试类代码示例

集成测试环境搭建

在 Spring Boot 应用中,spring-boot-starter-test 已经内置了对集成测试的支持。默认生成的项目结构中包含一个测试类,上面标注了 @SpringBootTest,它能够加载完整的 Spring 应用上下文,帮助我们执行端到端的集成测试。对于 Web 层,我们主要依赖 MockMvc 来模拟 HTTP 请求并对响应结果进行断言。

首先,保持测试包名与被测代码所在的包结构一致,例如被测 REST 控制器位于 com.example.rest,测试类也应该放在同名包下:

dir 复制代码
src
└── test
    └── java
        └── com
            └── example
                └── rest
                    └── SecuredRestApiIntTest.java

测试类基本骨架如下:

java 复制代码
package com.example.rest;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
public class SecuredRestApiIntTest {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @BeforeEach
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    public void shouldReturnOkWhenAccessApiWithoutSecurity() throws Exception {
        mockMvc.perform(get("/api/hello"))
               .andExpect(status().isOk());
    }
}

MockMvcBuilders.webAppContextSetup(context) 会基于完整的 Spring 应用上下文构建 MockMvc 实例。此时发起的请求会经过除了安全过滤器链之外的大部分 Spring MVC 基础设施(默认情况下不会应用安全配置)。

启用安全过滤链

如果希望集成测试也验证 Spring Security 的规则,可以在构建 MockMvc 时显式应用安全配置:

java 复制代码
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;

@BeforeEach
public void setUp() {
    mockMvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(SecurityMockMvcConfigurers.springSecurity())
            .build();
}

这样 MockMvc 就会加载完整的 Spring Security 过滤器链。此时再执行前面的测试,如果接口 /api/hello 被保护而请求中不携带认证信息,测试将会失败(例如返回 401 或 403)。

模拟已认证用户

为了在受保护的接口上便捷地进行测试,Spring Security Test 提供了 @WithMockUser 注解。它会在测试方法执行时创建一个虚拟的认证用户,绕过真实的登录流程,让我们专注于验证授权规则和业务逻辑。

java 复制代码
import org.springframework.security.test.context.support.WithMockUser;

@Test
@WithMockUser(username = "zhangsan", roles = {"USER"})
public void shouldReturnOkWhenUserHasRoleUser() throws Exception {
    mockMvc.perform(get("/api/hello"))
           .andExpect(status().isOk());
}

注解的可选属性包括:

属性 说明 默认值
username 模拟用户的用户名 "user"
roles 角色列表,会自动添加 ROLE_ 前缀 {"USER"}
password 密码(一般无需校验,可忽略) "password"
authorities 细粒度权限列表,与 roles 二选一

更完整的测试类可能同时包含无需安全、需要安全但匿名、以及需要特定角色的多种场景:

java 复制代码
package com.example.rest;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
public class SecuredRestApiIntTest {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @BeforeEach
    public void setUp() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Test
    public void shouldReturnUnauthorizedWhenNotAuthenticated() throws Exception {
        mockMvc.perform(get("/api/hello"))
               .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "lisi", roles = {"ADMIN"})
    public void shouldReturnForbiddenWhenUserLacksRole() throws Exception {
        mockMvc.perform(get("/api/hello"))
               .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "wangwu", roles = {"USER"})
    public void shouldReturnOkWhenUserHasCorrectRole() throws Exception {
        mockMvc.perform(get("/api/hello"))
               .andExpect(status().isOk());
    }
}

上述代码清晰地展示了三个层次:未认证用户被拒绝,权限不足用户被禁止,拥有正确角色的用户才能正常访问。

测试流程图示

一个典型的集成安全测试执行过程如下:
Controller Spring Security 过滤器 MockMvc 测试类 Controller Spring Security 过滤器 MockMvc 测试类 #mermaid-svg-zHTdgH8ikWBQicO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zHTdgH8ikWBQicO7 .error-icon{fill:#552222;}#mermaid-svg-zHTdgH8ikWBQicO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zHTdgH8ikWBQicO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zHTdgH8ikWBQicO7 .marker.cross{stroke:#333333;}#mermaid-svg-zHTdgH8ikWBQicO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zHTdgH8ikWBQicO7 p{margin:0;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zHTdgH8ikWBQicO7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .sequenceNumber{fill:white;}#mermaid-svg-zHTdgH8ikWBQicO7 #sequencenumber{fill:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageText{fill:#333;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 .labelText,#mermaid-svg-zHTdgH8ikWBQicO7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .loopText,#mermaid-svg-zHTdgH8ikWBQicO7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zHTdgH8ikWBQicO7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zHTdgH8ikWBQicO7 .noteText,#mermaid-svg-zHTdgH8ikWBQicO7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .actorPopupMenu{position:absolute;}#mermaid-svg-zHTdgH8ikWBQicO7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-man circle,#mermaid-svg-zHTdgH8ikWBQicO7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zHTdgH8ikWBQicO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt未认证@WithMockUser提供认证 perform(get("/api/hello"))请求进入安全过滤器链401/403 响应断言状态码加载虚拟 Authentication请求转发到控制器200 响应200 响应断言 status().isOk()

通过 SecurityMockMvcConfigurers.springSecurity()@WithMockUser 的组合,我们可以在不依赖真实用户数据库的情况下,灵活地测试各种安全场景。

关键依赖

确保 pom.xml 中包含以下测试依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

总结

本文基于 Spring Security 和 OAuth2 环境,介绍了使用 @SpringBootTestMockMvc 编写 Web 集成测试的方法。

重点演示了如何在测试中启用安全过滤器,并通过 @WithMockUser 高效模拟认证用户,从而在保证安全测试覆盖的同时,极大降低了环境准备和数据维护的成本。

实战中建议将安全测试和非安全测试分离,以保持用例清晰、执行快速。

相关推荐
Sylvia33.2 小时前
板球实时数据接入实战:从Frames模型到多赛制适配
java·python·websocket·网络协议·架构
志栋智能2 小时前
超自动化安全的变更与配置安全管理
数据库·安全·自动化
Zzzzmo_2 小时前
SpringBoot 日志
java·spring boot·spring
hai_android2 小时前
Android JNI 示例详解:Java 与 C++ 互调演示
android·java
小白说大模型2 小时前
《FDE前沿部署工程师实战教程》企业 Agent 项目实战:从需求分析到 PoC 落地
人工智能·spring·机器学习·自然语言处理·chatgpt·数据挖掘·需求分析
geovindu3 小时前
java: Strategy Pattern
java·开发语言·后端·设计模式·策略模式·行为模式
yimindev3 小时前
从 BI 到 Agent:我如何用 Java 打造一个轻量级数据库语义网关 DatI
java
计算机毕设定制辅导-无忧学长3 小时前
《基于SpringBoot的家政服务管理平台的设计与实现》
java·vue.js·spring boot·计算机毕业设计选题推荐·家政服务管理平台
海宇大数据3 小时前
零信任架构实战:基于海宇单人婚姻状态查询构建自动化线上房产交易合规网关
运维·人工智能·架构·自动化