0

0

Spring Boot 微服务控制器测试:处理跨服务调用与JWT认证

碧海醫心

碧海醫心

发布时间:2025-11-26 15:33:01

|

295人浏览过

|

来源于php中文网

原创

spring boot 微服务控制器测试:处理跨服务调用与jwt认证

在微服务架构中,一个服务的控制器往往需要依赖其他服务提供的功能,例如身份认证、数据查询等。当我们在一个服务的测试上下文中,尝试通过mockMvc模拟请求来获取另一个服务的资源(如JWT令牌)时,常常会遇到“404 Not Found”错误。这通常是因为mockMvc实例仅限于测试当前应用上下文中的控制器,无法感知或路由到外部服务的端点。此外,JWT令牌的有效性(如过期时间)在测试中也可能成为一个常见问题

本教程将详细介绍如何在Spring Boot微服务环境中,针对依赖外部服务和JWT认证的控制器进行有效测试的策略和实践。

理解问题根源:mockMvc的范围限制

在Spring Boot的测试中,MockMvc是用于测试控制器层的核心工具。它模拟HTTP请求,并在不启动完整HTTP服务器的情况下,直接调用控制器方法。然而,mockMvc的范围仅限于当前测试上下文加载的Spring应用。这意味着,如果你在“订单服务”的测试中,尝试通过mockMvc.perform(MockMvcRequestBuilders.post("/authenticate/login"))来调用“认证服务”的登录接口,这个请求将不会被路由到认证服务的控制器,因为认证服务不在订单服务的mockMvc上下文中,从而导致404错误。

此外,即使能够以某种方式模拟外部调用,JWT令牌本身的有效性(如签名、过期时间)也需要在测试中得到妥善处理,以避免因令牌过期或无效而导致的认证失败。

解决方案:有效模拟外部依赖与JWT处理

针对上述问题,我们可以采取以下几种策略来确保测试的隔离性和准确性:

1. 模拟外部服务调用

如果你的控制器或其依赖的服务通过RestTemplate或WebClient等HTTP客户端调用外部服务,最直接的方法是在测试中模拟这些HTTP客户端的行为。这样,当你的代码尝试进行外部调用时,它会得到一个预设的响应,而不会真正发起网络请求。

示例:模拟RestTemplate

假设你的OrderService内部有一个组件会调用AuthService来获取JWT。

// AuthServiceClient.java (在OrderService中)
@Component
public class AuthServiceClient {

    @Autowired
    private RestTemplate restTemplate; // 或者 WebClient

    @Value("${auth.service.url}")
    private String authServiceUrl;

    public JWTResponse login(LoginRequest loginRequest) {
        // 这里的URL可以是 "http://AUTH-SERVICE/authenticate/login"
        // 在实际运行时通过Eureka或其他服务发现机制解析
        // 在测试中,我们可以模拟这个restTemplate的行为
        return restTemplate.postForObject(authServiceUrl + "/authenticate/login", loginRequest, JWTResponse.class);
    }
}

