Reputation: 5560
servletApi() support of Spring Security is great.
I want to inject custom Principal as this:
public interface UserPrincipal extends Principal {
public Integer getId();
}
@RequestMapping(value = "/")
public ResponseEntity<List<Conversation>> listAfter(UserPrincipal user){
// implementation
}
or
@RequestMapping(value = "/")
public ResponseEntity<List<Conversation>> listAfter(UserPrincipalImpl user){
// implementation
}
Spring has support for injecting Principal
instances with the help of ServletRequestMethodArgumentResolver
.
It is injecting principal as this:
else if (Principal.class.isAssignableFrom(paramType)) {
return request.getUserPrincipal();
}
Here is the place where the problem begins. request
is here an instance of SecurityContextHolderAwareRequestWrapper
. It has an implementation of:
@Override
public Principal getUserPrincipal() {
Authentication auth = getAuthentication();
if ((auth == null) || (auth.getPrincipal() == null)) {
return null;
}
return auth;
}
Because an Authentication
is also an Principal
. (The only part of spring security I did not like so far. I will ask this a separate question as well.)
This is causing a problem. Because Authentication
is a Principal
not a UserPrincipal
.
How can I resolve this problem? Do I need to implement an authentication which is a UserPrincipal as well? Or should I change HandlerMethodArgumentResolver order a create a custom resolver? (This is not easy for Spring MVC because internal handlers has higher priority.)
As a extra information:
I am using Spring Security M2 and my configuration for AuthenticationManagerBuilder
is simply:
@Override
protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(detailsService);
}
Any help?
Upvotes: 15
Views: 20827
Reputation: 10539
I know this is an old question, but as it does come up on top on Google when searching for injecting a Principal, I'll post a 2020 update:
Since Spring Security 4.0 you can just simply inject an @AuthenticationPrincipal
into your controller methods:
@RequestMapping(value = "/")
public ResponseEntity<List<Conversation>> listAfter(@AuthenticationPrincipal UserPrincipal user){
// implementation
}
This will work out of the box, no additional config required.
Upvotes: 20
Reputation: 21720
Fundamentally this seems like trouble integrating with Spring MVC and not a Spring Security issue. Spring Security has no way of knowing that Authentication@getPrinicpal() implements Principal since the API returns an Object.
I see a few options for you. Each has some pros and cons, but I think the best is using @ModelAttribute
and @ControllerAdvice
@ModelAttribute
and @ControllerAdvice
The easiest option is annotate a method with @ModelAttribute
on custom @ControllerAdvice
. You can find details in the Spring Reference.
@ControllerAdvice
public class SecurityControllerAdvice {
@ModelAttribute
public UserPrincipal customPrincipal(Authentication a) {
return (UserPrincipal) a == null ? null : a.getPrincipal();
}
}
Now in your controller you can do something like this:
@RequestMapping(value = "/")
public ResponseEntity<List<Conversation>> listAfter(@ModelAttribute UserPrincipal user){
// implementation
}
Note that the @ModelAttribute
is necessary only to ensure the @ModelAttribute
is used over the HttpServletRequest#getPrincipal(). If it did not implement Principal, @ModelAttribute
is not required.
@Value
and ExpressionValueMethodArgumentResolverYou can also do something like this:
@RequestMapping(value = "/")
public ResponseEntity<List<Conversation>> listAfter(
@Value("#{request.userPrincipal.principal}") UserPrincipal user){
// implementation
}
This works because the HttpServletRequest is available as an attribute to the ExpressionValueMethodArgumentResolver (added by default by Spring MVC) which allows accessing things via SpEL. I find this less attractive than @ModelAttribute
due to the constant that must be in the @Value
annotation. It will be nicer when SPR-10760 is resolved which would allow for your own custom annotation to be used like:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Value("#{request.userPrincipal.principal}")
public @interface CurrentUser { }
This is a bit sloppy because the RequestMappingHandlerAdapter has already been initialized, but you can change the ordering of the HandlerMethodArgumentResolvers as shown here:
@EnableWebMvc
@Configuration
public class WebMvcConfiguration
extends WebMvcConfigurerAdapter {
...
@Autowired
public void setArgumentResolvers(RequestMappingHandlerAdapter adapter) {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
resolvers.add(new CustomPrincipalArgumentResolver());
resolvers.addAll(adapter.getArgumentResolvers().getResolvers());
adapter.setArgumentResolvers(resolvers);
}
}
You can also extend WebMvcConfigurationSupport instead of using @EnableWebMvc
to ensure your HandlerMethodArgumentResolver is used first. For example:
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {
...
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter()();
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
resolvers.add(new CustomPrincipalArgumentResolver());
resolvers.addAll(adapter.getArgumentResolvers().getResolvers());
adapter.setArgumentResolvers(resolvers);
return adapter;
}
}
Upvotes: 24