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

如何在SpringBootTest中集成SpringSecurity并测试评论提交接口

问题解决:Spring Security测试中配置SecurityContext实现评论关联用户

核心问题分析

  1. 自定义@WithMockCustomUser注解与对应的SecurityContext工厂类不匹配,导致依赖注入失败
  2. UserDetailService.returnUser()中通过字符串截取邮箱的逻辑过于脆弱,且容易出错
  3. 测试类中内部工厂类依赖外部测试类(非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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 21:30:53