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
相关产品推荐
相关产品推荐