在测试中,你可以创建一个AuthServiceClient的Mock对象,并定义其行为:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; // 推荐使用 @MockBean
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// 假设 OrderController 依赖 AuthServiceClient
@WebMvcTest(OrderController.class) // 只加载 OrderController 及其依赖
public class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // 使用 @MockBean 自动将 AuthServiceClient 替换为 Mockito mock
    @MockBean
    private AuthServiceClient authServiceClient;

    // 其他必要的 mock bean,例如 JWTUtils,Repository 等
    @MockBean
    private JwtUtils jwtUtils;
    // ... 其他 @MockBean

    @BeforeEach
    void setUp() {
        // 模拟 AuthServiceClient 的 login 方法
        JWTResponse mockJwtResponse = new JWTResponse("mocked_jwt_token_for_user", "User", List.of("ROLE_USER"));
        when(authServiceClient.login(any(LoginRequest.class)))
            .thenReturn(mockJwtResponse);

        // 模拟 jwtUtils 的 getUserNameFromJwtToken 方法
        when(jwtUtils.getUserNameFromJwtToken(eq("mocked_jwt_token_for_user")))
            .thenReturn("User");

        // 如果 OrderController 需要其他依赖,也需要进行模拟
        // 例如:when(orderRepository.findById(anyLong())).thenReturn(Optional.of(new Order()));
    }

    @Test
    @DisplayName("Place Order -- Success Scenario with Mocked Auth")
    void test_When_placeOrder_DoPayment_Success() throws Exception {
        OrderRequest orderRequest = getMockOrderRequest();
        String validJwt = "mocked_jwt_token_for_user"; // 使用模拟的JWT

        mockMvc.perform(post("/order/placeorder")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .header("Authorization", "Bearer " + validJwt)
                        .content(asJsonString(orderRequest)))
                .andExpect(status().isOk());

        // 进一步验证业务逻辑,例如调用了 orderRepository.save() 等
    }

    // 辅助方法,用于将对象转换为JSON字符串
    private String asJsonString(final Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 辅助方法,用于获取模拟的订单请求
    private OrderRequest getMockOrderRequest() {
        // ... 返回一个 OrderRequest 实例
        return new OrderRequest();
    }
}

通过@MockBean,Spring Test会自动在测试上下文中用Mockito的Mock对象替换AuthServiceClient的实际实例。这样,当OrderController在处理请求时需要调用AuthServiceClient.login()时,它会得到我们预设的mockJwtResponse,而不会尝试进行实际的网络调用。

2. 直接提供有效的JWT令牌

在许多测试场景中,我们可能并不关心JWT的生成过程,而只关心控制器是否能正确处理一个“有效”的JWT。在这种情况下,我们可以直接在测试中构造一个有效的JWT字符串,并将其用于请求头。

BlackBox AI
BlackBox AI

AI编程助手,智能对话问答助手

下载

要构造一个有效的JWT,你需要知道:

  • JWT密钥 (Secret Key): 用于签名的密钥,通常与认证服务使用的密钥相同。
  • 声明 (Claims): 包括主题(sub,通常是用户名)、角色(roles)、过期时间(exp)等。
  • 算法 (Algorithm): 签名算法,如HS512。

示例:使用jjwt库生成测试JWT

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class TestJwtGenerator {

    // 确保这里的密钥与你的认证服务用于签名的密钥一致
    // 或者在测试环境中配置一个专门的测试密钥
    private static final String TEST_JWT_SECRET = "yourTestSecretKeyThatIsAtLeast256BitsLongForHS512"; // 至少32字节
    private static final byte[] keyBytes = TEST_JWT_SECRET.getBytes();

    public static String generateValidJwtToken(String username, String role) {
        Map claims = new HashMap<>();
        claims.put("roles", role); // 根据你的认证服务实际使用的声明名称调整

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                // 设置一个未来的过期时间,例如24小时
                .setExpiration(new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24)))
                .signWith(Keys.hmacShaKeyFor(keyBytes), SignatureAlgorithm.HS512) // 使用HS512算法
                .compact();
    }

    // 在 OrderControllerTest 中使用
    // ...
    @Test
    @DisplayName("Place Order -- Success Scenario with Generated JWT")
    @WithMockUser(username = "User", authorities = { "ROLE_USER" }) // 也可以结合 @WithMockUser
    void test_When_placeOrder_DoPayment_Success_WithGeneratedJwt() throws Exception {
        OrderRequest orderRequest = getMockOrderRequest();
        // 直接生成一个有效的JWT令牌
        String validJwt = TestJwtGenerator.generateValidJwtToken("User", "ROLE_USER");

        mockMvc.perform(post("/order/placeorder")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .header("Authorization", "Bearer " + validJwt)
                        .content(asJsonString(orderRequest)))
                .andExpect(status().isOk());
    }
    // ...
}

