java 在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38330597/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 03:19:27  来源:igfitidea点击:

Inject @AuthenticationPrincipal when unit testing a Spring REST controller

javaspringspring-mvcspring-securityspring-test

提问by andrucz

I am having trouble trying to test a REST endpoint that receives an UserDetailsas a parameter annotated with @AuthenticationPrincipal.

我在尝试测试接收UserDetails作为参数的 REST 端点时遇到问题@AuthenticationPrincipal.

It seems like the user instance created in the test scenario is not being used, but an attempt to instantiate using default constructor is made instead: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;

似乎没有使用在测试场景中创建的用户实例,而是尝试使用默认构造函数进行实例化: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;

REST endpoint:

REST 端点:

@RestController
@RequestMapping("/api/items")
class ItemEndpoint {

    @Autowired
    private ItemService itemService;

    @RequestMapping(path = "/{id}",
                    method = RequestMethod.GET,
                    produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Callable<ItemDto> getItemById(@PathVariable("id") String id, @AuthenticationPrincipal AppUserDetails userDetails) {
        return () -> {
            Item item = itemService.getItemById(id).orElseThrow(() -> new ResourceNotFoundException(id));
            ...
        };
    }
}

Test class:

测试类:

public class ItemEndpointTests {

    @InjectMocks
    private ItemEndpoint itemEndpoint;

    @Mock
    private ItemService itemService;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(itemEndpoint)
                .build();
    }

    @Test
    public void findItem() throws Exception {
        when(itemService.getItemById("1")).thenReturn(Optional.of(new Item()));

        mockMvc.perform(get("/api/items/1").with(user(new AppUserDetails(new User()))))
                .andExpect(status().isOk());
    }

}

How can I solve that problem without having to switch to webAppContextSetup? I want to write tests having total control of service mocks, so I am using standaloneSetup.

如何在不必切换到 的情况下解决该问题webAppContextSetup?我想编写完全控制服务模拟的测试,所以我正在使用standaloneSetup.

回答by Michael Piefel

This can be done by injection a HandlerMethodArgumentResolverinto your Mock MVC context or standalone setup. Assuming your @AuthenticationPrincipalis of type ParticipantDetails:

这可以通过将 aHandlerMethodArgumentResolver注入您的 Mock MVC 上下文或独立设置来完成。假设您@AuthenticationPrincipal的类型为ParticipantDetails

private HandlerMethodArgumentResolver putAuthenticationPrincipal = new HandlerMethodArgumentResolver() {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(ParticipantDetails.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return new ParticipantDetails(…);
    }
};

This argument resolver can handle the type ParticipantDetailsand just creates it out of thin air, but you see you get a lot of context. Later on, this argument resolver is attached to the mock MVC object:

这个参数解析器可以处理类型ParticipantDetails,只是凭空创建它,但你会看到你得到了很多上下文。稍后,此参数解析器附加到模拟 MVC 对象:

@BeforeMethod
public void beforeMethod() {
    mockMvc = MockMvcBuilders
            .standaloneSetup(…)
            .setCustomArgumentResolvers(putAuthenticationPrincipal)
            .build();
}

This will result in your @AuthenticationPrincipalannotated method arguments to be populated with the details from your resolver.

这将导致您的带@AuthenticationPrincipal注释的方法参数被您的解析器的详细信息填充。

回答by pzeszko

For some reason Michael Piefel's solution didn't work for me so I came up with another one.

出于某种原因,Michael Piefel 的解决方案对我不起作用,所以我想出了另一个。

First of all, create abstract configuration class:

首先,创建抽象配置类:

@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    WithSecurityContextTestExecutionListener.class})
public abstract MockMvcTestPrototype {

    @Autowired
    protected WebApplicationContext context;

    protected MockMvc mockMvc;

    protected org.springframework.security.core.userdetails.User loggedUser;

    @Before
    public voivd setUp() {
         mockMvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())
            .build();

        loggedUser =  (User)  SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    } 
}

Then you can write tests like this:

然后你可以写这样的测试:

public class SomeTestClass extends MockMvcTestPrototype {

    @Test
    @WithUserDetails("[email protected]")
    public void someTest() throws Exception {
        mockMvc.
                perform(get("/api/someService")
                    .withUser(user(loggedUser)))
                .andExpect(status().isOk());

    }
}

And @AuthenticationPrincipal should inject your own User class implementation into controller method

并且@AuthenticationPrincipal 应该将您自己的 User 类实现注入到控制器方法中

public class SomeController {
...
    @RequestMapping(method = POST, value = "/update")
    public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user) {
        ...
        user.getUser(); // works like a charm!
       ...
    }
}

回答by Sam

I know the question is old but for folks still looking, what worked for me to write a Spring Boot test with @AuthenticationPrincipal(and this may not work with all instances), was annotating the test @WithMockUser("testuser1")

我知道这个问题很老,但对于仍在寻找的人来说,对我编写 Spring Boot 测试有用的东西@AuthenticationPrincipal(这可能不适用于所有实例),是对测试进行注释@WithMockUser("testuser1")

@Test
@WithMockUser("testuser1")
public void successfullyMockUser throws Exception {
    mvc.perform(...));
}

Here is a linkto the Spring documentation on @WithMockUser

这是Spring 文档的链接@WithMockUser

回答by nimai

It's not well documented but there's a way to inject the Authenticationobject as parameter of your MVC method in a standalone MockMvc. If you set the Authenticationin the SecurityContextHolder, the filter SecurityContextHolderAwareRequestFilteris usually instantiated by Spring Security and makes the injection of the auth for you.

它没有很好的文档记录,但是有一种方法可以将Authentication对象作为 MVC 方法的参数注入到独立的 MockMvc 中。如果您在Authentication中设置SecurityContextHolder,则过滤器SecurityContextHolderAwareRequestFilter通常由 Spring Security 实例化并为您注入身份验证。

You simply need to add that filter to your MockMvc setup, like this:

您只需将该过滤器添加到您的 MockMvc 设置中,如下所示:

@Before
public void before() throws Exception {
    SecurityContextHolder.getContext().setAuthentication(myAuthentication);
    SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
    authInjector.afterPropertiesSet();
    mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
}