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
中简单的如下实现即可:
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;
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,
此接口可能会是这样 :
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;
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 ;
void setResourceExpression(
int resourceExpression);
void setConvertUrlToLowercaseBeforeComparison(
boolean convertUrlToLowercaseBeforeComparison);
FilterInvocationDefinitionSource getFilterInvocationDefinitionSource();
}
其核心方法是 FilterInvocationDefinitionSource
getFilterInvocationDefinitionSource(), 此方法将代替配置文件提供资源和角色的配置,
下面是实现
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;
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();
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;
}
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
" );
}
private
void throwException()
throws IllegalArgumentException
{
throw
new IllegalArgumentException(
" wrong resourceExpression value
" );
}
public
ResourceMappingProvider getResourceMappingProvider()
{
return resourceMappingProvider;
}
public
void
setResourceMappingProvider(ResourceMappingProvider resourceMappingProvider)
{
this .resourceMappingProvider
= resourceMappingProvider;
}
public
boolean
isConvertUrlToLowercaseBeforeComparison()
{
return
convertUrlToLowercaseBeforeComparison;
}
public
void
setConvertUrlToLowercaseBeforeComparison(
boolean
convertUrlToLowercaseBeforeComparison)
{
this
.convertUrlToLowercaseBeforeComparison
=
convertUrlToLowercaseBeforeComparison;
}
public
int getResourceExpression()
{
return resourceExpression;
}
public
void setResourceExpression(
int resourceExpression)
{
this .resourceExpression
= resourceExpression;
}
public Cache getCache()
{
return cache;
}
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]
|