在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal
Posted
技术标签:
【中文标题】在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal【英文标题】:Inject @AuthenticationPrincipal when unit testing a Spring REST controller 【发布时间】:2016-11-14 19:07:33 【问题描述】:我在尝试测试接收 UserDetails
作为带有 @AuthenticationPrincipal.
注释的参数的 REST 端点时遇到问题
似乎没有使用在测试场景中创建的用户实例,而是尝试使用默认构造函数进行实例化:org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.andrucz.app.AppUserDetails]: No default constructor found;
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));
...
;
测试类:
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());
如何在不切换到webAppContextSetup
的情况下解决该问题?我想编写完全控制服务模拟的测试,所以我使用standaloneSetup.
【问题讨论】:
你需要follow these instructions。 那么没有办法使用standaloneSetup结合认证? 它在哪里说的? 我不确定,但是我怎么能得到一个FilterChainProxy,这是必需的? 您也可以使用 webAppContextSetup,同时仍然通过@ContextConfiguration
保持对 bean 的完全控制。
【参考方案1】:
这可以通过将HandlerMethodArgumentResolver
注入到您的 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(…);
;
这个参数解析器可以处理 ParticipantDetails
类型并且只是凭空创建它,但是你会看到你得到了很多上下文。稍后,这个参数解析器被附加到模拟 MVC 对象:
@BeforeMethod
public void beforeMethod()
mockMvc = MockMvcBuilders
.standaloneSetup(…)
.setCustomArgumentResolvers(putAuthenticationPrincipal)
.build();
这将导致您的 @AuthenticationPrincipal
带注释的方法参数被您的解析器中的详细信息填充。
【讨论】:
这是个好方法,只是要小心!默认情况下注册了大约 20 个参数解析器来处理有趣的类型。在我的例子中,一个 Authentication 参数由 ServletRequestMethodArgumentResolver 处理,而我的处理程序被忽略了。【参考方案2】:由于某种原因,Michael Piefel 的解决方案对我不起作用,所以我想出了另一个解决方案。
首先,创建抽象配置类:
@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();
然后你可以这样写测试:
public class SomeTestClass extends MockMvcTestPrototype
@Test
@WithUserDetails("someUser@app.com")
public void someTest() throws Exception
mockMvc.
perform(get("/api/someService")
.withUser(user(loggedUser)))
.andExpect(status().isOk());
并且@AuthenticationPrincipal 应该将你自己的 User 类实现注入到控制器方法中
public class SomeController
...
@RequestMapping(method = POST, value = "/update")
public String update(UdateDto dto, @AuthenticationPrincipal CurrentUser user)
...
user.getUser(); // works like a charm!
...
【讨论】:
【参考方案3】:没有很好的文档记录,但是有一种方法可以将Authentication
对象作为独立 MockMvc 中的 MVC 方法的参数注入。如果你在SecurityContextHolder
中设置Authentication
,过滤器SecurityContextHolderAwareRequestFilter
通常由Spring Security实例化并为你注入身份验证。
您只需将该过滤器添加到您的 MockMvc 设置中,如下所示:
@Before
public void before() throws Exception
SecurityContextHolder.getContext().setAuthentication(myAuthentication);
SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
authInjector.afterPropertiesSet();
mvc = MockMvcBuilders.standaloneSetup(myController).addFilters(authInjector).build();
【讨论】:
【参考方案4】:我知道这个问题已经过时了,但对于仍在寻找的人来说,对我使用 @AuthenticationPrincipal
编写 Spring Boot 测试(这可能不适用于所有实例)有用的是注释测试 @WithMockUser("testuser1")
@Test
@WithMockUser("testuser1")
public void successfullyMockUser throws Exception
mvc.perform(...));
Here is a link @WithMockUser
上的 Spring 文档
【讨论】:
新的Spring链接:docs.spring.io/spring-security/site/docs/current/api/org/…【参考方案5】:@pzeszko answer的简化:
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
public class ControllerTest
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails(value = "user@gmail.com")
void get() throws Exception
mockMvc.perform(MockMvcRequestBuilders.get(URL))
.andExpect(status().isOk())
.andDo(print());
见:
Testing with a mock environment Mock authentication in Spring【讨论】:
【参考方案6】:这个解决方案对我有用,我发现它真的很方便。
在test
包中创建一个实现UserDetailsServce的TestIUserDetails
服务:
@Service
@Primary
@Profile("test")
public class TestIUserDetails implements UserDetailsService
public static final String ADMIN_USERNAME = "admin@example.com";
public static final String USERNAME = "user@example.com";
private User getUser()
User user = new User();
user.setEmail(USERNAME);
user.setId(1L);
return user;
...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
if (Objects.equals(username, ADMIN_USERNAME))
return getAdminUser();
else if (Objects.equals(username, USERNAME))
return getUser();
return getPublicUser();
现在,在你的测试中:
@SpringMockWebEnvTestConfig
class AbcControllerTest
@Autowired
private MockMvc mvc;
@Autowired
UserDetailsService userDetailsService;
private User user;
@BeforeEach
void setUp()
user = (User) userDetailsService.loadUserByUsername(TestUserDetailsImpl.USERNAME);
@Test
public void testAbc()
this.mvc.perform(post(endpoint).with(user(user))
...
.andExpect(status().isCreated())...
【讨论】:
以上是关于在对 Spring REST 控制器进行单元测试时注入 @AuthenticationPrincipal的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Spring-boot 中不模拟服务类的情况下为 REST 控制器端点编写单元测试