Acegi 资源配置动态扩展实现
程序员文章站
2022-03-23 15:52:44
...
本文原出处 : http://starcraft.blogdriver.com/starcraft/1089862.html
Acegi 资源配置动态扩展实现
王 政 (Feiing) 于 2005-12-11
1. 问题提出
在使用 Acegi Security Framework 的过程中, 如果细心的话, 会发现其资源和角色配置是在配置文件中的, 下面是 Appfuse 中相关配置 :
上面的配置从功能上实现了资源与角色的映射, 但用户可能会提出在运行期动态改变权限分配的需求, 配置文件策略可能略显不足, 下面我将提供一种基于数据库的策略解决此问题.
2. E-R 模型
下图是需要的 E-R 模型
图1 Acegi 标准 RBAC E-R设计
图中的用户与角色不再多做解释, 我们主要关注一下 Permission 表 和 Resource 表, 这里 Resource 表用于存储系统资源, 在 web 层一般来说就是 url, 如果使用 acl, 就是 aclClass, 此时 Permission 表中的 aclMask 用来存储对应的 acl 权限, 考虑到 acl 在 web 项目中使用率不高, 下面我将着重介绍 web 层的权限控制, 对 acl 有兴趣的读者可以自己参阅 Acegi Reference Guide.
3. 如何阻止 acegi 从配置文件读取权限配置
从 Appfuse 中的示例性配置可以看出, acegi 对权限配置的要求是 “ 资源 = 角色1, 角色2 … 角色 n ”, 看过源代码的读者应该知道, 最终这些配置将被组装为 net.sf.acegisecurity.intercept. ObjectDefinitionSource(web 层对应的实现是 net.sf.acegisecurity.intercept.web. FilterInvocationDefinitionSource), 那么我们怎么才能用数据库的数据来组装 FilterInvocationDefinitionSource ? 这里涉及到一个 PropertyEditor 问题, 在 Acegi 中, FilterInvocationDefinitionSource 是通过 net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSourceEditor 组装的, 假如我们不想让 FilterInvocationDefinitionSourceEditor 从配置文件中读取权限配置, 就需要自己实现一个 ProdertyEditor 来覆盖默认实现, 下面是我的 配置 :
图2 customerEditorConfigurer 配置
那么, 这个 PropertyEditor 中需要做些什么呢 ? 要做的就是使用一个比较特殊的标记, 当遇到这个特殊标记的时候直接略过解析, 我这里使用的标记是 “DONT_USE_ME”, 然后在 PropertyEditor 中简单的如下实现即可:
Ok, 现在 FilterInvocationDefinitionSourceDynamicExtentionEditor 遇到配置文件中的 “DONT_USE_ME” 时将直接略过, 下面是我的 filterInvocationInterceptor 配置:
图3 filterInvocationInterceptor 配置
现在, 我们已经成功阻止 acegi 从配置文件读取权限配置, 下一个问题就是:
4. 如何从表中数据组装 FilterInvocationDefinitionSource
为了实现此功能, 需要一个自定义的资源定义接口来提供 FilterInvocationDefinitionSource, 此接口可能会是这样 :
其核心方法是 FilterInvocationDefinitionSource getFilterInvocationDefinitionSource(), 此方法将代替配置文件提供资源和角色的配置, 下面是实现
实现采用 EhCache 缓存资源权限配置, 这样如果资源权限数据发生变化, 可以 flush Cache 从数据库重新读取. 至于代码中的 ResourceMapingProvider 实现, 简单的把 Resource 表和 Role 表中的数据读取过来即可, 这里不再赘述.
5. 如何将数据库中的权限配置传递给 FilterInvocationInterceptor
完成以上步骤后, 最后一步就是如何把 FilterInvocationDefinitionSourceCache 中的 FilterInvocationDefinitionSource 传递给 FilterInvocationInterceptor, Simple implemention :
配置:
It’s Over Now.
本文引用代码及配置文件参见附件.[/url]
Acegi 资源配置动态扩展实现
王 政 (Feiing) 于 2005-12-11
1. 问题提出
在使用 Acegi Security Framework 的过程中, 如果细心的话, 会发现其资源和角色配置是在配置文件中的, 下面是 Appfuse 中相关配置 :
<bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"><ref local="authenticationManager"/></property> <property name="accessDecisionManager"><ref local="accessDecisionManager"/></property> <property name="objectDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /signup.html=ROLE_ANONYMOUS,admin,tomcat /clickstreams.jsp=admin </value> </property> </bean>
上面的配置从功能上实现了资源与角色的映射, 但用户可能会提出在运行期动态改变权限分配的需求, 配置文件策略可能略显不足, 下面我将提供一种基于数据库的策略解决此问题.
2. E-R 模型
下图是需要的 E-R 模型
图1 Acegi 标准 RBAC E-R设计
图中的用户与角色不再多做解释, 我们主要关注一下 Permission 表 和 Resource 表, 这里 Resource 表用于存储系统资源, 在 web 层一般来说就是 url, 如果使用 acl, 就是 aclClass, 此时 Permission 表中的 aclMask 用来存储对应的 acl 权限, 考虑到 acl 在 web 项目中使用率不高, 下面我将着重介绍 web 层的权限控制, 对 acl 有兴趣的读者可以自己参阅 Acegi Reference Guide.
3. 如何阻止 acegi 从配置文件读取权限配置
从 Appfuse 中的示例性配置可以看出, acegi 对权限配置的要求是 “ 资源 = 角色1, 角色2 … 角色 n ”, 看过源代码的读者应该知道, 最终这些配置将被组装为 net.sf.acegisecurity.intercept. ObjectDefinitionSource(web 层对应的实现是 net.sf.acegisecurity.intercept.web. FilterInvocationDefinitionSource), 那么我们怎么才能用数据库的数据来组装 FilterInvocationDefinitionSource ? 这里涉及到一个 PropertyEditor 问题, 在 Acegi 中, FilterInvocationDefinitionSource 是通过 net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSourceEditor 组装的, 假如我们不想让 FilterInvocationDefinitionSourceEditor 从配置文件中读取权限配置, 就需要自己实现一个 ProdertyEditor 来覆盖默认实现, 下面是我的 配置 :
图2 customerEditorConfigurer 配置
那么, 这个 PropertyEditor 中需要做些什么呢 ? 要做的就是使用一个比较特殊的标记, 当遇到这个特殊标记的时候直接略过解析, 我这里使用的标记是 “DONT_USE_ME”, 然后在 PropertyEditor 中简单的如下实现即可:
/* * Copyright 2004-2005 wangz. * Project shufe_newsroom */ package com.skyon.um.security.acegi.intercept.web; import java.beans.PropertyEditorSupport; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import net.sf.acegisecurity.ConfigAttributeDefinition; import net.sf.acegisecurity.ConfigAttributeEditor; import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionMap; import net.sf.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap; import net.sf.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * @since 2005-8-4 * @author 王政 * @version $Id: FilterInvocationDefinitionSourceDynamicExtentionEditor.java,v 1.2 2005/11/04 15:55:07 wangzheng Exp $ */ public class FilterInvocationDefinitionSourceDynamicExtentionEditor extends PropertyEditorSupport { public static final String ANT_PATH_KEY = "PATTERN_TYPE_APACHE_ANT"; public static final String LOWER_CASE_URL_KEY = "CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON"; public static final String DONT_USE_ME_KEY = "DONT_USE_ME"; public static final String STAND_DELIM_CHARACTER = ","; private static final Log logger = LogFactory.getLog(FilterInvocationDefinitionSourceDynamicExtentionEditor.class); /** * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String) */ public void setAsText(String text) throws IllegalArgumentException { FilterInvocationDefinitionMap source = new RegExpBasedFilterInvocationDefinitionMap(); if (StringUtils.isBlank(text)) { // Leave target object empty } else { // Check if we need to override the default definition map if (text.lastIndexOf(ANT_PATH_KEY) != -1) { source = new PathBasedFilterInvocationDefinitionMap(); if (logger.isDebugEnabled()) { logger.debug(("Detected PATTERN_TYPE_APACHE_ANT directive; using Apache Ant style path expressions")); } } if (text.lastIndexOf(LOWER_CASE_URL_KEY) != -1) { if (logger.isDebugEnabled()) { logger.debug("Instructing mapper to convert URLs to lowercase before comparison"); } source.setConvertUrlToLowercaseBeforeComparison(true); } if (text.indexOf(DONT_USE_ME_KEY) != -1) { if (logger.isDebugEnabled()) { logger.debug("DETECTED " + DONT_USE_ME_KEY + " directive; skip parse, Use " + EhCacheBasedFilterInvocationDefinitionSourceCache.class + " to parse!"); } addSecureUrl(source, "/dontuseme", "dontuseme"); } else { BufferedReader br = new BufferedReader(new StringReader(text)); int counter = 0; String line; while (true) { counter++; try { line = br.readLine(); } catch (IOException ioe) { throw new IllegalArgumentException(ioe.getMessage()); } if (line == null) { break; } line = line.trim(); if (logger.isDebugEnabled()) { logger.debug("Line " + counter + ": " + line); } if (line.startsWith("//")) { continue; } if (line.equals(LOWER_CASE_URL_KEY)) { continue; } if (line.lastIndexOf('=') == -1) { continue; } // Tokenize the line into its name/value tokens String[] nameValue = org.springframework.util.StringUtils.delimitedListToStringArray(line, "="); String name = nameValue[0]; String value = nameValue[1]; addSecureUrl(source, name, value); } } } setValue(source); } /** * @param source * @param name * @param value * @throws IllegalArgumentException */ private 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); } }
Ok, 现在 FilterInvocationDefinitionSourceDynamicExtentionEditor 遇到配置文件中的 “DONT_USE_ME” 时将直接略过, 下面是我的 filterInvocationInterceptor 配置:
图3 filterInvocationInterceptor 配置
现在, 我们已经成功阻止 acegi 从配置文件读取权限配置, 下一个问题就是:
4. 如何从表中数据组装 FilterInvocationDefinitionSource
为了实现此功能, 需要一个自定义的资源定义接口来提供 FilterInvocationDefinitionSource, 此接口可能会是这样 :
/* * Copyright 2005-2010 the original author or autors * * http://www.skyon.com.cn * * Project { SkyonFramwork } */ package com.skyon.um.security.acegi.intercept.web; import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource; import org.springframework.beans.factory.FactoryBean; import com.skyon.framework.spring.ehcache.FlushableCache; /** * <class>FilterInvocationDefinitionSourceCache</class> use to hold the global FilterInvocationDefinitionSource, * it keeps a static variable , if the source been changed(generally the database data), the reload method should be called * * @see com.skyon.um.security.acegi.intercept.event.FilterInvocationDefinitionSourceChangedEvent * @see com.skyon.um.security.acegi.intercept.event.FilterInvocationDefinitionSourceListener * @see com.skyon.um.security.acegi.intercept.web.SecurityEnforcementDynamicExtensionFilter * @since 2005-8-7 * @author 王政 * @version $Id: FilterInvocationDefinitionSourceCache.java,v 1.1 2005/11/04 15:55:07 wangzheng Exp $ */ public interface FilterInvocationDefinitionSourceCache extends FactoryBean, FlushableCache { /** The Perl5 expression */ int REOURCE_EXPRESSION_PERL5_REG_EXP = 1; /** The ant path expression */ int RESOURCE_EXPRESSION_ANT_PATH_KEY = 2; /** * Set resource expression, the value must be {@link #REOURCE_EXPRESSION_PERL5_REG_EXP} or {@link #RESOURCE_EXPRESSION_ANT_PATH_KEY} * @see #REOURCE_EXPRESSION_PERL5_REG_EXP * @see #RESOURCE_EXPRESSION_ANT_PATH_KEY * @param resourceExpression the resource expression */ void setResourceExpression(int resourceExpression); /** * Set whether convert url to lowercase before comparison * @param convertUrlToLowercaseBeforeComparison whether convertUrlToLowercaseBeforeComparison */ void setConvertUrlToLowercaseBeforeComparison(boolean convertUrlToLowercaseBeforeComparison); /** * Get the defination source, generally from a database schema * @return the defination source */ FilterInvocationDefinitionSource getFilterInvocationDefinitionSource(); }
其核心方法是 FilterInvocationDefinitionSource getFilterInvocationDefinitionSource(), 此方法将代替配置文件提供资源和角色的配置, 下面是实现
/* * Copyright 2005-2010 the original author or autors * * http://www.skyon.com.cn * * Project { SkyonFramwork } */ package com.skyon.um.security.acegi.intercept.web; import net.sf.acegisecurity.ConfigAttributeDefinition; import net.sf.acegisecurity.ConfigAttributeEditor; import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionMap; import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource; import net.sf.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap; import net.sf.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import com.skyon.framework.spring.ehcache.CacheUtils; import com.skyon.framework.spring.ehcache.SerializableObjectProvider; import com.skyon.framework.spring.support.MandatorySingletonBeanSupport; /** * @since 2005-8-7 * @author 王政 * @version $Id: EhCacheBasedFilterInvocationDefinitionSourceCache.java,v 1.2 2005/11/17 09:38:25 wangzheng Exp $ */ public class EhCacheBasedFilterInvocationDefinitionSourceCache extends MandatorySingletonBeanSupport implements FilterInvocationDefinitionSourceCache, InitializingBean { private static final Log logger = LogFactory.getLog(EhCacheBasedFilterInvocationDefinitionSourceCache.class); private int resourceExpression; private boolean convertUrlToLowercaseBeforeComparison = false; private ResourceMappingProvider resourceMappingProvider; private Cache cache; private Object lock = new Object(); /** * @see com.skyon.um.security.acegi.intercept.web.FilterInvocationDefinitionSourceCache#getFilterInvocationDefinitionSource() */ public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() { synchronized (lock) { Element element = CacheUtils.get(getCache(), "key"); if (element == null) { FilterInvocationDefinitionSource definitionSource = (FilterInvocationDefinitionSource) getFilterInvocationDefinitionSourceFromBackend(); element = new Element("key", new SerializableObjectProvider(definitionSource)); getCache().put(element); } return (FilterInvocationDefinitionSource) ((SerializableObjectProvider) element.getValue()).getSourceObject(); } } public void flushCache() { CacheUtils.flushCache(getCache()); getFilterInvocationDefinitionSource(); } private FilterInvocationDefinitionMap getFilterInvocationDefinitionSourceFromBackend() { logger.info(" 开始加载系统资源权限数据到缓存... "); FilterInvocationDefinitionMap definitionSource = null; switch (resourceExpression) { case REOURCE_EXPRESSION_PERL5_REG_EXP : { definitionSource = new RegExpBasedFilterInvocationDefinitionMap(); break; } case RESOURCE_EXPRESSION_ANT_PATH_KEY : { definitionSource = new PathBasedFilterInvocationDefinitionMap(); break; } default : { throwException(); } } definitionSource.setConvertUrlToLowercaseBeforeComparison(isConvertUrlToLowercaseBeforeComparison()); ResourceMapping[] mappings = getResourceMappingProvider().getResourceMappings(); if (mappings == null || mappings.length ==0) { return 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() + " has 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(FilterInvocationDefinitionSourceDynamicExtentionEditor.STAND_DELIM_CHARACTER); } } String value = valueBuffer.toString(); addSecureUrl(definitionSource, mapping.getResourcePath(), value); } logger.info(" 成功加载系统资源权限数据到缓存 ! "); return 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); } public void afterPropertiesSet() throws Exception { if (resourceExpression != REOURCE_EXPRESSION_PERL5_REG_EXP && resourceExpression != RESOURCE_EXPRESSION_ANT_PATH_KEY) { throwException(); } Assert.notNull(getResourceMappingProvider(), " resourceMappingProvider must be specified"); Assert.notNull(getCache(), " cache must be specified"); } /** * @throws IllegalArgumentException */ private void throwException() throws IllegalArgumentException { throw new IllegalArgumentException("wrong resourceExpression value"); } /** * @return Returns the resourceMappingProvider. */ public ResourceMappingProvider getResourceMappingProvider() { return resourceMappingProvider; } /** * @param resourceMappingProvider The resourceMappingProvider to set. */ public void setResourceMappingProvider(ResourceMappingProvider resourceMappingProvider) { this.resourceMappingProvider = resourceMappingProvider; } /** * @return Returns the convertUrlToLowercaseBeforeComparison. */ public boolean isConvertUrlToLowercaseBeforeComparison() { return convertUrlToLowercaseBeforeComparison; } /** * @param convertUrlToLowercaseBeforeComparison The convertUrlToLowercaseBeforeComparison to set. */ public void setConvertUrlToLowercaseBeforeComparison( boolean convertUrlToLowercaseBeforeComparison) { this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison; } /** * @return Returns the resourceExpression. */ public int getResourceExpression() { return resourceExpression; } /** * @param resourceExpression The resourceExpression to set. */ public void setResourceExpression(int resourceExpression) { this.resourceExpression = resourceExpression; } /** * @return Returns the cache. */ public Cache getCache() { return cache; } /** * @param cache The cache to set. */ public void setCache(Cache cache) { this.cache = cache; } }
实现采用 EhCache 缓存资源权限配置, 这样如果资源权限数据发生变化, 可以 flush Cache 从数据库重新读取. 至于代码中的 ResourceMapingProvider 实现, 简单的把 Resource 表和 Role 表中的数据读取过来即可, 这里不再赘述.
5. 如何将数据库中的权限配置传递给 FilterInvocationInterceptor
完成以上步骤后, 最后一步就是如何把 FilterInvocationDefinitionSourceCache 中的 FilterInvocationDefinitionSource 传递给 FilterInvocationInterceptor, Simple implemention :
public class SecurityEnforcementDynamicExtensionFilter extends SecurityEnforcementFilter implements InitializingBean { … 略去 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // get the defination source form soure holder getFilterSecurityInterceptor().setObjectDefinitionSource(getDefinitionSourceCache().getFilterInvocationDefinitionSource()); } }
配置:
It’s Over Now.
本文引用代码及配置文件参见附件.[/url]