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

Spring Security OAuth2中Mock JwtDecoder解决CurrentUserId空指针问题

问题:MockMvc测试中@CurrentUserId注解抛出空指针异常

我定义了用于多控制器的自定义注解@CurrentUserId:

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "@applicationUserDetailsService.loadUserByUsername(#this.getSubject()).getId()")
public @interface CurrentUserId {

}

同时实现了JwtEncoder和JwtDecoder的Bean:

@Bean
public JwtEncoder jwtEncoder() {
    final JWK jwk = new RSAKey.Builder(keys.getPublicKey()).privateKey(keys.getPrivateKey()).build();
    final JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(keys.getPublicKey()).build();
}

编写了自定义静态方法发起MockMvc请求:

public static ResultActions makeMockMvcRequest(final MockMvc mockMvc,
                                               final HttpMethod requestType,
                                               final String endpoint,
                                               final Map<String, Object> params,
                                               final Object body,
                                               final User user) throws Exception {
    final var requestBuilder = defineRequestBuilder(requestType, endpoint);
    addBodyIfExists(body, requestBuilder);
    addUserIfExists(user, requestBuilder);
    return mockMvc.perform(requestBuilder
            .contentType(MediaType.APPLICATION_JSON)
            .params(convertToMultiValueMap(params)));
}

private static MockHttpServletRequestBuilder defineRequestBuilder(final HttpMethod requestType, final String endpoint) {
    MockHttpServletRequestBuilder requestBuilder;
    if (POST.equals(requestType)) {
        requestBuilder = post(endpoint);
    } else if (GET.equals(requestType)) {
        requestBuilder = get(endpoint);
    } else if (PUT.equals(requestType)) {
        requestBuilder = put(endpoint);
    } else {
        throw new IllegalArgumentException("Unsupported HTTP request type: " + requestType);
    }
    return requestBuilder;
}

private static void addBodyIfExists(final Object body, final MockHttpServletRequestBuilder requestBuilder) {
    if (body != null) {
        requestBuilder.content(asJsonString(body));
    }
}

private static void addUserIfExists(final User user, final MockHttpServletRequestBuilder requestBuilder) {
    if (user != null) {
        final String[] roles = user.getRoles().stream()
                .map(Role::name)
                .distinct()
                .toArray(String[]::new);

        final List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> (GrantedAuthority) role::name)
                .toList();

        requestBuilder
                .with(user(user.getEmail()).roles(roles))
                .with(jwt().jwt(j -> j.subject(user.getEmail()).tokenValue("strong-token").build()).authorities(authorities));
    }
}

发起请求时出现错误:

jakarta.servlet.ServletException: Request processing failed: org.springframework.expression.spel.SpelEvaluationException: EL1011E: Method call: Attempted to call method getId() on null context object

请问如何Mock JwtDecoder以正确解析指定用户ID?或有更便捷的测试实现方式?


解决方案

方案1:Mock UserDetailsService,确保返回非null用户对象

错误根源是@applicationUserDetailsService.loadUserByUsername(#this.getSubject())返回了null,导致调用getId()时抛出异常。在测试类中Mock ApplicationUserDetailsService,让它针对测试用户的邮箱返回对应的UserDetails实例即可:

@MockBean
private ApplicationUserDetailsService applicationUserDetailsService;

@BeforeEach
void setUp() {
    // 初始化测试用户
    User testUser = new User(1L, "test@example.com", "password", List.of(Role.USER));
    // 假设你的UserDetails实现类是CustomUserDetails,包含getId()方法
    CustomUserDetails userDetails = new CustomUserDetails(testUser);
    
    when(applicationUserDetailsService.loadUserByUsername(testUser.getEmail()))
        .thenReturn(userDetails);
}

这样SpEL表达式就能拿到有效的UserDetails对象,调用getId()不会再抛出空指针。

方案2:直接Mock JwtDecoder,让JWT携带用户ID

如果不想依赖UserDetailsService的逻辑,可以修改@CurrentUserId的表达式,直接从JWT的claim中获取用户ID,同时Mock JwtDecoder返回预设的JWT对象:

第一步:修改@CurrentUserId注解

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "claims.get('userId')")
public @interface CurrentUserId {
}

第二步:Mock JwtDecoder返回包含用户ID的JWT

@MockBean
private JwtDecoder jwtDecoder;

@BeforeEach
void setUp() {
    User testUser = new User(1L, "test@example.com", "password", List.of(Role.USER));
    
    Jwt mockJwt = Jwt.withTokenValue("strong-token")
            .header("alg", "RS256")
            .claim("sub", testUser.getEmail())
            .claim("userId", testUser.getId()) // 将用户ID存入JWT的claim
            .build();
    
    when(jwtDecoder.decode(anyString())).thenReturn(mockJwt);
}

这种方式跳过了UserDetailsService的调用,更适合单元测试场景,测试逻辑更独立。

方案3:使用自定义测试注解简化代码

利用Spring Security Test的@WithUserDetails自定义注解,减少重复的Mock逻辑:

第一步:定义自定义测试注解

@Retention(RetentionPolicy.RUNTIME)
@WithUserDetails(value = "test@example.com", userDetailsServiceBeanName = "applicationUserDetailsService")
public @interface WithMockCustomUser {
    // 可扩展属性,比如指定用户ID
    long userId() default 1L;
}

第二步:在测试方法中使用注解

@SpringBootTest
@AutoConfigureMockMvc
class ControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private ApplicationUserDetailsService applicationUserDetailsService;

    @BeforeEach
    void setUp() {
        User testUser = new User(1L, "test@example.com", "password", List.of(Role.USER));
        CustomUserDetails userDetails = new CustomUserDetails(testUser);
        when(applicationUserDetailsService.loadUserByUsername("test@example.com"))
            .thenReturn(userDetails);
    }

    @Test
    @WithMockCustomUser(userId = 1L)
    void testControllerEndpoint() throws Exception {
        mockMvc.perform(get("/api/user/me")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.userId").value(1L));
    }
}

这种方式让测试代码更简洁,复用性更强。


内容的提问来源于stack exchange,提问作者yaroslav96

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 15:01:01