SpringSource has recently announced that they renamed AcegiSecurity
as SpringSecurity
, and are preparing for a major
release which will be called 2.0
. Actually, its first milestone release is already available for download. According
to Ben Alex, there are various enhancements to bean configurations and new features introduced such as hierarchical
roles, etc.
After the latest news from Spring Security side, let’s return to my current issue. Spring Security, which is highly
dependent on Servlet Filters, uses FilterSecurityInterceptor
to protect web resources. We simply provide URL
pattern – role mappings during bean configuration. For example;
<bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager" />
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager" />
</property>
<property name="alwaysReauthenticate">
<value>${security.auth.alwaysReauthenticate}</value>
</property>
<property name="objectDefinitionSource">
<value>
A.*index.*Z=ROLE_READER,ROLE_WRITER
A.*reader.faces.*Z=ROLE_READER
A.*writer.faces.*Z=ROLE_WRITER
A.*Z=ROLE_ANONYMOUS,ROLE_READER,ROLE_WRITER
</value>
</property>
</bean>
FilterSecurityInterceptor
has an objectDefinitionSource
property to which we provide those URL pattern – role mappings.
The type of objectDefinitionSource
property is FilterInvocationDefinitionSource
, and AcegiSecurity
provides a default
property editor (FilterInvocationDefinitionSourceEditor
), which is located in the same package with
FilterInvocationDefinitionSource
class. Spring invokes property editors during application context startup and obtains
target bean instances from some textual input in bean definition.
One of the limitations of the above approach is that it is difficult to package application context configurations with
security enabled into separate deployment units and reuse them in several applications. One way to overcome this limitation
would be to provide a default bean configuration of FilterSecurityInterceptor
and override it in your application’s
bean configuration and provide these mappings in that overriding bean definition.
Another limitation is that by defining URL pattern – role mappings in XML files, you won’t have any chance of adding new
or changing existing mappings in FilterInvocationDefinitionSource
bean. You have to restart your application context
each time you make a change to those mappings, so that your changes to objectDefinitionSource
property value can be
read and a new FilterInvocationDefinitionSource
bean is constructed again.
It would be very nice if we could externalize those mappings by moving them out of XML files. For example, we can create
a filterInvocationDefinitions.properties
file and put those mappings in it. By that way, we can place those bean
configurations in a separate JAR and reuse them in other web applications. We also won’t need to override
filterSecurityInterceptor
bean to define application-specific mappings, but only make changes to the properties file.
Moving objectDefinitionSource
values out of XML alone won’t enable us to reload mappings during runtime, but it
definitely opens up such a possibility. However, I will focus on the externalization process at the moment. Reloading
URL – role mappings during runtime is another day’s issue.
In order to externalize mappings, we need a way to process that filterInvocationDefinitions.properties
file and create
a FilterInvocationDefinitionSource
instance to inject into filterSecurityInterceptor
bean. Yes, the answer is very
simple: We can implement a FactoryBean
which reads up properties file and creates a FilterInvocationDefinitionSource
bean. For example;
<bean id="filterInvocationDefinitionSource" class="org.ems4j.security.intercept.web.ResourceFilterInvocationDefinitionSourceFactoryBean">
<property name="filterInvocationDefinitions" value="classpath:/resources/filterInvocationDefinitions.properties" />
</bean>
ResourceFilterInvocationDefinitionSourceFactoryBean
gets filterInvocationDefinitions.properties
in the form of
Spring’s Resource
type, processes contents, and creates a FilterInvocationDefinitionSource
bean. After such a
FactoryBean
configuration, our filterSecurityInterceptor
bean’s objectDefinitionSource
configuration needs to be
changed slightly;
<property name="objectDefinitionSource">
<ref local="filterInvocationDefinitionSource"/>
</property>
Let’s look at the inside of ResourceFilterInvocationDefinitionSourceFactoryBean
closely. Acegi
supports two different
types of FilterInvocationDefinitionSource
, namely PathBasedFilterInvocationDefinitionMap
(for ant-style patterns in
URLs) and RegExpBasedFilterInvocationDefinitionMap
(for regular expression-enabled URLs) which is by default. Previously
we could tell the property editor to instantiate PathBased
version with PATTERN_TYPE_APACHE_ANT
directive on top of
our mappings. Another directive is CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
to tell Acegi
that we want to lowercase
all URLs before doing any comparison during authorization of web resources. We can represent those two options as boolean
in our FactoryBean
.
public class ResourceFilterInvocationDefinitionSourceFactoryBean extends AbstractFactoryBean {
private boolean patternTypeApacheAnt = false;
private boolean convertUrlToLowercaseBeforeComparison = false;
In createInstance()
method of FactoryBean
, we instantiate one of those types mentioned above and wrap it with
FilterInvocationDefinitionDecorator
which is also a subtype of FilterInvocationDefinitionSource
, and is used for
lowercase URL conversion.
protected Object createInstance() throws Exception {
BufferedReader reader = null;
try {
FilterInvocationDefinitionDecorator decorator = new FilterInvocationDefinitionDecorator(
patternTypeApacheAnt ? new PathBasedFilterInvocationDefinitionMap() : new RegExpBasedFilterInvocationDefinitionMap());
decorator.setConvertUrlToLowercaseBeforeComparison(this.convertUrlToLowercaseBeforeComparison);
List mappings = new ArrayList();
reader = new BufferedReader(new InputStreamReader(
filterInvocationDefinitions.getInputStream(), "utf-8"));
String line = reader.readLine();
while (line != null) {
String name = StringSplitUtils.substringBeforeLast(line, "=");
String value = StringSplitUtils.substringAfterLast(line, "=");
We then read contents of filterInvocationDefinitions.properties
line by line and create FilterInvocationDefinitionSourceMapping
for each URL pattern-role list mapping. Roles are added into those mapping objects as ConfigAttribute
elements.
It is important to read the properties file line by line because Acegi
compares the current URL against URL patterns in
the order of definition. Comparison stops at the first match.
FilterInvocationDefinitionSourceMapping mapping = new FilterInvocationDefinitionSourceMapping();
mapping.setUrl(name);
String[] tokens = StringUtils.commaDelimitedListToStringArray(value);
for (int i = 0; i < tokens.length; i++) {
mapping.addConfigAttribute(tokens[i].trim());
}
mappings.add(mapping);
line = reader.readLine();
}
decorator.setMappings(mappings);
return decorator.getDecorated();
} finally {
try {
if (reader != null) reader.close();
} catch (Exception ex) {
// do nothing...
}
}
}
}
In our case, we moved mappings into a file. It is equally possible to move them into a database as well. You can also provide a user interface to manage your protected web resources and their accessibilities.