Using Spring Security with Spring MVC to provide method level security on Controller classes can be trooblesome : using CGLIB-based proxies might be mandatory and you might need to tweak your code to fit Spring.

When setting up exactly just that on a project, I ran into a series of problem and got a finer understanding on how Spring Security implements Method Security.

How to enable Method Security

As stated by the documentation, enabling Method level security with Spring Security is as simple as added a global-method-security tag in your configuration.

It's super easy and it works. You can even choose which annotation set you want to use with the appropriate attributes:

  • secured-annotations="enabled" for @Secured
  • jsr250-annotations="enabled" for @RolesAllowed
  • pre-post-annotations="enabled" for @PreAuthorize, @PostAuthorize, ...
  • metadata-source-ref="extraMethodSecurityMetadataSource" to use your own annotations
    • more on that in another article

Pitfalls when using Method Security on Spring MVC controllers

But in fact, it's not that easy, especially with Spring MVC controllers.

The position of global-method-security matters

The first problem I encountered when adding @Secured annotations on my Controller classes was that it simply didn't work. Spring would not enforce the required role(s), not even applying any control. My Controller classes didn't seem to be secured at all.

In fact, they weren't.

After some googling, I found out that the position of the global-method-security tag in the configuration files is very important. Only the beans in the current context seems to be "secured" (in fact proxied, more on that below).

As most people using Spring MVC, I had two Spring Application Contexts (AP) : one for the application and one for the DispatchServlet which inherits from the application context.

In this conditions, having the global-method-security tag in the application AP would ignore the Controller class declared/scanned in the DispatchServlet AP.

The solution is as simple as it seems : put the global-method-security tag in the DispatchServlet AP.

But if like me you have a Spring AP config file dedicated to Spring Security configuration, it will be part of the application's AP. And you will be sad to have to put just that one tag in another config file.

I found out I couldn't make the Spring Security config part of the DispatchServlet's AP. The DispatchServlet AP's would fail to start because no FilterChain bean existed when instanciating the org.springframework.web.filter.DelegatingFilterProxy declared in the web.xml.

So I put the global-method-security tag in the DispatcherServlet AP and was off to meet to the next problems :)

Having classes annoted with @Controller

To implement Method Security, Spring Security uses Spring AOP to create proxies of the annoted controllers. Proxies implements the security checks and it they are ok, call the user's class. By default, Spring will use JDK dynamic proxies to create a proxy object with the same methods as your class but which will not be an instance of your class.

This will more often that not cause errors at some point. The one I encountered is the following one where Spring MVC can not invoke the handler method :

java.lang.IllegalArgumentException: object is not an instance of declaring class
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:219)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:745)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:686)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:936)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:827)
javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:812)
javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118)
org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:54)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:183)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:343)
org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:260)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

What happens under the hood it that Spring MVC tries and fails to invoke the method annoted with @RequestMapping via refection. The Method instance it created when parsing controllers is based on the type of the concrete class of our controller. As the JDK proxy object is not an instance of the Controller class, we get a IllegalArgumentException.

A fast and efficient workaround this is to make Spring AOP create another type of proxy by adding proxy-target-class="true" to the global-method-security tag. This will tell Spring to use CGLIB-based subclass proxies instead of JDK dynamic proxies. Such proxies are actual instances of the proxied classes. That fixes our problem.

Having controller classes without default constructor

Happiness won't last though, if you have controllers which does not declare a default constructor.

If like me you favour constructor injection over property injection then your controller classes do not define a default constructor (well, they could, but not mine). So you're in for more trouble because CGLIB-based proxies require a default constructor to be created (see http://docs.spring.io/spring/docs/3.1.x/spring-framework-reference/html/aop.html#aop-proxying for details).

The DispatchServlet AP fails to start with an error such as the following :

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'managerController' defined in file [********************************/ManagerController.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class **********************.ManagerController]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:529)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
    at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:651)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:599)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:665)
    at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:518)
    at **********************.initWebApplicationContext(**********:**)
    at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:459)
    at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:136)
    at javax.servlet.GenericServlet.init(GenericServlet.java:160)
    at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1280)
    at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1193)
    at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1088)
    at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:5033)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5317)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class ****************.ManagerController]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:217)
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:111)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:477)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:362)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:322)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:409)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1488)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    ... 28 common frames omitted
Caused by: java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:721)
    at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
    at org.springframework.cglib.transform.TransformingClassGenerator.generateClass(TransformingClassGenerator.java:33)
    at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
    at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
    at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:285)
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:205)
    ... 35 common frames omitted