注意事项:

  • 密钥一致性: TEST_JWT_SECRET必须与认证服务用于签名JWT的密钥相同,否则验证会失败。在实际项目中,可以将这个密钥配置为测试专用的环境变量application-test.properties。
  • 声明匹配: 确保生成的JWT中的claims(如roles、authorities)与你的JwtUtils或认证过滤器期望的声明结构一致。

3. 绕过JWT验证(针对特定测试场景)

如果你的测试重点完全在于业务逻辑,而JWT验证本身不是本次测试的目标,你可以考虑在测试环境中绕过JWT验证机制。这通常通过在测试配置中提供一个特殊的JwtUtils实现来完成。

示例:测试专用的JwtUtils

// JwtUtils.java (在OrderService中)
@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String jwtSecret;

    public String getUserNameFromJwtToken(String token) {
        // 实际的JWT解析逻辑
        if(token.startsWith("Bearer")){
            token = token.substring(7);
        }
        return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
    }
    // ... 其他JWT相关方法
}

// TestJwtUtils.java (在测试包中,例如 src/test/java)
@TestConfiguration // 标记为测试配置
@Primary // 确保在测试时优先使用这个Bean
public class TestJwtUtils extends JwtUtils {

    @Override
    public String getUserNameFromJwtToken(String token) {
        // 在测试中,无论传入什么token,都返回一个预设的用户名
        // 或者根据token内容进行简单的模拟解析
        if ("mocked_jwt_token_for_user".equals(token.replace("Bearer ", ""))) {
            return "User";
        }
        // 对于其他token,可以抛出异常或返回null,取决于测试需求
        return "testUser"; // 默认返回一个测试用户
    }

    // 可以根据需要重写其他方法,例如验证token是否有效
    @Override
    public boolean validateJwtToken(String authToken) {
        return true; // 在测试中始终认为token是有效的
    }
}

在OrderControllerTest中,确保加载了TestJwtUtils这个测试配置:

import org.springframework.context.annotation.Import;
// ...

@WebMvcTest(OrderController.class)
@Import(TestJwtUtils.class) // 导入测试配置
public class OrderControllerTest {
    // ... 正常编写你的测试,JwtUtils 会被 TestJwtUtils 替换
    // 你可以直接在 Authorization 头中传入任何字符串,因为它会被 TestJwtUtils 视为有效
    @Test
    @DisplayName("Place Order -- Success Scenario with Bypassed Auth")
    void test_When_placeOrder_DoPayment_Success_WithBypassedAuth() throws Exception {
        OrderRequest orderRequest = getMockOrderRequest();
        String anyToken = "Bearer dummy_token"; // 任何字符串都可以

        mockMvc.perform(post("/order/placeorder")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .header("Authorization", anyToken)
                        .content(asJsonString(orderRequest)))
                .andExpect(status().isOk());
    }
}

@WithMockUser的替代方案

对于简单的授权场景,Spring Security提供了@WithMockUser注解,可以直接模拟一个已认证的用户。这通常比手动生成JWT或绕过验证更简洁,适用于你不需要测试JWT本身的解析和验证逻辑,而只关注基于角色的访问控制的场景。

import org.springframework.security.test.context.support.WithMockUser;
// ...

@Test
@DisplayName("Place Order -- Success Scenario with WithMockUser")
@WithMockUser(username = "User", authorities = { "ROLE_USER" }) // 模拟一个名为"User"、拥有"ROLE_USER"角色的用户
void test_When_placeOrder_DoPayment_Success_WithMockUser() throws Exception {
    OrderRequest orderRequest = getMockOrderRequest();
    // 此时不需要在header中添加Authorization,@WithMockUser会处理认证上下文
    mockMvc.perform(post("/order/placeorder")
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .content(asJsonString(orderRequest)))
            .andExpect(status().isOk());
}

