Spring Security测试:.getPrincipal()在应用程序和测试中返回不同的对象



我正在制作一个Reddit克隆,作为我投资组合的项目之一。

我无法解决的问题(我是初学者(是:

我有一个CommentController(REST(,它处理所有关于注释的api调用。创建评论有一个端点:

@PostMapping
public ResponseEntity<Comment> createComment(@Valid @RequestBody CommentDto commentDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw new DtoValidationException(bindingResult.getAllErrors());
URI uri = URI.create(ServletUriComponentsBuilder.fromCurrentContextPath().path("/api/comments/").toUriString());
Comment comment = commentService.save(commentDto);
return ResponseEntity.created(uri).body(comment);
}

在我的CommentService类中,这是保存当前登录用户所做评论的方法:

@Override
public Comment save(CommentDto commentDto) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Optional<User> userOptional = userRepository.findUserByUsername((String) principal);
if (userOptional.isPresent()) {
User user = userOptional.get();
Optional<Post> postOptional = postRepository.findById(commentDto.getPostId());
if (postOptional.isPresent()) {
Post post = postOptional.get();
Comment comment = new Comment(user, commentDto.getText(), post);
user.getComments().add(comment);
post.getComments().add(comment);
post.setCommentsCounter(post.getCommentsCounter());
return comment;
} else {
throw new PostNotFoundException(commentDto.getPostId());
}
} else {
throw new UserNotFoundException((String) principal);
}
}

该应用程序运行正常,没有任何异常,并且注释已保存到数据库中。

我正在为那个控制器写一个集成测试,我在一个测试类上使用了@WithMockUser(用户名="janedoe",密码="password"(,我一直得到这个异常:

ClassCastException: UserDetails can not be converted to String

我意识到问题出在保存方法中的这两行:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Optional<User> userOptional = userRepository.findUserByUsername((String) principal);

我不明白的是,为什么那些到行在测试中只抛出异常。应用程序运行时,一切正常。

我想,由于某种原因,在测试中.getPrincipal((方法不是返回String(用户名(,而是返回整个UserDetails对象。我不知道如何让它在测试中返回用户名。

我试图解决什么问题:

  1. 将@WithMockUser更改为@WithUserDetails

  2. 在类和方法级上同时使用@WithMockUser和@WithUserDetails

  3. 创建自定义@WithMockCustomUser注释:

    @Retention(RetentionPolicy.RUNTIME)
    @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
    public @interface WithMockCustomUser {
    String username() default "janedoe";
    String principal() default "janedoe";
    

}


Which just gives me the same ClassCastException with different text:
class com.reddit.controller.CustomUserDetails cannot be cast to class java.lang.String (com.reddit.controller.CustomUserDetails is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
Any help is appreciated :)

你说得对。这是因为生产代码中的身份验证逻辑与测试不同。在生产代码中,它为Authentication配置字符串类型主体,而@WithMockUser/@WithUserDetails配置非字符串类型主体。

只要将字符串类型主体配置为Authentication,就可以实现自定义@WithMockCustomUser

以下实现应该可以解决您的问题:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String[] authorities() default {};
String principal() default "foo-principal";
}
public class WithMockCustomUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser withUser) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : withUser.authorities()) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(withUser.principal(),
"somePassword", grantedAuthorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}

并在测试中使用它:

@Test
@WithMockCustomUser(principal="janedoe")
public void test() {
// your test code
}

最新更新