在我之前的一篇文章里, 说明了在 Acegi 中如何将资源权限数据存储到数据库中, 文章见 http://www.hibernate.org.cn/viewtopic.php?t=17538,
虽然文中方式实现了从数据库读取资源权限, 但是代码量较大, 并且重载了 SecurityEnforcementFilter, 造成比较大的侵入性,
这里我将提供另一种更简洁的方式实现此功能.
入口还是 org.acegisecurity.intercept.web.FilterSecurityInterceptor, 资源权限配置来自于
objectDefinitionSource, 标准配置方式采用 FilterInvocationDefinitionSourceEditor 解析, 支持 Perl5 和 AntPath 两种风格,
为了实现从其它位置(典型如数据库), 我们要做的就是实现一个自定义的 FilterInvocationDefinitionSource, 查看类层次结构图后可以发现,
acegi 中已经有一个 AbstractFilterInvocationDefinitionSource 已经实现此接口, 只要实现一个抽象方法即可
代码
public abstract ConfigAttributeDefinition lookupAttributes(String url);
因此, 自定义一个 Class 如下 :
代码
public class RdbmsBasedFilterInvocationDefinitionSource extends AbstractFilterInvocationDefinitionSource
因为 acegi 中已经提供了 Perl5 和 AntPath 的实现, 只需要集成过来即可, 因此定义接口如下
代码
/**
* <class>ConfigableFilterInvocationDefinition</class> 支持 Perl5 和 ant Path 两种风格的资源配置方式
* @since 2006-1-19
* @author 王政
* @version $Id: ConfigableFilterInvocationDefinition.java,v 1.3 2006/01/19 09:40:37 wz Exp $
*/
public interface ConfigableFilterInvocationDefinition {
/** The Perl5 expression */
String PERL5_KEY = "PATTERN_TYPE_PERL5";
/** The ant path expression */
String ANT_PATH_KEY = "PATTERN_TYPE_APACHE_ANT";
/** 标准分隔符 */
String STAND_DELIM_CHARACTER = ",";
/**
* Set resource expression, the value must be {@link #PERL5_KEY_REG_EXP} or {@link #ANT_PATH_KEY}
* @see #REOURCE_EXPRESSION_PERL5_REG_EXP
* @see #RESOURCE_EXPRESSION_ANT_PATH_KEY
* @param resourceExpression the resource expression
*/
void setResourceExpression(String resourceExpression);
/**
*
* @return resource expression
*/
String getResourceExpression();
/**
* Set whether convert url to lowercase before comparison
* @param convertUrlToLowercaseBeforeComparison whether convertUrlToLowercaseBeforeComparison
*/
void setConvertUrlToLowercaseBeforeComparison(boolean convertUrlToLowercaseBeforeComparison);
/**
*
* @return whether convert url to lowercase before comparison
*/
boolean isConvertUrlToLowercaseBeforeComparison();
}
再让 RdbmsBasedFilterInvocationDefinitionSource 实现此接口即可, 下面是简略代码
代码
/**
* <class>RdbmsBasedFilterInvocationDefinitionSource</class> 是基于数据库的权限存储实现, 它支持两种风格的配置
* @see com.skyon.uum.security.acegi.intercept.web.ConfigableFilterInvocationDefinition
* @see org.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap
* @see org.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap
* @since 2006-1-19
* @author 王政
* @version $Id: RdbmsBasedFilterInvocationDefinitionSource.java,v 1.6 2006/02/13 03:20:55 wz Exp $
*/
public class RdbmsBasedFilterInvocationDefinitionSource extends AbstractFilterInvocationDefinitionSource
implements ConfigableFilterInvocationDefinition, InitializingBean {
//~ Static fields/initializers =============================================
private static final Log logger = LogFactory.getLog(RdbmsBasedFilterInvocationDefinitionSource.class);
// ~ Instance fields ========================================================
private String resourceExpression = PERL5_KEY;
private boolean convertUrlToLowercaseBeforeComparison = false;
private ResourceMappingProvider resourceMappingProvider;
// ~ Methods ================================================================
/**
*
* @see org.acegisecurity.intercept.web.AbstractFilterInvocationDefinitionSource#lookupAttributes(java.lang.String)
*/
public ConfigAttributeDefinition lookupAttributes(String url) {
FilterInvocationDefinitionSource actualSource = populateFilterInvocationDefinitionSource();
if (RegExpBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((RegExpBasedFilterInvocationDefinitionMap) actualSource).lookupAttributes(url);
} else if (PathBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((PathBasedFilterInvocationDefinitionMap) actualSource).lookupAttributes(url);
}
throw new IllegalStateException("wrong type of " + actualSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class);
}
/**
*
* @see org.acegisecurity.intercept.ObjectDefinitionSource#getConfigAttributeDefinitions()
*/
public Iterator getConfigAttributeDefinitions() {
FilterInvocationDefinitionSource actualSource = populateFilterInvocationDefinitionSource();
if (RegExpBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((RegExpBasedFilterInvocationDefinitionMap) actualSource).getConfigAttributeDefinitions();
} else if (PathBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((PathBasedFilterInvocationDefinitionMap) actualSource).getConfigAttributeDefinitions();
}
throw new IllegalStateException("wrong type of " + actualSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class);
}
private FilterInvocationDefinitionSource populateFilterInvocationDefinitionSource() {
FilterInvocationDefinitionMap definitionSource = null;
if (PERL5_KEY.equals(getResourceExpression())) {
definitionSource = new RegExpBasedFilterInvocationDefinitionMap();
} else if (ANT_PATH_KEY.equals(getResourceExpression())) {
definitionSource = new PathBasedFilterInvocationDefinitionMap();
} else {
throw new IllegalArgumentException("wrong resourceExpression value");
}
definitionSource.setConvertUrlToLowercaseBeforeComparison(isConvertUrlToLowercaseBeforeComparison());
ResourceMapping[] mappings = getResourceMappingProvider().getResourceMappings();
if (mappings == null || mappings.length ==0) {
return (FilterInvocationDefinitionSource) definitionSource;
}
for (int i = 0; i < mappings.length; i++) {
ResourceMapping mapping = mappings[i];
String[] recipents = mapping.getRecipients();
if (recipents == null || recipents.length == 0) {
if (logger.isErrorEnabled()) {
logger.error("Notice, the resource : " + mapping.getResourcePath() + " hasn't no recipents, it will access by any one ! ");
}
continue;
}
StringBuffer valueBuffer = new StringBuffer();
for (int j = 0; j < recipents.length; j++) {
valueBuffer.append(recipents[j]);
if (j < recipents.length - 1) {
valueBuffer.append(STAND_DELIM_CHARACTER);
}
}
String value = valueBuffer.toString();
addSecureUrl(definitionSource, mapping.getResourcePath(), value);
}
return (FilterInvocationDefinitionSource )definitionSource;
}
/**
* @param source
* @param name
* @param value
* @throws IllegalArgumentException
*/
private synchronized void addSecureUrl(FilterInvocationDefinitionMap source, String name, String value)
throws IllegalArgumentException {
// Convert value to series of security configuration attributes
ConfigAttributeEditor configAttribEd = new ConfigAttributeEditor();
configAttribEd.setAsText(value);
ConfigAttributeDefinition attr = (ConfigAttributeDefinition) configAttribEd.getValue();
// Register the regular expression and its attribute
source.addSecureUrl(name, attr);
}
省去 getter, setter....
}
ResourceMappingProvider
代码
public interface ResourceMappingProvider {
String RESOURCE_PATH_PREFIX = "/";
/**
* Get Resource Mapping
* @return resource mapping
*/
ResourceMapping[] getResourceMappings();
}
public class ResourceMapping {
// url
private String resourcePath;
// 即角色
private String[] recipients = new String[0];
省去 getter, setter....
}
这样就很完美的既支持了从数据库的读取数据, 又可以自由选择 Perl5 和 AntPath 两种风格的配置, 最后不要忘了给 ResourceMappingProvider 加一层 cache, 我的配置如下
代码
<bean id="resourceCache" class="com.skyon.uum.security.acegi.intercept.web.cache.EhCacheBasedResourceCache">
<property name="dataSource">
<ref bean="dataSource"></ref>
</property>
<property name="cache">
<bean parent="cacheTemplate">
<property name="cacheName"><value>resourceCache</value></property>
</bean>
</property>
<property name="allResourcesQuery">
<value>
select distinct t.id, t.id, t.parent_id, t.url, t.title, t.layer, t.type, t.application_id
from uum_resource t order by t.orderField
</value>
</property>
</bean>
<bean id="permissionCache" class="com.skyon.uum.security.acegi.intercept.web.cache.EhCacheBasedPermissionCache">
<property name="dataSource">
<ref bean="dataSource"></ref>
</property>
<property name="cache">
<bean parent="cacheTemplate">
<property name="cacheName"><value>permissionCache</value></property>
</bean>
</property>
<property name="recipentsResourceMappingQuery">
<value>
select r.name, p.resource_id from uum_permission p left outer join uum_role r on p.role_id = r.id
</value>
</property>
</bean>
<bean id="resourceMappingProvider" class="com.skyon.uum.security.acegi.intercept.web.ResourceMappingProviderImpl" autowire="byType"/>
<!-- ======================== AUTHENTICATION ======================= -->
<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"><ref local="authenticationManager"/></property>
<property name="accessDecisionManager"><ref local="accessDecisionManager"/></property>
<property name="objectDefinitionSource"><ref local="filterInvocationDefinitionSource"></ref></property>
</bean>
<bean id="filterInvocationDefinitionSource" class="com.skyon.uum.security.acegi.intercept.web.RdbmsBasedFilterInvocationDefinitionSource">
<property name="resourceMappingProvider">
<ref local="resourceMappingProvider"></ref>
</property>
<property name="resourceExpression">
<value>PATTERN_TYPE_APACHE_ANT</value>
</property>
</bean>
这段时间看了很多人对 Acegi 的评价, 有不少观点认为 Acegi 的配置太过繁琐, 其实权限控制本来就不是一件很轻松的事, Acegi 用 AOP 实现, 配置文件的确有些繁琐,
但是只要一个配置文件就解决了整个系统的权限问题, 可谓一劳永逸, 相比较在 Action 中实现应该还是利远大于弊, 也有人说用 WebWork 的 Interceptor 实现, 虽然也是不错的 solution,
但是不要忘了, 并不是所有的项目都使用 webwork , 假如有一个 struts 的项目, 权限控制就会有移植性的问题.
虽然文中方式实现了从数据库读取资源权限, 但是代码量较大, 并且重载了 SecurityEnforcementFilter, 造成比较大的侵入性,
这里我将提供另一种更简洁的方式实现此功能.
入口还是 org.acegisecurity.intercept.web.FilterSecurityInterceptor, 资源权限配置来自于
objectDefinitionSource, 标准配置方式采用 FilterInvocationDefinitionSourceEditor 解析, 支持 Perl5 和 AntPath 两种风格,
为了实现从其它位置(典型如数据库), 我们要做的就是实现一个自定义的 FilterInvocationDefinitionSource, 查看类层次结构图后可以发现,
acegi 中已经有一个 AbstractFilterInvocationDefinitionSource 已经实现此接口, 只要实现一个抽象方法即可
代码
public abstract ConfigAttributeDefinition lookupAttributes(String url);
因此, 自定义一个 Class 如下 :
代码
public class RdbmsBasedFilterInvocationDefinitionSource extends AbstractFilterInvocationDefinitionSource
因为 acegi 中已经提供了 Perl5 和 AntPath 的实现, 只需要集成过来即可, 因此定义接口如下
代码
/**
* <class>ConfigableFilterInvocationDefinition</class> 支持 Perl5 和 ant Path 两种风格的资源配置方式
* @since 2006-1-19
* @author 王政
* @version $Id: ConfigableFilterInvocationDefinition.java,v 1.3 2006/01/19 09:40:37 wz Exp $
*/
public interface ConfigableFilterInvocationDefinition {
/** The Perl5 expression */
String PERL5_KEY = "PATTERN_TYPE_PERL5";
/** The ant path expression */
String ANT_PATH_KEY = "PATTERN_TYPE_APACHE_ANT";
/** 标准分隔符 */
String STAND_DELIM_CHARACTER = ",";
/**
* Set resource expression, the value must be {@link #PERL5_KEY_REG_EXP} or {@link #ANT_PATH_KEY}
* @see #REOURCE_EXPRESSION_PERL5_REG_EXP
* @see #RESOURCE_EXPRESSION_ANT_PATH_KEY
* @param resourceExpression the resource expression
*/
void setResourceExpression(String resourceExpression);
/**
*
* @return resource expression
*/
String getResourceExpression();
/**
* Set whether convert url to lowercase before comparison
* @param convertUrlToLowercaseBeforeComparison whether convertUrlToLowercaseBeforeComparison
*/
void setConvertUrlToLowercaseBeforeComparison(boolean convertUrlToLowercaseBeforeComparison);
/**
*
* @return whether convert url to lowercase before comparison
*/
boolean isConvertUrlToLowercaseBeforeComparison();
}
再让 RdbmsBasedFilterInvocationDefinitionSource 实现此接口即可, 下面是简略代码
代码
/**
* <class>RdbmsBasedFilterInvocationDefinitionSource</class> 是基于数据库的权限存储实现, 它支持两种风格的配置
* @see com.skyon.uum.security.acegi.intercept.web.ConfigableFilterInvocationDefinition
* @see org.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap
* @see org.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap
* @since 2006-1-19
* @author 王政
* @version $Id: RdbmsBasedFilterInvocationDefinitionSource.java,v 1.6 2006/02/13 03:20:55 wz Exp $
*/
public class RdbmsBasedFilterInvocationDefinitionSource extends AbstractFilterInvocationDefinitionSource
implements ConfigableFilterInvocationDefinition, InitializingBean {
//~ Static fields/initializers =============================================
private static final Log logger = LogFactory.getLog(RdbmsBasedFilterInvocationDefinitionSource.class);
// ~ Instance fields ========================================================
private String resourceExpression = PERL5_KEY;
private boolean convertUrlToLowercaseBeforeComparison = false;
private ResourceMappingProvider resourceMappingProvider;
// ~ Methods ================================================================
/**
*
* @see org.acegisecurity.intercept.web.AbstractFilterInvocationDefinitionSource#lookupAttributes(java.lang.String)
*/
public ConfigAttributeDefinition lookupAttributes(String url) {
FilterInvocationDefinitionSource actualSource = populateFilterInvocationDefinitionSource();
if (RegExpBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((RegExpBasedFilterInvocationDefinitionMap) actualSource).lookupAttributes(url);
} else if (PathBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((PathBasedFilterInvocationDefinitionMap) actualSource).lookupAttributes(url);
}
throw new IllegalStateException("wrong type of " + actualSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class);
}
/**
*
* @see org.acegisecurity.intercept.ObjectDefinitionSource#getConfigAttributeDefinitions()
*/
public Iterator getConfigAttributeDefinitions() {
FilterInvocationDefinitionSource actualSource = populateFilterInvocationDefinitionSource();
if (RegExpBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((RegExpBasedFilterInvocationDefinitionMap) actualSource).getConfigAttributeDefinitions();
} else if (PathBasedFilterInvocationDefinitionMap.class.isInstance(actualSource)) {
return ((PathBasedFilterInvocationDefinitionMap) actualSource).getConfigAttributeDefinitions();
}
throw new IllegalStateException("wrong type of " + actualSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
+ " or " + PathBasedFilterInvocationDefinitionMap.class);
}
private FilterInvocationDefinitionSource populateFilterInvocationDefinitionSource() {
FilterInvocationDefinitionMap definitionSource = null;
if (PERL5_KEY.equals(getResourceExpression())) {
definitionSource = new RegExpBasedFilterInvocationDefinitionMap();
} else if (ANT_PATH_KEY.equals(getResourceExpression())) {
definitionSource = new PathBasedFilterInvocationDefinitionMap();
} else {
throw new IllegalArgumentException("wrong resourceExpression value");
}
definitionSource.setConvertUrlToLowercaseBeforeComparison(isConvertUrlToLowercaseBeforeComparison());
ResourceMapping[] mappings = getResourceMappingProvider().getResourceMappings();
if (mappings == null || mappings.length ==0) {
return (FilterInvocationDefinitionSource) definitionSource;
}
for (int i = 0; i < mappings.length; i++) {
ResourceMapping mapping = mappings[i];
String[] recipents = mapping.getRecipients();
if (recipents == null || recipents.length == 0) {
if (logger.isErrorEnabled()) {
logger.error("Notice, the resource : " + mapping.getResourcePath() + " hasn't no recipents, it will access by any one ! ");
}
continue;
}
StringBuffer valueBuffer = new StringBuffer();
for (int j = 0; j < recipents.length; j++) {
valueBuffer.append(recipents[j]);
if (j < recipents.length - 1) {
valueBuffer.append(STAND_DELIM_CHARACTER);
}
}
String value = valueBuffer.toString();
addSecureUrl(definitionSource, mapping.getResourcePath(), value);
}
return (FilterInvocationDefinitionSource )definitionSource;
}
/**
* @param source
* @param name
* @param value
* @throws IllegalArgumentException
*/
private synchronized void addSecureUrl(FilterInvocationDefinitionMap source, String name, String value)
throws IllegalArgumentException {
// Convert value to series of security configuration attributes
ConfigAttributeEditor configAttribEd = new ConfigAttributeEditor();
configAttribEd.setAsText(value);
ConfigAttributeDefinition attr = (ConfigAttributeDefinition) configAttribEd.getValue();
// Register the regular expression and its attribute
source.addSecureUrl(name, attr);
}
省去 getter, setter....
}
ResourceMappingProvider
代码
public interface ResourceMappingProvider {
String RESOURCE_PATH_PREFIX = "/";
/**
* Get Resource Mapping
* @return resource mapping
*/
ResourceMapping[] getResourceMappings();
}
public class ResourceMapping {
// url
private String resourcePath;
// 即角色
private String[] recipients = new String[0];
省去 getter, setter....
}
这样就很完美的既支持了从数据库的读取数据, 又可以自由选择 Perl5 和 AntPath 两种风格的配置, 最后不要忘了给 ResourceMappingProvider 加一层 cache, 我的配置如下
代码
<bean id="resourceCache" class="com.skyon.uum.security.acegi.intercept.web.cache.EhCacheBasedResourceCache">
<property name="dataSource">
<ref bean="dataSource"></ref>
</property>
<property name="cache">
<bean parent="cacheTemplate">
<property name="cacheName"><value>resourceCache</value></property>
</bean>
</property>
<property name="allResourcesQuery">
<value>
select distinct t.id, t.id, t.parent_id, t.url, t.title, t.layer, t.type, t.application_id
from uum_resource t order by t.orderField
</value>
</property>
</bean>
<bean id="permissionCache" class="com.skyon.uum.security.acegi.intercept.web.cache.EhCacheBasedPermissionCache">
<property name="dataSource">
<ref bean="dataSource"></ref>
</property>
<property name="cache">
<bean parent="cacheTemplate">
<property name="cacheName"><value>permissionCache</value></property>
</bean>
</property>
<property name="recipentsResourceMappingQuery">
<value>
select r.name, p.resource_id from uum_permission p left outer join uum_role r on p.role_id = r.id
</value>
</property>
</bean>
<bean id="resourceMappingProvider" class="com.skyon.uum.security.acegi.intercept.web.ResourceMappingProviderImpl" autowire="byType"/>
<!-- ======================== AUTHENTICATION ======================= -->
<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"><ref local="authenticationManager"/></property>
<property name="accessDecisionManager"><ref local="accessDecisionManager"/></property>
<property name="objectDefinitionSource"><ref local="filterInvocationDefinitionSource"></ref></property>
</bean>
<bean id="filterInvocationDefinitionSource" class="com.skyon.uum.security.acegi.intercept.web.RdbmsBasedFilterInvocationDefinitionSource">
<property name="resourceMappingProvider">
<ref local="resourceMappingProvider"></ref>
</property>
<property name="resourceExpression">
<value>PATTERN_TYPE_APACHE_ANT</value>
</property>
</bean>
这段时间看了很多人对 Acegi 的评价, 有不少观点认为 Acegi 的配置太过繁琐, 其实权限控制本来就不是一件很轻松的事, Acegi 用 AOP 实现, 配置文件的确有些繁琐,
但是只要一个配置文件就解决了整个系统的权限问题, 可谓一劳永逸, 相比较在 Action 中实现应该还是利远大于弊, 也有人说用 WebWork 的 Interceptor 实现, 虽然也是不错的 solution,
但是不要忘了, 并不是所有的项目都使用 webwork , 假如有一个 struts 的项目, 权限控制就会有移植性的问题.