工作需要,又弄起了权限的管理。虽然很早以前都了解过基于容器的权限实现方式,但是一直都觉得那东西太简陋了。后来使用liferay时发现它的权限系统的确做得很优秀,感觉这也可能是它做得最出色的地方吧。但是当时只停留在怎么使用及一些与其衔接的关系之上,并没有对其底层进行了解,新到现在的公司后,发现这一课还是得补上。但是令人惊讶的是,目前可用的选择并不多,甚至很少,最有名的当属spring security了,虽然很有名,但是关于这方面的资料并不是很多,应用示例就更少了。还好有中文的官方文档与http://www.family168.com/bbs/发布的简要教程,因此学起来不至于太困难。然后参照了一篇downpour写的spring security文章,因此勉强熟悉了spring security的应用开发,但是基本只停留在勉强会用的基础之上,而且还花了本人不少时间,在一个项目的运用中,自己更是差点没下得了台,惊出了一身冷汗。当时的感觉spring security就是个垃圾东西,运用很复杂,哪怕是做一个只拦截路径的权限系统,也要经过很多步骤。现在熟悉了它的一些流程后,虽然不知道这样的实现方式是否是最合理的,但是的确也有它的道理。现在利用放假期间,可以静下心来,理解一些以前让自己迷惑的东西了。downpour牛人的那篇文章讲得很好,以至于本人着实花了点时间才把它完全熟悉,当前自己以前对acegi并不熟悉。熟悉了那篇文章后,还是有些地方让自己不太理解,其中之一就是spring security是怎样完成用户角色权限验证的。下面就对这人问题进行简单的介绍:
首先这篇文章是基于downpour那篇文章的,其地址为:
http://www.javaeye.com/topic/319965
最先着手就是配置文件,这也是整个spring security最重要的入口点:
上面的“accessDecisionManager”就是切入点,首先需要说明的是,在验证用户是否能通过验证时,spring security提供了三种策略,分别对应那个策略类:
UnanimousBased.java 只要有一个Voter不能完全通过权限要求,就禁止访问。
AffirmativeBased.java只要有一个Voter可以通过权限要求,就可以访问。
ConsensusBased.java只要通过的Voter比禁止的Voter数目多就可以访问了。
在此说一点,ConsensusBased这个类有点特别,如果通过的票数与禁止的票数相同怎么办?
这个类有个allowIfEqualGrantedDeniedDecisions属性,默认为true,关键代码:
if ((grant == deny) && (grant != 0)) {
if (this.allowIfEqualGrantedDeniedDecisions) {
return;
} else {
throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied",
"Access is denied"));
}
}
上面的代码表示如果allowIfEqualGrantedDeniedDecisions为true而且通过的票数不为0就授权访问。
在提供好验证策略以后,继续关注其是怎么进行验证的。在上面的XML文件中,accessDescisionManager有个allowIfAllAbstainDecisions属性,这个属性的默认值为false,作用见注释处。现在主要关注的是其decisionVoters属性,中文的理解就是投票,RoleVoter.java 主要作用就是完成角色的投票验证,需要注意的是,它验证的角色,名称必须以ROLE_开头,当然这也可以通过配置文件改变,如:
至于RoleVoter是如何完成验证的呆会再说,先回顾一下com.javaeye.sample.security.interceptor.SecureResourceFilterInvocationDefinitionSource:
.....
public ConfigAttributeDefinition getAttributes(Object filter) throws IllegalArgumentException {
FilterInvocation filterInvocation = (FilterInvocation) filter;
String requestURI = filterInvocation.getRequestUrl();
Map urlAuthorities = this.getUrlAuthorities(filterInvocation);
String grantedAuthorities = null;
for(Iterator> iter = urlAuthorities.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = iter.next();
//url表示从资源表取出的值,在这里代表的是相应的URL
String url = entry.getKey();
//这段代码表示数据库内的需要验证的资源URL与当前请求的URL相匹配时进行验证
if(urlMatcher.pathMatchesUrl(url, requestURI)) {
//grantedAuthorities表示每个资源对应的角色,如果有多个角色,则以','隔开
grantedAuthorities = entry.getValue();
break;
}
}
if(grantedAuthorities != null) {
ConfigAttributeEditor configAttrEditor = new ConfigAttributeEditor();
configAttrEditor.setAsText(grantedAuthorities);
return (ConfigAttributeDefinition) configAttrEditor.getValue();
}
return null;
}
....
虽然不重要,但是还是有必要引用一下:
当时一直不明白这个getAttributes到底拿来做什么的。下面一步步进行追终,通过配置文件可知,这个类首先会到org.springframework.security.intercept.web.FilterSecurityInterceptor这个类中,这个类有个主要的方法:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observce
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
在此有两点需要说明,首先是observeOncePerRequest属性,一般情况下保持其默认的true,文档上有说:
true
,meaning the FilterSecurityInterceptor
will only execute once-per-request. Sometimes users may wishit to execute more than once per request, such as when JSP forwards are being used and filter security is desired on each included fragment of the HTTP request. /***
Indicates whether once-per-request handling will be observed. By default this is true
,meaning the FilterSecurityInterceptor
will only execute once-per-request. Sometimes users may wishit to execute more than once per request, such as when JSP forwards are being used and filter security is desired on each included fragment of the HTTP request.
*/
其次我们需要关注的重点是FilterSecurityInterceptor的超类AbstractSecurityInterceptor的beforeInvocation方法,下面贴出这个类中我们最需要关注的代码:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource().getAttributes(object);
if (attr == null) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"No public invocations are allowed via this AbstractSecurityInterceptor. "
+ "This indicates a configuration error because the "
+ "AbstractSecurityInterceptor.rejectPublicInvocations property is set to 'true'");
}
if (logger.isDebugEnabled()) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (logger.isDebugEnabled()) {
logger.debug("Secure object: " + object + "; ConfigAttributes: " + attr);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"), object, attr);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attr);
}
catch (AccessDeniedException accessDeniedException) {
AuthorizationFailureEvent event = new AuthorizationFailureEvent(object, attr, authenticated,
accessDeniedException);
publishEvent(event);
throw accessDeniedException;
}
if (logger.isDebugEnabled()) {
logger.debug("Authorization successful");
}
AuthorizedEvent event = new AuthorizedEvent(object, attr, authenticated);
publishEvent(event);
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attr);
if (runAs == null) {
if (logger.isDebugEnabled()) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(authenticated, false, attr, object);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContextHolder.getContext().setAuthentication(runAs);
// revert to token.Authenticated post-invocation
return new InterceptorStatusToken(authenticated, true, attr, object);
}
}
/**
* Checks the current authentication token and passes it to the AuthenticationManager if
* {@link org.springframework.security.Authentication#isAuthenticated()} returns false or the property
* alwaysReauthenticate has been set to true.
*
* @return an authenticated Authentication object.
*/
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
if (logger.isDebugEnabled()) {
logger.debug("Previously Authenticated: " + authentication);
}
return authentication;
}
上面的代码首先关注其中的一行代码:
ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource().getAttributes(object);
不错,这行代码就是SecureResourceFilterInvocationDefinitionSource存在的主要目的,它主要提供URL与ROLE这两个东西,至于需要验证的用户来源,上面有句代码:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
众所周知,用户登陆成功后,可以通过:
(User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
上面的代码可以获取当前登陆的用户的基本信息,所以验证时需要它也很自然的。既然所需要的东西都具备了,下面就讲讲该怎么验证的问题了。在上面的AbstractSecurityInterceptor内,有句代码:
this.accessDecisionManager.decide(authenticated, object, attr);
上面这个accessDecisionManager就是最开始讲的那个accessDecisionManager,终于回到原来的问题上了,由于上面的配置文件中使用了AffirmativeBased投票策略,下面就直接进入此类的decide方法:
public void decide(Authentication authentication, Object object, ConfigAttributeDefinition config)
throws AccessDeniedException {
Iterator iter = this.getDecisionVoters().iterator();
int deny = 0;
while (iter.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter) iter.next();
int result = voter.vote(authentication, object, config);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied",
"Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
这个方法主要有三个作用,第一作用就是完成投票,第二就是验证,从上面的switch与if语句可以看出,只要有一个投票者赞成,就直接返回,验证通过。如果没有一个投票者赞成(弃权)而且有人反对,deny++,到if(deny>0)时扔出异常,最后禁止访问。最后一句
//到这步时,所有的投票者都弃权了
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
这就到了allowIfAllAbstainDecisions属性起作用的时候了。下面就来讲讲AffirmativeBased中用到的投票类RoleVoter,这个类主要工作就是完成投票工作,然后将结果反馈给AffirmativeBased,下面列出RoleVoter的代码:
ROLE_
to be overridden. true
public class RoleVoter implements AccessDecisionVoter {
//~ Instance fields ================================================================================================
private String rolePrefix = "ROLE_";
//~ Methods ========================================================================================================
public String getRolePrefix() {
return rolePrefix;
}
/**
* Allows the default role prefix of ROLE_
to be overridden.
* May be set to an empty value, although this is usually not desirable.
*
* @param rolePrefix the new prefix
*/
public void setRolePrefix(String rolePrefix) {
this.rolePrefix = rolePrefix;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) {
return true;
}
else {
return false;
}
}
/**
* This implementation supports any type of class, because it does not query
* the presented secure object.
*
* @param clazz the secure object
*
* @return always true
*/
public boolean supports(Class clazz) {
return true;
}
public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
int result = ACCESS_ABSTAIN;
Iterator iter = config.getConfigAttributes().iterator();
GrantedAuthority[] authorities = extractAuthorities(authentication);
while (iter.hasNext()) {
ConfigAttribute attribute = (ConfigAttribute) iter.next();
if (this.supports(attribute)) {
result = ACCESS_DENIED;
// Attempt to find a matching granted authority
for (int i = 0; i < authorities.length; i++) {
if (attribute.getAttribute().equals(authorities[i].getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
GrantedAuthority[] extractAuthorities(Authentication authentication) {
return authentication.getAuthorities();
}
}
这个类中最重要的代码就是这句:
if (attribute.getAttribute().equals(authorities[i].getAuthority())) {
return ACCESS_GRANTED;
}
这句代码的意思就是把SecureResourceFilterInvocationDefinitionSource传入的角色名称与SecurityContextHolder.getContext().getAuthentication()传入的用户所拥有的角色的角色名称相比较,如果相等则通过验证。
在配置文件中还用到了一个投票类AuthenticatedVoter,这个类与RoleVoter属于同级,RoleVoter用来验证角色,那AutherticatedVoter又是用来干什么的呢?
这个类完整代码:
true
public class AuthenticatedVoter implements AccessDecisionVoter {
//~ Static fields/initializers =====================================================================================
public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY";
public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED";
public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY";
//~ Instance fields ================================================================================================
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
//~ Methods ========================================================================================================
private boolean isFullyAuthenticated(Authentication authentication) {
return (!authenticationTrustResolver.isAnonymous(authentication)
&& !authenticationTrustResolver.isRememberMe(authentication));
}
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
Assert.notNull(authenticationTrustResolver, "AuthenticationTrustResolver cannot be set to null");
this.authenticationTrustResolver = authenticationTrustResolver;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())
|| IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute())
|| IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute()))) {
return true;
} else {
return false;
}
}
/**
* This implementation supports any type of class, because it does not query the presented secure object.
*
* @param clazz the secure object
*
* @return always true
*/
public boolean supports(Class clazz) {
return true;
}
public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
int result = ACCESS_ABSTAIN;
Iterator iter = config.getConfigAttributes().iterator();
while (iter.hasNext()) {
ConfigAttribute attribute = (ConfigAttribute) iter.next();
if (this.supports(attribute)) {
result = ACCESS_DENIED;
if (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())) {
if (isFullyAuthenticated(authentication)) {
return ACCESS_GRANTED;
}
}
if (IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute())) {
if (authenticationTrustResolver.isRememberMe(authentication)
|| isFullyAuthenticated(authentication)) {
return ACCESS_GRANTED;
}
}
if (IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute())) {
if (authenticationTrustResolver.isAnonymous(authentication) || isFullyAuthenticated(authentication)
|| authenticationTrustResolver.isRememberMe(authentication)) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
}
作用见下面引用:
其中上面引用中的ConfigAttribute就是指SecureResourceFilterInvocationDefinitionSource的getAttributes()方法中ConfigAttributeDefinition中的ConfigAttribute,downpour的文章只是传入了ROLE_USER,ROLE_ADMIN等内容,要想让AuthenticatedVoter有用武之地,可以传入
IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY中的值。
需要注意的是AffirmativeBased中遍历的投票者是要分先后的,也就是说RoleVoter在AuthenticatedVoter前面的话,会先进行RoleVoter验证,如果RoleVoter投票未通过,再进行了AuthenticatedVoter投票。
这样spring security的验证流程就基本清楚了,当然这篇文章也还是有些地方讲得不完善,以事有时间再来修改。
在前一篇文章中,还是有些地方没讲清楚,但那篇文章已经有点长了,所以还是另外单独讲一下吧。见SecureResourceFilterInvocationDefinitionSource代码:
/**
* RegexUrlPathMatcher默认不进行小写转换,而AntUrlPathMatcher默认要进行小写转换
*/
public void afterPropertiesSet() throws Exception {
// default url matcher will be RegexUrlPathMatcher
this.urlMatcher = new RegexUrlPathMatcher();
if (useAntPath) { // change the implementation if required
this.urlMatcher = new AntUrlPathMatcher();
}
// Only change from the defaults if the attribute has been set
if ("true".equals(lowercaseComparisons)) {
if (!this.useAntPath) {
((RegexUrlPathMatcher) this.urlMatcher).setRequiresLowerCaseUrl(true);
}
} else if ("false".equals(lowercaseComparisons)) {
if (this.useAntPath) {
//是否对URL全部转换成小写格式
((AntUrlPathMatcher) this.urlMatcher).setRequiresLowerCaseUrl(false);
}
}
}
//这个方法主要会在FilterSecurityInterceptor->AbstractSecurityInterceptor->beforeInvocation中用到
public ConfigAttributeDefinition getAttributes(Object filter) throws IllegalArgumentException {
FilterInvocation filterInvocation = (FilterInvocation) filter;
String requestURI = filterInvocation.getRequestUrl();
Map urlAuthorities = this.getUrlAuthorities(filterInvocation);
String grantedAuthorities = null;
for(Iterator> iter = urlAuthorities.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = iter.next();
//url表示从资源表取出的值,在这里代表的是相应的URL
String url = entry.getKey();
//这段代码表示数据库内的需要验证的资源URL与当前请求的URL相匹配时进行验证
if(urlMatcher.pathMatchesUrl(url, requestURI)) {
//grantedAuthorities表示每个资源对应的角色,如果有多个角色,则以','隔开
grantedAuthorities = entry.getValue();
break;
}
}
if(grantedAuthorities != null) {
ConfigAttributeEditor configAttrEditor = new ConfigAttributeEditor();
configAttrEditor.setAsText(grantedAuthorities);
return (ConfigAttributeDefinition) configAttrEditor.getValue();
}
//返回null表示不会验证
return null;
}
这个方法的主要作用是从数据库的resource表加载出所有的资源URL值,通过它与request请求的URL相比较,而比较器一般采用AntUrlPathMatcher,如果需要更加灵活的方式,可以使用RegexUrlPathMatcher,先介绍一下上面的getAttributes是怎么通过URL进行验证的。从上面的for循环可以看出,首先遍历资源URL的列表,如果发现有与当前request请求URL相匹配的,就进行验证,如果当前用户拥有的角色与此资源URL对应的角色相同,则通过验证,否则禁止访问。见下图:
上面一张简单的图,基本能说明意思,需要说明的是只要发现有与当前请求路径相匹配的,就会进行验证,而且只验证一次,没有通过验证也不会再检查是否匹配第二个资源URL,的当然如果请求的URL与第一个资源URL不匹配,就会继续向下查找,直到找到与请求URL匹配的资源URL为止,如果遍历完以后,还是没有查到,就到弃权处理,至于所有投票者都弃权以后,该怎么处理,前一篇博客有介绍的。由此可知,这个资源的URL路径的顺序就比较重要了。如果遍历出来的第一个资源URL为/**,而普通用户角色对应的资源URL没有它,那么即使普通角色拥有其它的资源URL权限也是不能访问到相应的页面的。但是如果普通用户有/**的权限,而/**是匹配所有路径的,如/admin/index.jsp也是会匹配的,这样就相当于普通用户拥有任何路径的权限,这也是不行的。所以遍历出来的资源URL顺序很重要。
在基于XML的方式授权时就是把/**放在最后面的,如:
也就是说,这个路径放的顺序很重要,在数据库里存放URL时也要遵循这样的规则,如上面的XML放在数据库里应该这样:
这里需要多做的一个步骤是,取出来后不要改变取数据库记录的顺序。在SecurityManagerSupport中的代码:
public Map loadUrlAuthorities() {
Map urlAuthorities = new LinkedHashMap();
@SuppressWarnings("unchecked")
List urlResources = getHibernateTemplate().find("FROM Resource resource WHERE resource.type = ?", "URL");
for(Resource resource : urlResources) {
urlAuthorities.put(resource.getValue(), resource.getRoleAuthorities());
}
return urlAuthorities;
}
要保持取出的数据的顺序不改变,需要使用LinkedHashMap,这样SecureResourceFilterInvocationDefinitionSource在验证URL的顺序就与数据库里面存的顺序一致了,表面上看的确有些不灵活,而实际上,在使用中时资源URL是固定的,用户不能改变的,所以这也没什么影响。
关于AntUrlPathMatcher的匹配规则也很简单,文档中有说明:
* ? matches one character
* * matches zero or more characters
* ** matches zero or more 'directories' in a path