2014-02-26

Spring Security: authorization on controller methods

Spring Security offers three methods of authorization:
  • securing URLs using <intercept-url> or authorizeRequests() in XML/Java config (also called web security or URL security)
  • securing individual method invocations using <global-method-security> or @EnableGlobalMethodSecurity
  • securing domain objects using ACL
The first method is well-suited for securing servlets and controllers.
The second works (with a default configuration) for service layer components, but not for controllers. This is because you usually add Spring Security in root application context and, since controllers live in web application context, Spring Security cannot access them. Still, very often developers try to apply global method security to controller methods. Spring Security FAQ has even an answer to such question. Usually the solution is to move <global-method-security> to web application context.

I think that many of those developers actually don't need global method security on controller methods. For example see these four threads: [1], [2], [3] and [4] - they all require a simple "hasRole(XXX)" check. So I think that what they really need is to configure some authorization mechanism using annotations directly on controller methods (because it is more convenient than configuring it using URL patterns). It doesn't matter, if it's web security or global method security. However, they choose global method security, because it supports annotations.

In this post I would like to show an alternative approach - something that is between web security and global method security. It will use web security expressions, but will be configured by annotations applied to controller methods. Let's see how this can be implemented.

Implementation
What we want to achieve is to be able to write controller methods like this:
@Controller
public class AppController {

  @AuthorizeRequest("hasRole('ROLE_USER')")
  @RequestMapping("/user")
  public String user() {
    return "/user";
  }
  ...
}
and have authorization rules applied by Spring Security. We want to do this without moving <global-method-security> to web application context (so everything remains in its usual place). Instead we will use URL security mechanisms, but configure and invoke them in a different way than through <intercept-url>.

First let's create an annotation and an interceptor:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorizeRequest {
  /**
   * @return the Spring-EL expression to be evaluated before invoking the protected method
   */
  String value();
}
public class AuthorizeRequestInterceptor extends HandlerInterceptorAdapter {

  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
    if (handler instanceof HandlerMethod) {
      HandlerMethod handlerMethod = (HandlerMethod) handler;
      AuthorizeRequest ann = handlerMethod.getMethodAnnotation(AuthorizeRequest.class);
      if (ann != null) {
        checkAccess(req, resp, ann);
      }
    }
    return true;
  }

  private void checkAccess(HttpServletRequest req, HttpServletResponse resp, AuthorizeRequest ann) {
    ...
  }
}
This interceptor looks for and handles @AuthorizeRequest annotations (we could have used @PreAuthorize annotation, but with another annotation it is really clear that we're not using global method security). The challenge is to implement checkAccess() method. We would like it to do the same thing, which is done for <intercept-url> elements. After searching Spring Security sources it turns out that <intercept-url>s are handled by a FilterSecurityInterceptor, which uses an AccessDecisionManager. The AccessDecisionManager has one AccessDecisionVoter - the WebExpressionVoter, which makes authorization decisions. So AuthorizeRequestInterceptor can be implemented like this:
public class AuthorizeRequestInterceptor extends HandlerInterceptorAdapter {

  @Autowired
  private AccessDecisionManager accessDecisionManager;

  @Autowired
  private SecurityExpressionHandler<FilterInvocation> securityExprHandler;

  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
    ...
  }

  private void checkAccess(HttpServletRequest req, HttpServletResponse resp, 
      AuthorizeRequest ann) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    FilterInvocation fi = new FilterInvocation(req, resp, new NullFilterChain());
    Expression expr = securityExprHandler.getExpressionParser().parseExpression(ann.value());
    ConfigAttribute configAttribute = new PublicWebExpressionConfigAttribute(expr);
    List<ConfigAttribute> configAttributes = Arrays.asList(configAttribute);
    accessDecisionManager.decide(authentication, fi, configAttributes);
  }
}
This looks quite complicated, but it is just preparing parameters using standard Spring classes and calling accessDecisionManager.decide() (I will describe my custom classes NullFilterChain and PublicWebExpressionConfigAttribute in a minute). Unfortunately it won't work yet, because the AccessDecisionManager used by the FilterSecurityInterceptor is not exposed as a Spring bean and cannot be @Autowired. I couldn't find a way to expose it, but fortunately it is possible to make it the other way around: create the AccessDecisionManager bean yourself and pass it to the FilterSecurityInterceptor. This is how it looks in code:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().accessDecisionManager(accessDecisionManager())
    ...
  }

  @SuppressWarnings("rawtypes")
  @Bean
  public AccessDecisionManager accessDecisionManager() {
    WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
    return new AffirmativeBased(Arrays.<AccessDecisionVoter> asList(webExpressionVoter));
  }
  ...
}
The implementation is finished and works as expected. Going back to method AuthorizeRequestInterceptor.checkAccess() and classes NullFilterChain and PublicWebExpressionConfigAttribute:
  • I needed a FilterChain to create a FilterInvocation. I couldn't pass null to the constructor, but since this FilterChain is not used, I passed a NullFilterChain which does nothing.
  • I also needed a WebExpressionConfigAttribute, but it is not a public class and I couldn't instantiate it. So I created a public PublicWebExpressionConfigAttribute in package org.springframework.security.web.access.expression (see this issue for more information).
For the record, this is the source code of aforementioned classes:
public class NullFilterChain implements FilterChain {

  @Override
  public void doFilter(ServletRequest request, ServletResponse response) {
    throw new UnsupportedOperationException("doFilter()");
  }
}
public class PublicWebExpressionConfigAttribute extends WebExpressionConfigAttribute {

  private static final long serialVersionUID = 1L;

  public PublicWebExpressionConfigAttribute(Expression authorizeExpression) {
    super(authorizeExpression);
  }
}
The full source code for this post can be downloaded from my github account. It is a web application, which demonstrates various ways of authorization: URL security, global method security and AuthorizeRequestInterceptor. It shows those mechanisms with three users (user, manager, admin) and a simple RoleHierarchy (admin > manager > user).

Summary
The advantage of my solution over using pure URL security is that sometimes it is more convenient to specify authorization rules directly on controller methods rather than through URL pattern matching. Note that AuthorizeRequestInterceptor and FilterSecurityInterceptor use the same AccessDecisionManager, so their authorization decisions are consistent and you can easily swap one for the other.

The advantage of my solution over using global method security in web application context is that Spring Security beans stay in their normal place. This is important for example when you have more than one DispatcherServlet and thus more than one web application context. However, the advantage of using global method security is that it can use more complex authorization expressions (for example it can refer to method's arguments). AuthorizeRequestInterceptor can use only what is available in web security expressions. Still, it can be sufficient for many situations.

On a final note, I have somewhat mixed feelings about my implementation. On the one hand, it is quite simple (just a few classes, which use existing Spring Security classes and don't reinvent the wheel), consistent with standard web security expressions and easy to use. But on the other hand, I had to create a class in Spring's package, which feels rather 'dirty'. Nonetheless I think it is an interesting solution and implementing it gave me a chance to better understand how Spring Security works internally.

Update 2014-03-30
I improved the code from this post, so it now integrates with Spring Security better. For more information see my second post about authorization on controller methods.