To fix this error, just add a default constructor to your bean :

@Controller
public class ManagerController {

  private final SomeService someService;

  // default constructor for CGLIB proxying
  // default constructor is called first and then public constructor is called with autowired dependencies
  // required to use the @Secured annotation on methods of this class
  public ManagerController() {
    this(null, null);
  }

  @Autowired
  public ManagerController(SomeService someService) {
    this.someService = someService;
  }

  @RequestMapping("/all")
  @Secured("ROLE_VIEW_MANAGERS")
  public String list() {
    return "viewname";
  }
}

Using a private default constructor

With Spring 3.2.4, the default constructor used by CGLIB-based proxies does not need to be public, so we can use a private constructor. This will avoid poluting the exposed methods of your class, but it is not very elegant nor practical. In fact, it is very annoying to have to add private default constructor to every Controller class that will use the @Secured annotation and a non-default constructor.

With Spring 3.2.8, this is not possible anyway. The default constructor has to be public otherwise proxy creation fails with the following error:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'managerController' defined in file [****************************************/ManagerController.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class **********************.ManagerController]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:529)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:296)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
    at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:651)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:602)
    at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:665)
    at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:521)
    at **********************.initWebApplicationContext(**************:**)
    at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:462)
    at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:136)
    at javax.servlet.GenericServlet.init(GenericServlet.java:160)
    at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1280)
    at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1193)
    at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:1088)
    at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:5033)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5317)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class ******************.ManagerController]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:218)
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:111)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:477)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:362)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:322)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:409)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1518)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    ... 28 common frames omitted
Caused by: java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
    at org.springframework.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:721)
    at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
    at org.springframework.cglib.transform.TransformingClassGenerator.generateClass(TransformingClassGenerator.java:33)
    at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
    at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
    at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:285)
    at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206)
    ... 35 common frames omitted

Using Controller interfaces

Theoretically, a way around the ugly default constructor is to add Spring MVC and Security annotations on an interface instead of a concrete class.

This way, we do not need to use CGLIB-based proxies and can stick to JDK proxies.

@Controller
public interface ManagerController {

  @RequestMapping("/all")
  @Secured("ROLE_VIEW_MANAGERS")
  String list();
}
@Component
public class ManagerControllerImpl implements ManagerController {

  private final SomeService someService;

  @Autowired
  public ManagerController(SomeService someService) {
    this.someService = someService;
  }

  @Override
  public String list() {
    return "viewname";
  }
}

Please note that the @Controller annotation has been moved to the interface as well as @RequestMapping and @Control annotations. Unfortunatly, this doesn't work with Spring MVC.

In this case as well in the one where we started struggling with Spring-AOP in the first place, we get the object is not an instance of declaring class exception from the Having classes annoted with @Controller part.

I'm wonder if this is a bug or at least if it could be improved...

Conclusion

For the time beeing I will stick to the CGLIB-enabled proxies with the default controller solution.

But I will try and see later if AspectJ couldn't be used to weave beans at compile time and remove the use of proxy completly.

Some references

While working and googling on this issue, I found this interrested comment on Stackoverflow which discusses the reasons to use Spring-AOP with controller when many ways to implements cross cutting concerns exist: http://stackoverflow.com/a/12045331.

I wrote an article on using one of theses other way in TODO article on generatic pagination solution with Spring MVC.

Also, on the subject of comparing JDK proxies and CGLIB-based proxies, this article points that @Transaction annotation does not work with JDK proxies: Spring annotation on interface or class implementing the interface??

References in Spring documentation

Various articles on the use of proxy-target-class="true" on the global-method-security tag:

Article on the importance of the position of the global-method-security tag in your Spring Config : http://stackoverflow.com/questions/517527/spring-not-enforcing-method-security-annotations


Published

Category

articles

Tags

Contact