总结与最佳实践

在Spring Boot微服务环境中测试控制器,特别是涉及跨服务调用和JWT认证时,选择合适的测试策略至关重要:

  • 隔离性: 始终努力使单元/集成测试尽可能隔离。避免在测试中启动整个微服务集群或进行实际的网络调用。
  • 选择合适的模拟级别:
    • 模拟外部HTTP客户端 (RestTemplate/WebClient): 当你的服务需要调用其他服务来获取数据或执行操作时,这是最常见的做法。它允许你测试服务间的集成点,而无需启动实际的外部服务。
    • 直接提供有效JWT: 如果你关注的是控制器对JWT的正确处理(例如解析用户ID、角色),但不想模拟整个认证服务,可以直接生成一个有效的JWT。
    • 绕过JWT验证或使用@WithMockUser: 当你的测试重点完全在于业务逻辑,而JWT验证本身不是测试目标时,这些方法可以极大地简化测试代码。但请注意,这种方式无法测试JWT验证本身的正确性。
  • 测试数据管理: 对于JWT,确保你使用的密钥、声明和过期时间在测试中是可控且有效的。避免使用过期的静态JWT。
  • 清晰的测试目标: 在编写测试之前,明确你的测试目的是什么。是为了验证JWT的解析和授权逻辑?还是仅仅验证控制器在收到一个“已认证”请求后的业务行为?不同的目标会导向不同的测试策略。

通过采纳这些策略,你可以有效地测试Spring Boot微服务控制器,确保代码质量和系统稳定性,同时保持测试的快速和可靠。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

102

2025.08.06

spring boot框架优点
spring boot框架优点

spring boot框架的优点有简化配置、快速开发、内嵌服务器、微服务支持、自动化测试和生态系统支持。本专题为大家提供spring boot相关的文章、下载、课程内容,供大家免费下载体验。

135

2023.09.05

spring框架有哪些
spring框架有哪些

spring框架有Spring Core、Spring MVC、Spring Data、Spring Security、Spring AOP和Spring Boot。详细介绍:1、Spring Core,通过将对象的创建和依赖关系的管理交给容器来实现,从而降低了组件之间的耦合度;2、Spring MVC,提供基于模型-视图-控制器的架构,用于开发灵活和可扩展的Web应用程序等。

389

2023.10.12

Java Spring Boot开发
Java Spring Boot开发

本专题围绕 Java 主流开发框架 Spring Boot 展开,系统讲解依赖注入、配置管理、数据访问、RESTful API、微服务架构与安全认证等核心知识,并通过电商平台、博客系统与企业管理系统等项目实战,帮助学员掌握使用 Spring Boot 快速开发高效、稳定的企业级应用。

68

2025.08.19

Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性
Java Spring Boot 4更新教程_Java Spring Boot 4有哪些新特性

Spring Boot 是一个基于 Spring 框架的 Java 开发框架,它通过 约定优于配置的原则,大幅简化了 Spring 应用的初始搭建、配置和开发过程,让开发者可以快速构建独立的、生产级别的 Spring 应用,无需繁琐的样板配置,通常集成嵌入式服务器(如 Tomcat),提供“开箱即用”的体验,是构建微服务和 Web 应用的流行工具。

33

2025.12.22

Java Spring Boot 微服务实战
Java Spring Boot 微服务实战

本专题深入讲解 Java Spring Boot 在微服务架构中的应用,内容涵盖服务注册与发现、REST API开发、配置中心、负载均衡、熔断与限流、日志与监控。通过实际项目案例(如电商订单系统),帮助开发者掌握 从单体应用迁移到高可用微服务系统的完整流程与实战能力。

114

2025.12.24

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

256

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

208

2023.09.04

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

8

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.5万人学习

C# 教程
C# 教程

共94课时 | 6.8万人学习

Java 教程
Java 教程

共578课时 | 46.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号