如何在SpringBootTest中集成SpringSecurity并测试评论提交接口
问题解决:Spring Security测试中配置SecurityContext实现评论关联用户
核心问题分析
- 自定义
@WithMockCustomUser注解与对应的SecurityContext工厂类不匹配,导致依赖注入失败 UserDetailService.returnUser()中通过字符串截取邮箱的逻辑过于脆弱,且容易出错- 测试类中内部工厂类依赖外部测试类(非Spring Bean),无法被正确注入
解决方案步骤
1. 优化UserDetailService的用户获取逻辑
去掉字符串截取,通过强转Principal直接获取邮箱(建议让User实体实现UserDetails接口,简化逻辑):
@RequiredArgsConstructor @Service public class UserDetailService { private final UserRepository userRepository; public User returnUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { throw new IllegalStateException("当前用户未认证"); } Object principal = authentication.getPrincipal(); String email; // 若User实现了UserDetails,直接强转获取邮箱 if (principal instanceof User) { email = ((User) principal).getEmail(); } // 兼容Spring默认UserDetails实现 else if (principal instanceof UserDetails) { email = ((UserDetails) principal).getUsername(); } else { email = principal.toString(); } // 找不到用户直接抛异常,避免返回Null return userRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("不存在邮箱为" + email + "的用户")); } }
2. 修复自定义Security注解与工厂类
将工厂类改为静态内部类(避免依赖外部测试类),并与自定义注解泛型匹配:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional public class CommentsApiControllerTest { // ... 其他注入、setup/teardown代码保持不变 // 自定义测试用户注解 @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) public @interface WithMockCustomUser { String name() default "testName"; String email() default "testemail@gmail.com"; Role role() default Role.USER; } // 静态内部类:避免依赖外部测试类,Spring可直接注入依赖 static class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { private final UserRepository userRepository; @Autowired public WithMockCustomUserSecurityContextFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public SecurityContext createSecurityContext(WithMockCustomUser annotation) { // 提前将测试用户存入数据库,确保returnUser能查询到 User testUser = User.builder() .name(annotation.name()) .email(annotation.email()) .picture("testPic.com") .role(annotation.role()) .build(); userRepository.save(testUser); // 构建Authentication:若User实现了UserDetails,直接传入testUser UserDetails userDetails = testUser; // 若未实现UserDetails,可使用Spring默认构造器 // UserDetails userDetails = User.withUsername(testUser.getEmail()) // .password("dummy-password") // .roles(testUser.getRole().name()) // .build(); Authentication authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); return context; } } @Test @WithMockCustomUser(email = "testemail@gmail.com") public void comment_등록() throws Exception { // given String title = "title"; String content = "content"; // 从数据库获取工厂类预先创建的用户 User testUser = userRepository.findByEmail("testemail@gmail.com").get(); PostsSaveRequestDto postRequest = PostsSaveRequestDto.builder() .title(title) .content(content) .user(testUser) .build(); postsRepository.save(postRequest.toEntity()); String commentContent = "comment"; Posts posts = postsRepository.findAll().get(0); CommentSaveRequestDto commentRequest = CommentSaveRequestDto.builder() .comment(commentContent) .posts(posts) .build(); Long postId = posts.getId(); String url = "http://localhost:"+ port + "/api/posts/" + postId + "/comments"; // when & then mvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(commentRequest))) .andExpect(status().isOk()) .andDo(print()); } }
3. 简化方案(可选)
如果不想自定义注解,可直接使用Spring Security提供的@WithUserDetails:
@Test @WithUserDetails(value = "testemail@gmail.com") public void comment_등록() throws Exception { // given:提前存入与@WithUserDetails匹配的用户 userRepository.save(User.builder() .name("testName") .email("testemail@gmail.com") .picture("testPic.com") .role(Role.USER) .build()); // 后续测试代码与之前一致 }
注意:需确保你的UserDetailsService的loadUserByUsername方法能通过邮箱查询到用户。
内容的提问来源于stack exchange,提问作者YSEO
相关产品推荐
相关产品推荐

