前一篇文章裡介紹了Spring Security的一些基礎知識相信你對Spring Security的工作流程已經有了一定的了解如果你同時在讀源代碼那你應該可以認識的更深刻在這篇文章裡我們將對Spring Security進行一些自定義的擴展比如自定義實現UserDetailsService保護業務方法以及如何對用戶權限等信息進行動態的配置管理
一 自定義UserDetailsService實現
UserDetailsService接口這個接口中只定義了唯一的UserDetails loadUserByUsername(String username)方法它通過用戶名來獲取整個UserDetails對象
前一篇文章已經介紹了系統提供的默認實現方式InMemoryDaoImpl它從配置文件中讀取用戶的身份信息(用戶名密碼等)如果你的客戶想修改用戶信息就需要直接修改配置文件(你需要告訴用戶配置文件的路徑應該在什麼地方修改如何把明文密碼通過MD加密以及如何重啟服務器等)聽起來是不是很費勁啊!
在實際應用中我們可能需要提供動態的方式來獲取用戶身份信息最常用的莫過於數據庫了當然也可以是LDAP服務器等本文首先介紹系統提供的一個默認實現類JdbcDaoImpl(orgspringframeworksecurityuserdetailsjdbc JdbcDaoImpl)它通過用戶名從數據庫中獲取用戶身份信息修改配置文件將userDetailsService Bean的配置修改如下
<bean id=
userDetailsService
class=
org
springframework
security
userdetails
jdbc
JdbcDaoImpl
p:dataSource
ref=
dataSource
p:usersByUsernameQuery=
select userName
passWord
enabled
from users where userName=?
p:authoritiesByUsernameQuery=
select
u
userName
r
roleName from users u
roles
r
users_roles ur where u
userId=ur
userId and
r
roleId=ur
roleId and u
userName=?
/>
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類的部分代碼
private class UsersByUsernameMapping extends MappingSqlQuery {
protected UsersByUsernameMapping(DataSource ds) {
super(ds
usersByUsernameQuery);
declareParameter(new SqlParameter(Types
VARCHAR));
compile();
}
protected Object mapRow(ResultSet rs
int rownum) throws SQLException {
String username = rs
getString(
);
String password = rs
getString(
);
boolean enabled = rs
getBoolean(
);
UserDetails user = new User(username
password
enabled
true
true
true
new GrantedAuthority[] {new GrantedAuthorityImpl(
HOLDER
)});
return user;
}
}
也許你已經看出什麼來了對了系統返回的UserDetails對象就是從這裡來的這就是讀源代碼的好處DaoAuthenticationProvider提供者通過調用自己的authenticate(Authentication authentication)方法將用戶在登錄頁面輸入的用戶信息與這裡從數據庫獲取的用戶信息進行匹配如果匹配成功則將用戶的權限信息賦給Authentication對象並將其存放在SecurityContext中供其它請求使用
那麼我們要擴展獲得更多的用戶信息就要從這裡下手了(數據庫表這裡不在列出來可以參考項目的WebRoot/db目錄下的schemasql文件)比如我們自己的數據庫設計中是通過一個loginId和用戶名來登錄或者我們需要額外IDEMAIL地址等信息MySecurityJdbcDaoImpl實現如下
protectedclass UsersByUsernameMapping extends MappingSqlQuery {
protected UsersByUsernameMapping(DataSource ds) {
super(ds
usersByUsernameQuery);
declareParameter(new SqlParameter(Types
VARCHAR));
compile();
}
protected Object mapRow(ResultSet rs
int rownum) throws SQLException {
// TODO Auto
generated method stub
String userName = rs
getString(
);
String passWord = rs
getString(
);
boolean enabled = rs
getBoolean(
);
Integer userId = rs
getInt(
);
String email = rs
getString(
);
MyUserDetails user = new MyUser(userName
passWord
enabled
true
true
true
new GrantedAuthority[]{new
GrantedAuthorityImpl(
HOLDER
)});
user
setEmail(email);
user
setUserId(userId);
return user;
}
}
如果你已經看過源代碼你會發現這裡只是其中的一部分代碼 具體的實現請看項目的MySecurityJdbcDaoImpl類實現以及MyUserDetails和MyUser類這裡步在一一列出
如果使用Hibernate來操作數據庫你也可以從你的DAO中獲取用戶信息最後你只要將存放了用戶身份信息和權限信息的列表(List)返回給系統就可以
每當用戶請求一個受保護的資源時就會調用認證管理器以獲取用戶認證信息但是如果我們的用戶信息保存在數據庫中那麼每次請求都從數據庫中獲取信息將會影響系統性能那麼將用戶信息進行緩存就有必要了下面就介紹如何在Spring Security中使用緩存
二緩存用戶信息
查看AuthenticationProvider接口的實現類AbstractUserDetailsAuthenticationProvider抽象類(我們配置文件中配置的DaoAuthenticationProvider類繼承了該類)的源代碼會有一行代碼
UserDetails user = this
userCache
getUserFromCache(username);
DaoAuthenticationProvider認證提供者使用UserCache接口的實現來實現對用戶信息的緩存修改DaoAuthenticationProvider的配置如下
<bean id=
daoAuthenticationProvider
class=
org
springframework
security
providers
dao
DaoAuthenticationProvider
p:userCache
ref=
userCache
p:passwordEncoder
ref=
passwordEncoder
p:userDetailsService
ref=
userDetailsService
/>
這裡我們加入了對userCache Bean的引用userCache使用Ehcache來實現對用戶信息的緩存userCache配置如下
<bean id=
userCache
class=
org
springframework
security
providers
dao
cache
EhCacheBasedUserCache
p:cache
ref=
cache
/>
<bean id=
cache
class=
org
springframework
cache
ehcache
EhCacheFactoryBean
p:cacheManager
ref=
cacheManager
p:cacheName=
userCache
/>
<bean id=
cacheManager
class=
org
springframework
cache
ehcache
EhCacheManagerFactoryBean
p:configLocation=
classpath:ehcache
xml
>
</bean>
我們這裡使用的是EhCacheBasedUserCache也就是用EhCache實現緩存的另外系統還提供了一個默認的實現類NullUserCache類我們可以通過源代碼了解到無論上面使用這個類都返回一個null值也就是不使用緩存
三保護業務方法
從第一篇文章中我們已經了解到Spring Security使用Servlet過濾器來攔截用戶的請求來保護WEB資源而這裡卻是使用Spring 框架的AOP來提供對方法的聲明式保護它通過一個攔截器來攔截方法調用並調用方法安全攔截器來保護方法
在介紹之前我們先回憶一下過濾器安全攔截器是如何工作的過濾器安全攔截器首先調用AuthenticationManager認證管理器認證用戶信息如果用過認證則調用AccessDecisionManager訪問決策管理器來驗證用戶是否有權限訪問objectDefinitionSource中配置的受保護資源
首先看看如何配置方法安全攔截器它和過濾器安全攔截器一方繼承自AbstractSecurityInterceptor抽象類(請看源代碼)如下
<bean id=
methodSecurityInterceptor
class=
org
springframework
sethod
aopalliance
MethodSecurityInterceptor
p:authenticationManager
ref=
authenticationManager
p:accessDecisionManager
ref=
accessDecisionManager
>
<property name=
objectDefinitionSource
>
<value>
com
test
service
UserService
get*=ROLE_SUPERVISOR
</value>
</property>
</bean>
這段代碼是不是很眼熟啊哈哈~這和我們配置的過濾器安全攔截器幾乎完全一樣方法安全攔截器的處理過程實際和過濾器安全攔截器的實現機制是相同的這裡就在累述詳細介紹請參考< Spring Security 學習總結一>中相關部分但是也有不同的地方那就是這裡的objectDefinitionSource的配置在等號前面的不在是URL資源而是需要保護的業務方法等號後面還是訪問該方法需要的用戶權限我們這裡配置的comtestserviceUserServiceget*表示對comtestservice包下UserService類的所有以get開頭的方法都需要ROLE_SUPERVISOR權限才能調用這裡使用了提供的實現方法MethodSecurityInterceptor系統還給我們提供了aspectj的實現方式這裡不在介紹(我也正在學…)讀者可以參考其它相關資料
之前已經提到過了Spring Security使用Spring 框架的AOP來提供對方法的聲明式保護即攔截方法調用那麼接下來就是創建一個攔截器配置如下
<bean id=
autoProxyCreator
class=
org
springframework
aop
framework
autoproxy
BeanNameAutoProxyCreator
>
<property name=
interceptorNames
>
<list>
<value>methodSecurityInterceptor</value>
</list>
</property>
<property name=
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接口代碼如下
public ConfigAttributeDefinition getAttributes(Object object)
throws IllegalArgumentException {
if (object == null || !(this
supports(object
getClass()))) {
thrownew IllegalArgumentException(
Object must be a FilterInvocation
);
}
String url = ((FilterInvocation)object)
getRequestUrl();
returnthis
lookupAttributes(url);
}
publicabstract ConfigAttributeDefinition lookupAttributes(String url);
@SuppressWarnings(
unchecked
)
publicabstract Collection getConfigAttributeDefinitions();
@SuppressWarnings(
unchecked
)
publicboolean supports(Class clazz) {
return FilterInvocation
class
isAssignableFrom(clazz);
}
這段代碼你也可以在中找到getAttributes方法的入口參數是一個Object對象這是由系統傳給我們的因為是URL資源的請求所有可以將這個Object對象強制轉換為FilterInvocation對象並通過調用它的getRequestUrl()方法來獲取用戶當前請求的URL地址然後調用子類需要實現的lookupAttributes方法並將該URL地址作為參數傳給該方法下面是具體的實現類DataBaseFilterInvocationDefinitionSource類的代碼也就是我們需要實現抽象父類的lookupAttributes方法
@Override
public ConfigAttributeDefinition lookupAttributes(String url) {
// TODO Auto
generated method stub
//初始化數據
從數據庫讀取
cacheManager
initResourceInCache();
if (isUseAntPath()) {
int firstQuestionMarkIndex = url
lastIndexOf(
?
);
if (firstQuestionMarkIndex !=
) {
url = url
substring(
firstQuestionMarkIndex);
}
}
//將URL在比較前都轉換為小寫
if (isConvertUrlToLowercaseBeforeComprison()) {
url = url
toLowerCase();
}
//獲取所有的URL
List<String> urls = cacheManager
getUrlResources();
//倒敘排序
如果不進行排序
如果用戶使用浏覽器的導航工具訪問頁面可能出現問題
//例如
訪問被拒絕後用戶刷新頁面
Collections
sort(urls);
Collections
reverse(urls);
GrantedAuthority[] authorities = new GrantedAuthority[
];
//將請求的URL與配置的URL資源進行匹配
並將正確匹配的URL資源對應的權限
//取出
for (String resourceName_url : urls) {
boolean matched = false;
//使用ant匹配URL
if (isUseAntPath()) {
matched = pathMatcher
match(resourceName_url
url);
} else {//perl
編譯URL
Pattern compliedPattern = null;
Perl
Compiler compiler = new Perl
Compiler();
try {
compliedPattern = pile(resourceName_url
Perl
Compiler
READ_ONLY_MASK);
} catch (MalformedPatternException e) {
e
printStackTrace();
}
matched = matcher
matches(url
compliedPattern);
}
//匹配正確
獲取響應權限
if (matched) {
//獲取正確匹配URL資源對應的權限
ResourcDetail detail = cacheManager
getResourcDetailFromCache(resourceName_url);
authorities = detail
getAuthorities();
break;
}
}
//將權限封裝成ConfigAttributeDefinition對象返回(使用ConfigAttributeEditor)
if (authorities
length >
) {
String authTemp =
;
for (GrantedAuthority grantedAuthority : authorities) {
authTemp += grantedAuthority
getAuthority() +
;
}
String authority = authTemp
substring(
(authTemp
length()
));
System
out
println(authority);
ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
attributeEditor
setAsText(authority
trim());
return (ConfigAttributeDefinition)attributeEditor
getValue();
}
returnnull;
}
我們這裡同樣使用了緩存它參考自系統的UseCache接口的實現這裡不在介紹你可以查看本例的源代碼和系統的實現和本例的配置文件這裡將用戶請求的URL地址與從數據庫中獲取的受保護的URL資源使用ant和perl匹配(這取決與你的配置)如果匹配成功則從緩存中獲取訪問該資源需要的權限信息並將其封裝成ConfigAttributeDefinition對象返回這裡使用orgspringframeworksecurityConfigAttributeEditor類該類提供了一個setAsText(String s)該方法收取一個字符串作為參數在該方法中創建ConfigAttributeDefinition對象並將字符串參數傳遞給ConfigAttributeDefinition類的構造函數來初始化該對象詳細的實現還是請你看源代碼現在我們在配置文件添加自己的實現如下
<bean id=
objectDefinitionSource
class=
org
security
intercept
web
DataBaseFilterInvocationDefinitionSource
p:convertUrlToLowercaseBeforeComprison=
true
p:useAntPath=
true
p:cacheManager
ref=
securityCacheManager
/>
convertUrlToLowercaseBeforeComprison屬性定義了在匹配之前將URL都轉換為小寫useAntPath屬性定義使用Ant方式匹配URLcacheManager屬性定義了指向另一個Bean的引用我們使用它從緩存中獲取相應的信息
自定義MethodDefinitionSource
將方法資源存放在數據庫中的實現與URL資源類似這裡不在累述下面是DataBaseMethodInvocationDefinitionSource的源代碼讀者可以參考注釋進行閱讀(該類也是繼承自一個自定義的抽象類AbstractMethodDefinitionSource)
public ConfigAttributeDefinition lookupAttributes(Method method
Class targetClass) {
// TODO Auto
generated method stub
//初始化資源並緩存
securityCacheManager
initResourceInCache();
//獲取所有方法資源
List<String> methods = securityCacheManager
getMethodResources();
//權限集合
Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
//遍歷方法資源
並獲取匹配的資源名稱
然後從緩存中獲取匹配正確
//的資源對應的權限(ResourcDetail對象的GrantedAuthority[]對象數據)
for (String resourceName_method : methods) {
if (isMatch(targetClass
method
resourceName_method)) {
ResourcDetail detail = securityCacheManager
getResourcDetailFromCache(resourceName_method);
if (detail == null) {
break;
}
GrantedAuthority[] authorities = detail
getAuthorities();
if (authorities == null || authorities
length ==
) {
break;
}
authSet
addAll(Arrays
asList(authorities));
}
}
if (authSet
size() >
) {
String authString =
;
for (GrantedAuthority grantedAuthority : authSet) {
authString += grantedAuthority
getAuthority() +
;
}
String authority = authString
substring(
(authString
length()
));
System
out
println(
>>>>>>>>>>>>>>>
+ authority);
ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
attributeEditor
setAsText(authority
trim());
return (ConfigAttributeDefinition)attributeEditor
getValue();
}
returnnull;
}
isMatch方法用於對用戶當前調用的方法與受保護的方法進行匹配與URL資源類似請參考代碼下面是applicationContextsecurityxml文件中的配置請查看該配置文件
<bean id=
methodDefinitionSource
class=
org
sethod
DataBaseMethodInvocationDefinitionSource
p:securityCacheManager
ref=
securityCacheManager
/>
securityCacheManager屬性定義了指向另一個Bean的引用我們使用它從緩存中獲取相應的信息這個Bean和前一節中介紹的一樣只是這裡我們獲取的是方法保護定義資源
本文到此也結束了還請各位多指教
From:http://tw.wingwit.com/Article/program/Java/ky/201311/28719.html