spring security验证流程

发布时间:2022-12-29 11:00

工作需要,又弄起了权限的管理。虽然很早以前都了解过基于容器的权限实现方式,但是一直都觉得那东西太简陋了。后来使用liferay时发现它的权限系统的确做得很优秀,感觉这也可能是它做得最出色的地方吧。但是当时只停留在怎么使用及一些与其衔接的关系之上,并没有对其底层进行了解,新到现在的公司后,发现这一课还是得补上。但是令人惊讶的是,目前可用的选择并不多,甚至很少,最有名的当属spring security了,虽然很有名,但是关于这方面的资料并不是很多,应用示例就更少了。还好有中文的官方文档与http://www.family168.com/bbs/发布的简要教程,因此学起来不至于太困难。然后参照了一篇downpour写的spring security文章,因此勉强熟悉了spring security的应用开发,但是基本只停留在勉强会用的基础之上,而且还花了本人不少时间,在一个项目的运用中,自己更是差点没下得了台,惊出了一身冷汗。当时的感觉spring security就是个垃圾东西,运用很复杂,哪怕是做一个只拦截路径的权限系统,也要经过很多步骤。现在熟悉了它的一些流程后,虽然不知道这样的实现方式是否是最合理的,但是的确也有它的道理。现在利用放假期间,可以静下心来,理解一些以前让自己迷惑的东西了。downpour牛人的那篇文章讲得很好,以至于本人着实花了点时间才把它完全熟悉,当前自己以前对acegi并不熟悉。熟悉了那篇文章后,还是有些地方让自己不太理解,其中之一就是spring security是怎样完成用户角色权限验证的。下面就对这人问题进行简单的介绍:

首先这篇文章是基于downpour那篇文章的,其地址为:
[url]http://www.iteye.com/topic/319965[/url]

最先着手就是配置文件,这也是整个spring security最重要的入口点:


............

class="org.springframework.security.providers.ProviderManager">



class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
value="classpath:org/springframework/security/messages_zh_CN" />







class="org.springframework.security.vote.AffirmativeBased">

value="false" />









class="org.springframework.security.intercept.web.FilterSecurityInterceptor">


ref="secureResourceFilterInvocationDefinitionSource" />





class="com.javaeye.sample.security.interceptor.SecureResourceFilterInvocationDefinitionSource" />
....


上面的“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;
}
....


虽然不重要,但是还是有必要引用一下:

[quote]
处于继承树顶端的AbstractSecurityInterceptor有三个实现类:

FilterSecurityInterceptor,负责处理FilterInvocation,实现对URL资源的拦截。
MethodSecurityInterceptor,负责处理MethodInvocation,实现对方法调用的拦截。
AspectJSecurityInterceptor,负责处理JoinPoint,主要也是用于对方法调用的拦截。

为了限制用户访问被保护资源,Spring Security提供了一套元数据,用于定义被保护资源的访问权限,这套元数据主要体现为ConfigAttribute和ConfigAttributeDefinition。每个ConfigAttribute中只包含一个字符串,而一个ConfigAttributeDefinition中可以包含多个ConfigAttribute。对于系统来说,每个被保护资源都将对应一个ConfigAttributeDefinition,这个ConfigAttributeDefinition中包含的多个ConfigAttribute就是访问该资源所需的权限。

实际应用中,ConfigAttributeDefinition会保存在ObjectDefinitionSource中,这是一个主要接口,FilterSecurityInterceptor所需的DefaultFilterInvocationDefinitionSource和MethodSecurityInterceptor所需的MethodDefinitionAttributes都实现了这个接口。ObjectDefinitionSource可以看做是Spring Security中权限配置的源头,框架内部所有的验证组件都是从ObjectDefintionSource中获得数据,来对被保护资源进行权限控制的。

为了从xml中将用户配置的访问权限转换成ObjectDefinitionSource类型的对象,Spring Security专门扩展了Spring中提供的PropertyEditor实现了ConfigAttributeEditor,它可以把以逗号分隔的一系列字符串转换成包含多个ConfigAttribute的ConfigAttributeDefintion对象。

"ROLE_ADMIN,ROLE_USER"



ConfigAttributeDefinition
ConfigAttribute["ROLE_ADMIN"]
ConfigAttribute["ROLE_USER"]

对于FilterSecurityInterceptor来说,最终生成的就是一个包含了url pattern和ConfigAttributeConfiguration的ObjectDefinitionSource。






ConfigAttributeDefinition
"/admin.jsp" → ConfigAttribute["ROLE_ADMIN"]
ConfigAttribute["ROLE_USER"]

换而言之,无论我们将权限配置的原始数据保存在什么地方,只要最终可以将其转换为ObjectDefintionSource就可以提供给验证组件进行调用,实现权限控制。

[/quote]

当时一直不明白这个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,文档上有说:


/***
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的代码:


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又是用来干什么的呢?
这个类完整代码:


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;
}
}



作用见下面引用:
[quote]
AuthenticatedVoter用于判断ConfigAttribute上是否拥有IS_AUTHENTICATED_FULLY,IS_AUTHENTICATED_REMEMBERED或IS_AUTHENTICATED_ANONYMOUSLY之类的配置。
如果配置为IS_AUTHENTICATED_FULLY,那么只有AuthenticationTrustResolver的isAnonymous()和isRememberMe()都返回false时才能通过验证。
如果配置为IS_AUTHENTICATED_REMEMBERED,那么会在AuthenticationTrustResolver的isAnonymous()返回false时通过验证。
如果配置为IS_AUTHENTICATED_ANONYMOUSLY,就可以在AuthenticationTrustResolver的isAnonymous()和isRememberMe()两个方法返回任意值时都可以通过验证。
[/quote]

其中上面引用中的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的验证流程就基本清楚了,当然这篇文章也还是有些地方讲得不完善,以事有时间再来修改。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号