如何在Spring Boot POST方法的测试中使用.getPrincipal()方法?

4
我有一个 POST 方法,用于发送“Item”。每个“Item”都包含作者的用户 ID。为了获取作者 ID,我使用:(User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 但是在测试中,由于结果以下原因,它不能正常工作: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.ClassCastException: org.springframework.security.core.userdetails.User cannot be cast to ru.pravvich.domain.User POST 方法:
@PostMapping("/get_all_items/add_item_page/add_item")
public String addItem(@RequestParam(value = "description") final String description) {

    final User user = (User) SecurityContextHolder
            .getContext()
            .getAuthentication()
            .getPrincipal();

    final Item item = new Item();
    item.setDescription(description);
    item.setAuthorId(user.getId());
    service.add(item);
    return "redirect:/get_all_items";
}

And test:

@Test
@WithMockUser(username = "user", roles = "USER")
public void whenPostThenAdd() throws Exception {

    Item item = new Item();
    item.setDescription("test");

    mvc.perform(
            post("/get_all_items/add_item_page/add_item")
                    .param("description", "test")
    ).andExpect(
            status().is3xxRedirection()
    );

    verify(itemService, times(1)).add(item);
}

从浏览器表单发送数据时为什么不会抛出ClassCastException?如何解决这个问题?
谢谢您。
更新:
@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepo;

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepo.findByUsername(username);
            if (user == null) user = new User();
            return user;
        }
}

可能是[为什么Spring测试失败,@MockBean不起作用]的重复问题(https://dev59.com/K6Tja4cB1Zd3GeqPFa78)。 - Abhijit Sarkar
你能否使用@WithUserDetails?https://docs.spring.io/spring-security/site/docs/current/reference/html/test-method.html#test-method-withuserdetails - Daniel C.
2个回答

4
@WithMockUser可以帮助您注入一个org.springframework.security.core.userdetails.User对象并进行测试,但在这种情况下,您需要一个自定义用户对象ru.pravvich.domain.User
为什么它与浏览器表单一起使用?因为principal来自您的身份验证过程,例如,如果您有自己的UserDetailsService以返回您的自定义ru.pravvich.domain.User,则在正常情况下,在通过Web表单进行身份验证时会发生此过程,但在测试用例场景中,除非您配置了测试用例并告知您的自定义实现,否则不会发生这种情况。让我们看一个例子:
扩展自用户的自定义用户类(在此处更新)
   //MySpring user is just a class that extends from 
   //org.springframework.security.core.userdetails.User
   //org...security....User implements UserDetailsService that is the 
   //the reason why I can return this class from loadUserByUsername method, you will see it later
   //it helps me to wrap my own User definition
   public class MySpringUser  extends User {

    //this is my own user definition, where all my custom fields are defined
    private MyUser user;

    //the constructor expect for myUser and then I call the super
    //constructor to fill the basic fields that are mandatory for spring 
    //security in order to perform the authentication process
    public MySpringUser(MyUser myUser, Collection<? extends GrantedAuthority> authorities) {
        super(myUser.getUsername(), myUser.getPassword(), myUser.isEnabled()
                , true, true, true, authorities);

        this.setUser(myUser);
    }

    public MyUser getUser() {
        return user;
    }

    public void setUser(MyUser user) {
        this.user = user;
    }
    //this is an example of how to get custom fields a custom id for example
    public Long getId(){
        return this.getUser().getId();
    }
}

自定义用户详细信息服务 (在此更新)

@Service
public class MyUserService implements UserDetailsService {

    @Autowired
    MyUserRepository myUserRepository;

    //Principal
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        //Here I get the User that I defined for my own domain structure
        MyUser myUser = myUserRepository.findFirstByUsername(s);

        //Here I return a new User object, in this case MySpringUser is
        //just a class that  extends from User class, its constructor 
        //receive myUser object as parameter in order to get my own custom fields later   
        return new MySpringUser(myUser,AuthorityUtils.createAuthorityList("ADMIN"));

    }
}

最后,使用@WithUserDetails测试用例通知测试应该调用由自定义UserDetailsService实现的loadUserByUsername方法,在这种情况下,您可以将主体转换为自定义用户对象。

    @Test
    @WithUserDetails( "myUserNameOnDatabase")
    public void contextLoads() {
        try {
            mockMvc.perform(get("/stores")).andExpect(status().isOk()).andDo(print());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

这是在控制器中执行转换的方式,就像你的一样。
  @RequestMapping("/stores")
    public List<Store> getStores(Principal principal){

        MySpringUser activeUser = (MySpringUser) ((Authentication) principal).getPrincipal();
        System.out.println(activeUser.getUsername());

        return storeRepository.findByMyUserId();

    }

(更新内容) 我还上传了一个完整的示例到我的git存储库,你可以下载并尝试它以便更清楚地了解,当然这只是其中一种选项,还有其他选项,例如,你可以创建自己的类来实现UserDetails接口,而不是扩展spring User类,选择取决于你的需求。
希望这个替代方案对你有所帮助。 我的git存储库示例如何使用@WithUserDetails

谢谢。我在理解loadUserByUsername(String s)方面遇到了问题,您能否不使用lambda重写一下?我还有一个问题:是否有一种解决方案,可以不使用org.springframework.security.core.userdetails.User?当我使用security...User时,我的认证系统崩溃了。我已经更新了问题和我的UserServiece版本。也许这会提供更多关于情况的信息。再次感谢您。 - Pavel
是的!你很棒!它起作用了。Git仓库回答了所有问题。谢谢! - Pavel

0

Spring Security的@WithMockUser会使用org.springframework.security.core.userdetails.User对象填充安全上下文,这就是为什么您会遇到此异常的原因。文档非常清楚:

身份验证中的主体是Spring Security的User对象

解决ClassCastException的一种可能方法是简单地重用Spring的User类,并使用getUsername()返回作者ID,或者如果您必须自定义User对象,则找到另一种方法来填充测试的安全上下文。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接