前一篇文章里介绍了Spring Security的一些基础知识相信你对Spring Security的工作流程已经有了一定的了解如果你同时在读源代码那你应该可以认识的更深刻在这篇文章里我们将对Spring Security进行一些自定义的扩展比如自定义实现UserDetailsService保护业务方法以及如何对用户权限等信息进行动态的配置管理
一 自定义UserDetailsService实现
UserDetailsService接口这个接口中只定义了唯一的UserDetails loadUserByUsername(String username)方法它通过用户名来获取整个UserDetails对象
前一篇文章已经介绍了系统提供的默认实现方式InMemoryDaoImpl它从配置文件中读取用户的身份信息(用户名密码等)如果你的客户想修改用户信息就需要直接修改配置文件(你需要告诉用户配置文件的路径应该在什么地方修改如何把明文密码通过MD加密以及如何重启服务器等)听起来是不是很费劲啊!
在实际应用中我们可能需要提供动态的方式来获取用户身份信息最常用的莫过于数据库了当然也可以是LDAP服务器等本文首先介绍系统提供的一个默认实现类JdbcDaoImpl(orgspringframeworksecurityuserdetailsjdbc JdbcDaoImpl)它通过用户名从数据库中获取用户身份信息修改配置文件将userDetailsService Bean的配置修改如下
<beanid=userDetailsService
class=orgspringframeworksecurityuserdetailsjdbcJdbcDaoImpl
p:dataSourceref=dataSource
p:usersByUsernameQuery=selectuserNamepassWordenabledfromuserswhereuserName=?
p:authoritiesByUsernameQuery=select
uuserNamerroleNamefromusersuroles
rusers_rolesurwhereuuserId=uruserIdand
rroleId=urroleIdanduuserName=?/>
JdbcDaoImpl类继承自Spring Framework的JdbcDaoSupport类并实现了UserDetailsService接口因为从数据库中读取信息所以首先需要一个数据源对象这里不在多说这里需要重点介绍的是usersByUsernameQuery和authoritiesByUsernameQuery属性它们的值都是一条SQL语句JdbcDaoImpl类通过SQL从数据库中检索相应的信息usersByUsernameQuery属性定义了通过用户名检索用户信息的SQL语句包括用户名密码以及用户是否可用authoritiesByUsernameQuery属性定义了通过用户名检索用户权限信息的SQL语句这两个属性都引用一个MappingSqlQuery(请参考Spring Framework相关资料)实例MappingSqlQuery的mapRow()方法将一个ResultSet(结果集)中的字段映射为一个领域对象Spring Security为我们提供了默认的数据库表如下图所示(摘自《Spring in Action》)
图<!——[if supportFields]——><!——[if supportFields]——> JdbcDaoImp数据库表
如果我们需要获取用户的其它信息就需要自己来扩展系统的默认实现首先应该了解一下UserDetailsService实现的原理还是要回到源代码以下是JdbcDaoImpl类的部分代码
privateclassUsersByUsernameMappingextendsMappingSqlQuery{
protectedUsersByUsernameMapping(DataSourceds){
super(dsusersByUsernameQuery);
declareParameter(newSqlParameter(TypesVARCHAR));
compile();
}
protectedObjectmapRow(ResultSetrsintrownum)throwsSQLException{
Stringusername=rsgetString();
Stringpassword=rsgetString();
booleanenabled=rsgetBoolean();
UserDetailsuser=newUser(usernamepasswordenabledtrue
truetruenewGrantedAuthority[]{newGrantedAuthorityImpl(HOLDER)});
returnuser;
}
}
也许你已经看出什么来了对了系统返回的UserDetails对象就是从这里来的这就是读源代码的好处DaoAuthenticationProvider提供者通过调用自己的authenticate(Authentication authentication)方法将用户在登录页面输入的用户信息与这里从数据库获取的用户信息进行匹配如果匹配成功则将用户的权限信息赋给Authentication对象并将其存放在SecurityContext中供其它请求使用
那么我们要扩展获得更多的用户信息就要从这里下手了(数据库表这里不在列出来可以参考项目的WebRoot/db目录下的schemasql文件)比如我们自己的数据库设计中是通过一个loginId和用户名来登录或者我们需要额外IDEMAIL地址等信息MySecurityJdbcDaoImpl实现如下
protectedclassUsersByUsernameMappingextendsMappingSqlQuery{
protectedUsersByUsernameMapping(DataSourceds){
super(dsusersByUsernameQuery);
declareParameter(newSqlParameter(TypesVARCHAR));
compile();
}
protectedObjectmapRow(ResultSetrsintrownum)throwsSQLException{
//TODOAutogeneratedmethodstub
StringuserName=rsgetString();
StringpassWord=rsgetString();
booleanenabled=rsgetBoolean();
IntegeruserId=rsgetInt();
Stringemail=rsgetString();
MyUserDetailsuser=newMyUser(userNamepassWordenabledtrue
truetruenewGrantedAuthority[]{new
GrantedAuthorityImpl(HOLDER)});
usersetEmail(email);
usersetUserId(userId);
returnuser;
}
}
如果你已经看过源代码你会发现这里只是其中的一部分代码 具体的实现请看项目的MySecurityJdbcDaoImpl类实现以及MyUserDetails和MyUser类这里步在一一列出
如果使用Hibernate来操作数据库你也可以从你的DAO中获取用户信息最后你只要将存放了用户身份信息和权限信息的列表(List)返回给系统就可以
每当用户请求一个受保护的资源时就会调用认证管理器以获取用户认证信息但是如果我们的用户信息保存在数据库中那么每次请求都从数据库中获取信息将会影响系统性能那么将用户信息进行缓存就有必要了下面就介绍如何在Spring Security中使用缓存
二缓存用户信息
查看AuthenticationProvider接口的实现类AbstractUserDetailsAuthenticationProvider抽象类(我们配置文件中配置的DaoAuthenticationProvider类继承了该类)的源代码会有一行代码
UserDetailsuser=thisuserCachegetUserFromCache(username);
DaoAuthenticationProvider认证提供者使用UserCache接口的实现来实现对用户信息的缓存修改DaoAuthenticationProvider的配置如下
<beanid=daoAuthenticationProvider
class=orgspringframeworksecurityprovidersdaoDaoAuthenticationProvider
p:userCacheref=userCache
p:passwordEncoderref=passwordEncoder
p:userDetailsServiceref=userDetailsService/>
这里我们加入了对userCache Bean的引用userCache使用Ehcache来实现对用户信息的缓存userCache配置如下
<beanid=userCache
class=orgspringframeworksecurityprovidersdaocacheEhCacheBasedUserCache
p:cacheref=cache/>
<beanid=cache
class=orgspringframeworkcacheehcacheEhCacheFactoryBean
p:cacheManagerref=cacheManager
p:cacheName=userCache/>
<beanid=cacheManager
class=orgspringframeworkcacheehcacheEhCacheManagerFactoryBean
p:configLocation=classpath:ehcachexml>
</bean>
我们这里使用的是EhCacheBasedUserCache也就是用EhCache实现缓存的另外系统还提供了一个默认的实现类NullUserCache类我们可以通过源代码了解到无论上面使用这个类都返回一个null值也就是不使用缓存
三保护业务方法
从第一篇文章中我们已经了解到Spring Security使用Servlet过滤器来拦截用户的请求来保护WEB资源而这里却是使用Spring 框架的AOP来提供对方法的声明式保护它通过一个拦截器来拦截方法调用并调用方法安全拦截器来保护方法
在介绍之前我们先回忆一下过滤器安全拦截器是如何工作的过滤器安全拦截器首先调用AuthenticationManager认证管理器认证用户信息如果用过认证则调用AccessDecisionManager访问决策管理器来验证用户是否有权限访问objectDefinitionSource中配置的受保护资源
首先看看如何配置方法安全拦截器它和过滤器安全拦截器一方继承自AbstractSecurityInterceptor抽象类(请看源代码)如下
<beanid=methodSecurityInterceptor
class=orgspringframeworksethodaopallianceMethodSecurityInterceptor
p:authenticationManagerref=authenticationManager
p:accessDecisionManagerref=accessDecisionManager>
<propertyname=objectDefinitionSource>
<value>
comtestserviceUserServiceget*=ROLE_SUPERVISOR
</value>
</property>
</bean>
这段代码是不是很眼熟啊哈哈~这和我们配置的过滤器安全拦截器几乎完全一样方法安全拦截器的处理过程实际和过滤器安全拦截器的实现机制是相同的这里就在累述详细介绍请参考< Spring Security 学习总结一>中相关部分但是也有不同的地方那就是这里的objectDefinitionSource的配置在等号前面的不在是URL资源而是需要保护的业务方法等号后面还是访问该方法需要的用户权限我们这里配置的comtestserviceUserServiceget*表示对comtestservice包下UserService类的所有以get开头的方法都需要ROLE_SUPERVISOR权限才能调用这里使用了提供的实现方法MethodSecurityInterceptor系统还给我们提供了aspectj的实现方式这里不在介绍(我也正在学…)读者可以参考其它相关资料
之前已经提到过了Spring Security使用Spring 框架的AOP来提供对方法的声明式保护即拦截方法调用那么接下来就是创建一个拦截器配置如下
<beanid=autoProxyCreator
class=orgspringframeworkaopframeworkautoproxyBeanNameAutoProxyCreator>
<propertyname=interceptorNames>
<list>
<value>methodSecurityInterceptor</value>
</list>
</property>
<propertyname=beanNames>
<list>
<value>userService</value>
</list>
</property>
</bean>
userService是我们在applicationContextxml中配置的一个BeanAOP的知识不是本文介绍的内容到这里保护业务方法的配置就介绍完了
四将资源放在数据库中
现在你的用户提出了新的需求它们需要自己可以给系统用户分配或者取消权限其实这个并不是什么新鲜事作为开发者你也应该为用户提供这样的功能那么我们就需要这些受保护的资源和用户权限等信息都是动态的你可以选择把它们存放在数据库中或者LDAP服务器上本文以数据库为例介绍如何实现用户权限的动态控制
通过前面的介绍你可能也注意到了不管是MethodSecurityInterceptor还是FilterSecurityInterceptor都使用authenticationManager和accessDecisionManager属性用于验证用户并且都是通过使用objectDefinitionSource属性来定义受保护的资源不同的是过滤器安全拦截器将URL资源与权限关联而方法安全拦截器将业务方法与权限关联
你猜对了我们要做的就是自定义这个objectDefinitionSource的实现首先让我们来认识一下系统为我们提供的ObjectDefinitionSource接口objectDefinitionSource属性正是指向此接口的实现类该接口中定义了个方法ConfigAttributeDefinition getAttributes(Object object)方法用户获取保护资源对应的权限信息该方法返回一个ConfigAttributeDefinition对象(位于orgspringframeworksecurity包下)通过源代码我们可以知道该对象中实际就只有一个List列表我们可以通过使用ConfigAttributeDefinition类的构造函数来创建这个List列表这样安全拦截器就通过调用getAttributes(Object object)方法来获取ConfigAttributeDefinition对象并将该对象和当前用户拥有的Authentication对象传递给accessDecisionManager(访问决策管理器请查看orgspringframeworksecurityvote包下的AffirmativeBased类该类是访问决策管理器的一个实现类它通过一组投票者来决定用户是否有访问当前请求资源的权限)访问决策管理器在将其传递给AffirmativeBased类维护的投票者这些投票者从ConfigAttributeDefinition对象中获取这个存放了访问保护资源需要的权限信息的列表然后遍历这个列表并与Authentication对象中GrantedAuthority[]数据中的用户权限信息进行匹配如果匹配成功投票者就会投赞成票否则就投反对票最后访问决策管理器来统计这些投票决定用户是否能访问该资源是不是又觉得乱了还是那句话如果你结合源代码你现在一定更明白了
说了这么些那我们到底应该如何来实现这个ObjectDefinitionSource接口呢?
首先还是说说Acegi Seucrity x版本orgacegisecurityinterceptweb和orgacegisethod包下AbstractFilterInvocationDefinitionSource和AbstractMethodDefinitionSource两个抽象类这两个类分别实现了FilterInvocationDefinitionSource和MethodDefinitionSource接口而这两个接口都继承自ObjectDefinitionSource接口并实现了其中的方法两个抽象类都使用方法模板模式来实现将具体的实现方法交给了子类
提示两个抽象类实现了各自接口的 getAttributes(Object object)方法并在此方法中调用lookupAttributes(Method method)方法而实际该方法在抽象类中并没有具体的实现而是留给了子类去实现
在Acegi Seucrity x版本中系统为我们提供了默认的实现MethodDefinitionMap类用于返回方法的权限信息而PathBasedFilterInvocationDefinitionMap类和RegExpBasedFilterInvocationDefinitionMap类用于返回URL资源对应的权限信息也就是ConfigAttributeDefinition对象现在也许明白一点儿了吧我们只要按照这三个类的实现方式(也就是模仿从后面的代码中你可以看到)从数据库中获取用户信息和权限信息然后封装成一个ConfigAttributeDefinition对象返回即可(其实就是一个List列表前面已经介绍过了)相信通过Hibernate从数据库中获取一个列表应该是再容易不过的了
回到Spring Security系统为我们提供的默认实现有些变化DefaultFilterInvocationDefinitionSource和DelegatingMethodDefinitionSource两个类从名字也可以看出来它们分别是干什么的了这两个类分别实现了FilterInvocationDefinitionSource和MethodDefinitionSource接口而这两个接口都继承自ObjectDefinitionSource接口并实现了其中的方法这和x版本中一样它们都是从配置文件中得到资源和相应权限的信息
通过上面的介绍你或许更名白了一些那我们下面要做的就是实现系统的FilterInvocationDefinitionSource和MethodDefinitionSource接口只是数据源不是从配置文件中读取配置信息是数据库而已
我们这里对比着Acegi Seucrity x版本中的实现我个人认为它更好理解还是请你好好看看源代码
自定义FilterInvocationDefinitionSource
在中系统没有在系统抽象类所以我们还是使用x中的实现方式首先通过一个抽象类来实现ObjectDefinitionSource接口代码如下
publicConfigAttributeDefinitiongetAttributes(Objectobject)
throwsIllegalArgumentException{
if(object==null||!(thissupports(objectgetClass()))){
thrownewIllegalArgumentException(ObjectmustbeaFilterInvocation);
}
Stringurl=((FilterInvocation)object)getRequestUrl();
returnthislookupAttributes(url);
}
publicabstractConfigAttributeDefinitionlookupAttributes(Stringurl);
@SuppressWarnings(unchecked)
publicabstractCollectiongetConfigAttributeDefinitions();
@SuppressWarnings(unchecked)
publicbooleansupports(Classclazz){
returnFilterInvocationclassisAssignableFrom(clazz);
}
这段代码你也可以在中找到getAttributes方法的入口参数是一个Object对象这是由系统传给我们的因为是URL资源的请求所有可以将这个Object对象强制转换为FilterInvocation对象并通过调用它的getRequestUrl()方法来获取用户当前请求的URL地址然后调用子类需要实现的lookupAttributes方法并将该URL地址作为参数传给该方法下面是具体的实现类DataBaseFilterInvocationDefinitionSource类的代码也就是我们需要实现抽象父类的lookupAttributes方法
@Override
publicConfigAttributeDefinitionlookupAttributes(Stringurl){
//TODOAutogeneratedmethodstub
//初始化数据从数据库读取
cacheManagerinitResourceInCache();
if(isUseAntPath()){
intfirstQuestionMarkIndex=urllastIndexOf(?);
if(firstQuestionMarkIndex!=){
url=urlsubstring(firstQuestionMarkIndex);
}
}
//将URL在比较前都转换为小写
if(isConvertUrlToLowercaseBeforeComprison()){
url=urltoLowerCase();
}
//获取所有的URL
List<String>urls=cacheManagergetUrlResources();
//倒叙排序如果不进行排序如果用户使用浏览器的导航工具访问页面可能出现问题
//例如访问被拒绝后用户刷新页面
Collectionssort(urls);
Collectionsreverse(urls);
GrantedAuthority[]authorities=newGrantedAuthority[];
//将请求的URL与配置的URL资源进行匹配并将正确匹配的URL资源对应的权限
//取出
for(StringresourceName_url:urls){
booleanmatched=false;
//使用ant匹配URL
if(isUseAntPath()){
matched=pathMatchermatch(resourceName_urlurl);
}else{//perl编译URL
PatterncompliedPattern=null;
PerlCompilercompiler=newPerlCompiler();
try{
compliedPattern=pile(resourceName_urlPerlCompilerREAD_ONLY_MASK);
}catch(MalformedPatternExceptione){
eprintStackTrace();
}
matched=matchermatches(urlcompliedPattern);
}
//匹配正确获取响应权限
if(matched){
//获取正确匹配URL资源对应的权限
ResourcDetaildetail=cacheManagergetResourcDetailFromCache(resourceName_url);
authorities=detailgetAuthorities();
break;
}
}
//将权限封装成ConfigAttributeDefinition对象返回(使用ConfigAttributeEditor)
if(authoritieslength>){
StringauthTemp=;
for(GrantedAuthoritygrantedAuthority:authorities){
authTemp+=grantedAuthoritygetAuthority()+;
}
Stringauthority=authTempsubstring((authTemplength()));
Systemoutprintln(authority);
ConfigAttributeEditorattributeEditor=newConfigAttributeEditor();
attributeEditorsetAsText(authoritytrim());
return(ConfigAttributeDefinition)attributeEditorgetValue();
}
returnnull;
}
我们这里同样使用了缓存它参考自系统的UseCache接口的实现这里不在介绍你可以查看本例的源代码和系统的实现和本例的配置文件这里将用户请求的URL地址与从数据库中获取的受保护的URL资源使用ant和perl匹配(这取决与你的配置)如果匹配成功则从缓存中获取访问该资源需要的权限信息并将其封装成ConfigAttributeDefinition对象返回这里使用orgspringframeworksecurityConfigAttributeEditor类该类提供了一个setAsText(String s)该方法收取一个字符串作为参数在该方法中创建ConfigAttributeDefinition对象并将字符串参数传递给ConfigAttributeDefinition类的构造函数来初始化该对象详细的实现还是请你看源代码现在我们在配置文件添加自己的实现如下
<beanid=objectDefinitionSource
class=orgsecurityinterceptwebDataBaseFilterInvocationDefinitionSource
p:convertUrlToLowercaseBeforeComprison=true
p:useAntPath=true
p:cacheManagerref=securityCacheManager/>
convertUrlToLowercaseBeforeComprison属性定义了在匹配之前将URL都转换为小写useAntPath属性定义使用Ant方式匹配URLcacheManager属性定义了指向另一个Bean的引用我们使用它从缓存中获取相应的信息
自定义MethodDefinitionSource
将方法资源存放在数据库中的实现与URL资源类似这里不在累述下面是DataBaseMethodInvocationDefinitionSource的源代码读者可以参考注释进行阅读(该类也是继承自一个自定义的抽象类AbstractMethodDefinitionSource)
publicConfigAttributeDefinitionlookupAttributes(MethodmethodClasstargetClass){
//TODOAutogeneratedmethodstub
//初始化资源并缓存
securityCacheManagerinitResourceInCache();
//获取所有方法资源
List<String>methods=securityCacheManagergetMethodResources();
//权限集合
Set<GrantedAuthority>authSet=newHashSet<GrantedAuthority>();
//遍历方法资源并获取匹配的资源名称然后从缓存中获取匹配正确
//的资源对应的权限(ResourcDetail对象的GrantedAuthority[]对象数据)
for(StringresourceName_method:methods){
if(isMatch(targetClassmethodresourceName_method)){
ResourcDetaildetail=securityCacheManagergetResourcDetailFromCache(resourceName_method);
if(detail==null){
break;
}
GrantedAuthority[]authorities=detailgetAuthorities();
if(authorities==null||authoritieslength==){
break;
}
authSetaddAll(ArraysasList(authorities));
}
}
if(authSetsize()>){
StringauthString=;
for(GrantedAuthoritygrantedAuthority:authSet){
authString+=grantedAuthoritygetAuthority()+;
}
Stringauthority=authStringsubstring((authStringlength()));
Systemoutprintln(>>>>>>>>>>>>>>>+authority);
ConfigAttributeEditorattributeEditor=newConfigAttributeEditor();
attributeEditorsetAsText(authoritytrim());
return(ConfigAttributeDefinition)attributeEditorgetValue();
}
returnnull;
}
isMatch方法用于对用户当前调用的方法与受保护的方法进行匹配与URL资源类似请参考代码下面是applicationContextsecurityxml文件中的配置请查看该配置文件
<beanid=methodDefinitionSource
class=orgsethodDataBaseMethodInvocationDefinitionSource
p:securityCacheManagerref=securityCacheManager/>
securityCacheManager属性定义了指向另一个Bean的引用我们使用它从缓存中获取相应的信息这个Bean和前一节中介绍的一样只是这里我们获取的是方法保护定义资源
本文到此也结束了还请各位多指教