Short version:
I had a few problems with the <intercept-url> style of securing my web-app, so I switched to using @Secured on my controllers, but found that the <authorize> JSP tag no longer worked. So I fixed it. I'd like feedback on my approach, and some advice on how to get this fix polished up so I can submit it for inclusion in the codebase.
Long version:
I have numbered specific questions inline.
My app uses Spring 3 and Spring Security. I have role based permission, and REST / CRUD style controllers.
For example, I might have a FooController, with list(), show(), editForm(), createForm(), handleFormSubmission() actions.
These actions would map to "/foo" for the list, "/foo/{id}" for the show, "/foo/{id}?form" for the edit form, etc.
(This is how Spring Roo sets up a project, but I'm not currently using Roo).
Now, I want all the FooController actions to be restricted only to users with ROLE_FOO_MANAGER.
What should I do?
As far as I can tell, I need to add to my applicationContext-security.xml a line like:
1. Is that right?Code:<intercept-url pattern="/foo/**" access="hasRole('ROLE_FOO_MANAGER')" />
Now I found that this blocks access to /foo and /foo/3, but it didn't block access to /foo.html ! This is a pretty gaping security hole.
Did I need instead to add:
?Code:<intercept-url pattern="/foo/**" access="hasRole('ROLE_FOO_MANAGER')" /> <intercept-url pattern="/foo*" access="hasRole('ROLE_FOO_MANAGER')" />
Will there be any more holes I am not aware of?
I'm not that happy with the security config being specified by URL pattern, when the business security requirement is specified by controller (or action). Clearly the @Secured annotation on the controller is a better fit for what I'm trying to do.
So I switched to that. I had to turn on
in both my applicationContext-security.xml and my applicationContext.xml files. I then converted my leaky URL patterns into @Secured annotations on the controllers and the actions, and I was much happier.Code:<global-method-security secured-annotations="enabled" />
However, this change means that the <authorize> JSP tags no longer worked. Looking through the code, it seems their implementation is tied to the <intercept-url> tags.
2. Is there an already-implemented way to get <authorize> tags to work with @Secured annotations?
I was able to enhance the <authorize> tags to work with @Secured annotations on controllers by doing the following:
- Writing a custom WebInvocationPrivilegeEvaluator (code below)
- Adding my Privilege Evaluator as a bean to the applicationContext-security.xml before the <http> tag, causing the <authorize> tags to use my Evaluator instead of the Default evaluator.
- Writing a custom Spring DispatcherServlet which registers itself as a singleton in the application context, so my WebInvocationPrivilegeEvaluator can get a reference to it (code below)
- Making the (previously protected) getHandler() method public in my custom DispatcherServlet
- Tweaking the Spring Security DummyRequest class a bit (code below)
I have the following questions:
3. Is this sensible? Have I made any mistakes?
4. Would this enhancement be useful to others? Shall I try and get it committed to Spring Security?
5. In order to add this to Spring Security, I will need to either change the DispatcherServlet in Spring at the same time, or require that Security users of this feature use a custom DispatcherServlet. What's the best way around this?
6. It seems to me that the @Secured annotation is much better than the <intercept-url> patterns, due to the issues discussed above. Once I have this committed, shall I try and update the Spring Security documentation to recommend that instead of the patterns?
I need to resolve (5) before I can submit a patch. Any suggestions?
Thanks,
Rich
Here's the new WebInvocationPrivilegeEvaluator:
Here's the custom DispatcherServlet:Code:/** * A WebInvocationPrivilegeEvaluator which defers to the annotation-driven * MethodSecurityInterceptor instead of to the pattern-driven FilterInvocation * security interceptors. */ public class AnnotationAwareWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { @Autowired private MethodSecurityInterceptor security; @Autowired private ApplicationContext applicationContext; // Note: not autowired private VisibleDispatcherServlet dispatcherServlet; private VisibleDispatcherServlet getDispatcherServlet() { // When this bean is first constructed, the DispatcherServlet is not yet // registered in the application context. By the time we get called to evaluate // a privilege, it will be. if (dispatcherServlet == null) { dispatcherServlet = applicationContext.getBean(VisibleDispatcherServlet.class); } return dispatcherServlet; } @Override public boolean isAllowed(String uri, Authentication authentication) { return isAllowed(null, uri, null, authentication); } @Override public boolean isAllowed( String contextPath, String uri, String method, Authentication authentication) { if (authentication == null) { return false; } // Create a request which is sufficiently realistic to fool the // handler matchers in the Dispatcher DummyRequest2 request = new DummyRequest2(); request.setContextPath(contextPath); request.setRequestURI(contextPath + uri); request.setMethod(StringUtils.defaultIfEmpty(method, "GET")); HandlerExecutionChain handler = getDispatcherServlet().getHandler(request); if (handler != null) { final HandlerMethod hMethod = (HandlerMethod) handler.getHandler(); MethodInvocation mi = new MethodInvocation() { @Override public Object proceed() throws Throwable { return null; // do nothing } @Override public Object getThis() { return hMethod.getBean(); } @Override public AccessibleObject getStaticPart() { return null; } @Override public Object[] getArguments() { return null; } @Override public Method getMethod() { return hMethod.getMethod(); } }; try { security.invoke(mi); } catch (AccessDeniedException e) { return false; } catch (RuntimeException e) { throw e; } catch (Throwable t) { throw new RuntimeException(t); } return true; } return true; } }
Here's the modified DummyRequest class (note that it needs to be in the Spring Security package, as the base class is package-private):Code:/** * A DispatcherServlet that registers itself in the application context, * so you can find it from beans which require a reference to the Dispatcher. * * Note that autowiring often won't work, as we can only register this * bean in the application context after most other beans have been autowired. * * However, you will be able to do appCtx.getBean(DispatcherServlet.class) */ public class VisibleDispatcherServlet extends DispatcherServlet { @Override protected void initStrategies(ApplicationContext context) { super.initStrategies(context); XmlWebApplicationContext parent = (XmlWebApplicationContext) context.getParent(); ConfigurableListableBeanFactory bf = parent.getBeanFactory(); bf.registerSingleton("dispatcherServlet", this); } @Override public HandlerExecutionChain getHandler(HttpServletRequest request) { try { return super.getHandler(request); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } }
Code:package org.springframework.security.web; public class DummyRequest2 extends DummyRequest { @Override public Object getAttribute(String name) { return null; } @Override public void setAttribute(String name, Object o) { // ignore } @Override public String getCharacterEncoding() { return "utf8"; } @Override public String getServletPath() { return ""; } @Override public String getParameter(String name) { return null; } }


Reply With Quote
).