In a webservice application I am currently working on, users are identified by a userId. Like any typical webservice, each request to the service needs to have the userId parameter tacked on to the request parameters. The application has a fairly sophisticated, but home-grown authentication and authorization mechanism that it inherited from the previous version of the application. Since I was using a new architecture and new libraries anyway for this application, I figured that I would give Acegi, the popular Spring based security framework, a try. I ultimately decided to not use Acegi, but I describe here the new classes and configuration I needed to build to integrate my application with Acegi, why I ended up not using it, and what parts of Acegi I decided to reimplement in my code.
The original application
The webservice is accessed via HTTP GET calls. It returns XML (or JSON) as a response. The webservice application carries out the following authentication checks. In all cases, if the check fails, a HTTP Forbidden status code of 403 with an appropriate error message is returned.
- Is the userId supplied in the request parameters? A userId request parameter must be specified in the request.
- If supplied, is it valid? We do a match against our user database to find out.
- If valid, is it coming from an IP address for which the user is registered? When the user is registered in the system, zero or more subnet masks are also registered. If no subnet masks are registered, then this authentication step is skipped. Otherwise, the user's IP address must match one of the registered subnet masks for the user.
Once these checks are done, the following authorization checks are made. As before, if the check fails, a HTTP Forbidden status code of 403 with an appropriate error message is returned.
- Is the user authorized to make the webservice method call he is making? The user is authorized to one or more webservice methods in the user database.
- Is the user authorized to any explicitly requested content? A method may return one or more response elements. If the user specifically requests one or more elements in the request, then this check is made.
- During the time the response is generated, each element is checked against the user's content authorization, and if the user is not authorized for the content, generation of the element is skipped.
The webservice is written using the Spring Framework. The first 5 checks are done by a Spring HandlerInterceptor which intercepts all method calls to the Controller object that the DispatcherServlet delegates to. If any of these fail, the response is sent to an error page with the appropriate HTTP Status code (403).
The sixth check is done in my ResponseGenerator class. The Controller delegates to a suitably configured ResponseGenerator based on the webservice method requested. The ResponseGenerator itself is simply a delegator that takes a chain of ResponseElementGenerator objects and defines a strategy for processing these objects. The chain is injected into the ResponseGenerator using Spring configuration. Because of this, the authorization code only occurs once in the application.
As you can see, the code is fairly well structured (most of the credit goes to my predecessor who structured the first 5 authentication checks in a Servlet Filter implementation in a previous non-Spring version of the application). However, I had heard a lot about Acegi, most recently from an ex-colleague who was actually integrating it into one of his client's applications, so I decided that I should at least check it out.
Changes needed for Acegi
With Acegi, I would have to configure a FilterToBeanProxy as a filter in my web.xml file, which would proxy a FilterChainProxy in my Spring application context. The FilterChainProxy would be configured with a custom Filter implementation that would extract and authenticate the user's request and use a configured custom AuthenticationManager that would actually retrieve the user by name using a custom UserDetailsService into a custom UserDetails bean. As you can see, quite a lot of custom work here. The filter declaration in the web.xml is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | <!-- web.xml -->
<web-app ...>
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
|
The supporting beans for the Acegi FilterChainProxy are defined in the Spring application context below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | <beans ...>
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=requestProcessingFilter
</value>
</property>
</bean>
<bean id="requestProcessingFilter" class="com.mycompany.myapp.MyRequestProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="authenticationProvider"/>
</list>
</property>
</bean>
<bean id="authenticationProvider" class="com.mycompany.myapp.MyAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService"/>
<property name="userCache" ref="userCache"/>
</bean>
<bean id="userDetails" class="com.mycompany.myapp.MyUserDetails"/>
<bean id="userDetailsService" class="com.mycompany.myapp.MyUserDetailsService">
<!-- reference to a DAO to retrieve users from db -->
<property name="userDao" ref="userDao"/>
</bean>
</beans>
|
The code for the various classes referenced above are shown below. The filterChainProxy proxies the bean named requestProcessingFilter, which is in the Spring applicationContext, and which therefore can be injected with the AuthorityManager. The requestProcessingFilter pulls the userId out of the request parameters and creates a seed Authentication token with the principal set to the userId. Then it delegates to the AuthenticationProvider which returns an Authentication token populated with the UserDetails object corresponding to the userId. This is followed by a series of authentication checks (1-5).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | // MyRequestProcessingFilter.java
package com.mycompany.myapp;
public class MyRequestProcessingFilter implements Filter {
private AuthenticationProvider authenticationProvider;
@Required
public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
public void init(FilterConfig filterConfig) throws ServletException { /* NOOP */ }
public void destroy() { /* NOOP */ }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String userId = ServletRequestUtils.getRequiredStringParameter(request, "userId");
Authentication authentication = new UsernamePasswordAuthenticationToken(userId, userId);
authentication = authenticationProvider.authenticate(authentication);
if (authentication.getPrincipal() instanceof UserDetails) {
// this is the only place in the code where we have access to the
// request and remote IP address, so do the additional checks here
MyUserDetails myuserdetails = (MyUserDetails) authentication.getPrincipal();
if (! myuserdetails.isAuthorizedForSubnet(request.getRemoteAddress())) {
SecurityContextHolder.getContext().setAuthentication(null);
}
if (! myuserdetails.isAuthorizedForService(getServiceFromRequest(request))) {
SecurityContextHolder.getContext().setAuthentication(null);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
SecurityContextHolder.getContext().setAuthentication(null);
}
chain.doFilter(request, response);
}
}
|
The AuthenticationProvider in turn delegates to the UserDetailsService to load the user from the database. If a user is not found, then the AuthenticationProvider returns the original Authentication object passed in.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | // MyAuthenticationProvider.java
public class MyAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Required
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
UserDetails userdetails = userDetailsService.loadUserByUsername((String) principal);
if (userdetails != null) {
Authentication usernamePasswordAuthToken = new UsernamePasswordAuthenticationToken(userdetails, userdetails);
usernamePasswordAuthToken.setAuthenticated(true);
return usernamePasswordAuthToken;
}
}
return authentication;
}
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
|
The UserDetails service delegates to an application specific UserDao that returns the UserDetails object. In reality, this returns an User object and is passed through some adapter code to convert this to a UserDetails object, but I omitted that for brevity.
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | // MyUserDetailsService.java
public class MyUserDetailsService extends JdbcDaoSupport implements UserDetailsService {
private UserDao userDao;
@Required
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException, DataAccessException {
return userDao.getUserById(userId);
}
}
|
The MyUserDetails.java class internally stores the GrantedAuthority objects as a Map of Set of Strings, so it can be used more efficiently to look up a user's authority for a specified service or content. It also returns an array of GrantedAuthority objects as per the interface's contract. I have not included the code for this as it is long and not relevant to the discussion.
1
2
3
4
5
6
7 | // MyUserDetails.java
public class MyUserDetails implements UserDetails {
private String username;
private Map<String,Set<String>> authorities = new HashMap<String,Set<String>>();
...
}
|
For the sixth check (content level authorization), the Acegi recommended approach is to use a MethodInterceptor to wrap each ResponseElementGenerator's generate() method and use a Spring AOP Proxy to inject into the ResponseGenerator. This approach, however, would add a lot of bulk to the applicationContext.xml file, since each ResponseElementGenerator implementation (and there are quite a few) would need to be declaratively wrapped into an AOP Proxy. It is far easier and more maintainable, though perhaps not as neat, to just have this logic inside the ResponseGenerator itself, which is the current approach.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | // ResponseGenerator.java
public class ResponseGenerator {
private List<IResponseElementGenerator> generators;
@Required
public void setResponseElementGenerators(List<IResponseElementGenerator> generators) {
this.generators = generators;
}
public List<? extends IResponse> generate(ParameterBean parameters, OpenSearchInfo openSearchInfo) throws Exception {
for (IResponseElementGenerator generator : generators) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// at this point, our authentication should already contain our
// UserDetails class
MyUserDetails userdetails = (MyUserDetails) authentication.getPrincipal();
if (! userdetails.isAuthorizedForContent(generator.getContentType())) {
// user not authorized for content, skip this generator
continue;
}
...
}
}
}
|
Why I ended up not using Acegi
After all this, I decided not to use Acegi after all. Although Acegi comes with a lot of built in components, none of the components seemed reusable for my application, which is why I ended up writing the custom implementations described above. At this point, I had written about as many lines of code for integrating Acegi as there were in the original security handling code in the application. Using Acegi in my application at this point would have been just for the sake of using it.
Adding Acegi features to my application
There were a few things that I saw in Acegi that I decided to implement in my application. They were the use of the static ThreadLocal class SecurityContextHolder to make the authenticated User object available throughout the application, and the built in caching of the User object.
My original application was using a reference to the UserDao inside the HandlerInterceptor as well as in the ResponseGenerator. I also needed it in some of the ResponseElementGenerator implementations, so there were lots of references to the userDao all over my applicationContext.xml file. Based on how the SecurityContextHolder works, I am now pulling in the User object in the HandlerInterceptor and putting it into a ThreadLocal called UserHolder. The ResponseGenerator, as well as the ResponseElementGenerators uses the UserHolder.get() call within the same request thread to retrieve the authenticated user from anywhere in the application.
The AbstractAuthenticationProvider class provides built in caching using EhCache. My current application had a home-grown in-memory cache of users which would get refreshed at a specific time interval, inherited from the previous version of the application. I was already using EhCache for caching webservice responses, so I decided to make my UserDao cached using EhCache with a fixed time-to-live setting.
When would I use Acegi
I think Acegi would have been a good choice if my application did not already have a security mechanism built in, or if the security mechanism it used was based on a username/password scheme, which Acegi has built-in components for. It would also have been a good choice if I was going to connect to one of the popular authentication engines that Acegi already contains connectors for. Because of the way it uses chains of AuthenticationProviders, it may also be a good choice if I was looking to change or add authentication mechanisms in the future.