Describe the bug
When using a CustomUserDetails
(interface & extends UserDetails
) and testing presentation(controller) layer via @WebMvcTest
, org.springframework.data.web.ProxingHandlerMethodArgumentResolver
is being used as the ArgumentResolver instead of AuthenticationPrincipalArgumentResolver
.
Consequently, a null
value is bound to the CustomUserDetails userDetails
method parameter in the Controller class.
To Reproduce
Need to use @WebMvcTest
and test a controller's handler method that hava CustomUserDetails
(interface) as a parameter. Additionally, you must configure springSecurity()
when setting mockMvc
and conduct the test without importing any custom @EnableWebSecurity
classes.
Expected behavior
I expected the `CustomUserDetailsImpl value to be bound correctly, but it wasn't.
Sample
@RestController
public class TestController {
@GetMapping("/test")
public ResponseEntity<?> getTest(@AuthenticationPrincipal CustomUserDetails userDetails) {
System.out.println("user Id : " +userDetails.getUserId());
System.out.println("user : " + userDetails);
System.out.println("user name : " + userDetails.getUsername());
return ResponseEntity.ok("success");
}
public interface CustomUserDetails extends UserDetails {
Role getRole();
Long getUserId();
}
@Getter
@RequiredArgsConstructor
public class CustomUserDetailsImpl implements CustomUserDetails {
private final User user; // Custom User
/* ... */
}
@WebMvcTest(
controllers = TestController.class
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})
}
)
public class TestControllerTest {
private MockMvc mockMvc;
private CustomUserDetails userDetails;
void setUpUserDetails(Role role) {
User user = User.builder()
.username("test")
.password("password")
.email("test@test.com")
.role(role)
.nickname("nickname")
.build();
ReflectionTestUtils.setField(user, "id", 1L);
userDetails = new CustomUserDetailsImpl(user);
}
@BeforeEach
void setUp(WebApplicationContext webApplicationContext) {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.build();
}
@Test
@DisplayName("CustomUserDetailsTest")
void testCustomUserDetails() throws Exception {
// given
setUpUserDetails(Role.USER);
// when
ResultActions resultActions = mockMvc.perform(
get("/test/get")
.with(user(userDetails))
);
// then
resultActions.
andExpect(status().isOk);
}
}
Motivation & Solution
I designed my project with the above structure to use polymorphism and use multiple CustomUserDetails
implementations for users with various roles.
Upon debugging, I discovered that when running tests with @WebMvcTest
without importing a custom @EnableWebSecurity
class, SecurityAutoConfiguration.class
is imported.
In this scenario, org.springframework.data.web.ProxingHandlerMethodArgumentResolver
is positioned before AuthenticationPrincipalArgumentResolver
, causing it to be used as the ArgumentResolver.
More precisely, when WebMvcConfigurer
is registered, SpringDataWebConfiguration
is registered before WebMvcSecurityConfiguration
. This results in springframework.data.web
related ArgumentResolvers being registered first.
However, when a custom @EnableWebSecurity
class is imported, WebMvcSecurityConfiguration
is registered first, placing AuthenticationPrincipalArgumentResolver
before ProxingHandlerMethodArgumentResolver
. This eliminates the issue when CustomUserDetails
is used as a method argument.
Therefore, I believe this problem can be resolved by advancing the registration order of WebMvcSecurityConfiguration
as a Configurer when SpringAutoConfiguration
is used.
Although this issue doesn't seem to be exclusively limited to the spring-security project, I am reporting it there first.