In my current project we decided to use Spring security to replace somewhat hacky authorization we use now. All controllers inherit from AbstractController, that overrides handleRequestInternal:
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
String methodName = getMethodNameResolver().getHandlerMethodName(request);
Method method = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
Permissioned annotation = method.getAnnotation(Permissioned.class);
if (annotation != null) {
String action = annotation.action();
String resource = annotation.resource();
String userName = request.getHeader("USERNAME");
if (!userService.isAllowed(userName, resource, action)) {
throw new ControllerAccessDenied("User not allowed");
}
}
return invokeNamedMethod(methodName, request, response);
} catch (NoSuchRequestHandlingMethodException ex) {
return handleNoSuchRequestHandlingMethod(ex, request, response);
}
}
It is of course not a true code taken from the project but gives you the idea of the approach. All methods on the controller that are supposed to be secured are annotated like this:
@Secured("administrator")
@Permissioned(action = "View", resource = "Video")
public ModelAndView doSomething(HttpServletRequest request, HttpServletResponse response) {
command.doSomething();
return new ModelAndView(...);
}
This is the authorization part. As for authentication, it is handled externally and at the point of reaching the controller we only care about the USERNAME header.
Implementing similar thing using spring security seemed relatively easy. The plan was to add @Secured annotation where our @Permissioned was used and then handle the logic in a voter.
First we needed to add standard stuff to web.xml:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Then we created security-context.xml with the following contents:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<security:global-method-security secured-annotations="enabled" access-decision-manager-ref="accessDecisionManager" />
<bean id="accessDecisionManager" class="org.springframework.security.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="simpleVoter" />
</list>
</property>
<property name="allowIfAllAbstainDecisions" value="true" />
</bean>
<security:http entry-point-ref="preAuthenticatedProcessingFilterEntryPoint">
<security:intercept-url pattern="/*" access="IS_AUTHENTICATED_FULLY" />
</security:http>
<bean id="preAuthenticatedProcessingFilterEntryPoint" class="org.springframework.security.ui.preauth.PreAuthenticatedProcessingFilterEntryPoint" />
<bean id="requestHeaderProcessingFilter" class="org.springframework.security.ui.preauth.header.RequestHeaderPreAuthenticatedProcessingFilter">
<security:custom-filter position="PRE_AUTH_FILTER" />
<property name="principalRequestHeader" value="USERNAME" />
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="preAuthenticatedAuthenticationProvider" class="org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationProvider">
<security:custom-authentication-provider />
<property name="preAuthenticatedUserDetailsService" ref="userService" />
</bean>
<bean id="simpleVoter" class="security.Voter" />
<security:authentication-manager alias="authenticationManager" />
</beans>
A little explanation: security:global-method-security makes spring pick up @Secured annotations and... well.. secure annottated methods. Access decision manager contains the list of voters that will make a decision on access, security:http is the usual stuff explained on 100 different websites. The only other interesting bit is the *PreAuthenticated* thing, which allows us to ignore the authentication phase assuming that we've done it in other place. Our userService only provides UserDetails object which will be injected into security context and then used e.g. by voters.
Controllers context looked like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="simpleController" class="controllers.SimpleController" />
<bean id="simpleCommand" class="commands.SimpleCommand" />
<bean id="userService" class="security.UserServiceImpl" />
</beans>
Additionally we needed dispatcher-servlet.xml which contained just urls mappings:
<bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/s1/*">simpleController</prop>
</props>
</property>
</bean>
I've put @Security annotation on controller, removed the old overridden method. Tomcat started fine but I was still allowed to enter the method even though every call should be rejected.
(An hour later...) The reason for that was simple. We annotated controllers methods. Since they are written in the 'old style', extending spring controllers, every call reaches our code through handleRequest method. Thus it becomes an internal call and obviously in Spring Aop such calls are no longer intercepted (they don't go through a proxy). The solution in this case was simply to use spring security on the service level rather than controller. However, it does not completely replace our previous solution and therefore I will investigate if there is a workaround.
No comments:
Post a Comment