As already stated, Spring Security's @PreAuthorize
is method advice, which means that it does not get to participate until the method and its arguments have already been resolved.
Aside from the answer already given, there are a few ways to move authorization before argument resolution, instead.
Filter Security
First, Spring Security checks URLs before the request is mapped to a method. And since this is a @Controller
, it's reasonable to suppose that you could instead map the request to the role at that level instead of @PreAuthorize
:
http
.authorizeRequests()
.mvcMatchers(POST, "/somepath").hasRole("CREATE_USER")
Handler Interceptor
Second, Spring MVC does ship with limited support for checking authorities before parsing method arguments. For example, you can do:
@EnableWebMvc
public static class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
UserRoleAuthorizationInterceptor userRole =
new UserRoleAuthorizationInterceptor();
userRole.setAuthorizedRoles("CREATE_USER");
registry.addInterceptor(userRole);
}
}
This is much more basic than @PreAuthorize
since it's a global setting, but I've included it for completeness.
Handler Interceptor, Part 2
Third (warning, some inelegance ahead), you can create your own HandlerInterceptor
.
The flow is:
FilterSecurityInterceptor
<== where .mvcMatchers(...).hasRole(...)
lives
- Then
HandlerInterceptor
s
- Then argument validation
- Then
MethodSecurityInterceptor
<== where @PreAuthorize
lives
So, your HandlerInterceptor
would check before arguments are resolved. It doesn't have to be as involved as MethodSecurityInterceptor
, though. It could, for example, simply be:
static class AuthorizationInterceptor extends HandlerInterceptorAdapter {
SecurityMetadataSource securityMetadataSource;
AccessDecisionManager accessDecisionManager;
@Override
public void preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
Authentication authenticated = (Authentication) request.getUserPrincipal();
MethodInvocation mi = convert(handler);
Collection<ConfigAttribute> attributes =
this.securityMetadataSource.getAttributes(mi);
// throws AccessDeniedException
this.accessDecisionManager.decide(authenticated, mi, attributes);
return true;
}
}
Then you wire it together with:
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodConfig extends GlobalMethodSecurityConfiguration {
@Bean
HandlerInterceptor preAuthorize() throws Exception {
return new AuthorizationInterceptor(
accessDecisionManager(), methodSecurityMetadataSource());
}
}
@EnableWebMvc
public static class MvcConfig implements WebMvcConfigurer {
@Autowired
AuthorizationInterceptor authorizationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor);
}
}
It's inelegant because MethodSecurityInterceptor
would still participate in authorized requests, which would ostensibly be the majority.