Spring Security學習總結二
背景知識:Spring Security 學習總結一
SpringSide(你可以去官網了解更多信息,我也是從這里開始了解和學習Spring Security的)
前一篇文章里介紹了Spring Security的一些基礎知識,相信你對Spring Security的工作流程已經有了一定的了解,如果你同時在讀源代碼,那你應該可以認識的更深刻。在這篇文章里,我們將對Spring Security進行一些自定義的擴展,比如自定義實現UserDetailsService,保護業務方法以及如何對用戶權限等信息進行動態的配置管理。
說明:
如果你通過Google搜索,可以找到很
多類似主題的文章,本文的目的在于通過這些實例來介紹的工作
原理,我覺得這些才是最重要的。相信你在讀完本文之后應該可以按照自己的想法去擴展Spring Secu
rity,這也是我寫這兩篇文章的目的。希望能對
初學者們有所幫助。
|
|
廢話少說,咱們直接進入正題。
一 自定義UserDetailsService實現
UserDetailsService接口,這個接口中只定義了唯一的UserDetails loadUserByUsername(String username)方法,它通過用戶名來獲取整個UserDetails對象。
前一篇文章已經介紹了系統提供的默認實現方式InMemoryDaoImpl,它從配置文件中讀取用戶的身份信息(用戶名,密碼等),如果你的客戶想修改用戶信息,就需要直接修改配置文件(你需要告訴用戶配置文件的路徑,應該在什么地方修改,如何把明文密碼通過MD5加密以及如何重啟服務器等)。聽起來是不是很費勁啊!
在實際應用中,我們可能需要提供動態的方式來獲取用戶身份信息,最常用的莫過于數據庫了,當然也可以是LDAP服務器等。本文首先介紹系統提供的一個默認實現類JdbcDaoImpl(org.springframework.security.userdetails.jdbc. JdbcDaoImpl),它通過用戶名從數據庫中獲取用戶身份信息,修改配置文件,將userDetailsService Bean的配置修改如下:
1 <bean id="userDetailsService"
2 class="org.springframework.security.userdetails.jdbc.JdbcDaoImpl"
3 p:dataSource-ref="dataSource"
4 p:usersByUsernameQuery="select userName, passWord, enabled, from users where userName=?"
5 p:authoritiesByUsernameQuery="select
6 u.userName,r.roleName from users u,roles
7 r,users_roles ur where u.userId=ur.userId and
8 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》):
圖1 JdbcDaoImp數據庫表
如果我們需要獲取用戶的其它信息就需要自己來擴展系統的默認實現,首先應該了解一下UserDetailsService實現的原理,還是要回到源代碼,以下是JdbcDaoImpl類的部分代碼:
1 private class UsersByUsernameMapping extends MappingSqlQuery {
2
3 protected UsersByUsernameMapping(DataSource ds) {
4
5 super(ds, usersByUsernameQuery);
6
7 declareParameter(new SqlParameter(Types.VARCHAR));
8
9 compile();
10
11 }
12
13 protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
14
15 String username = rs.getString(1);
16
17 String password = rs.getString(2);
18
19 boolean enabled = rs.getBoolean(3);
20
21 UserDetails user = new User(username, password, enabled, true,
22
23 true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});
24
25 return user;
26 }
27
28 }
也許你已經看出什么來了,對了,系統返回的UserDetails對象就是從這里來的,這就是讀源代碼的好處,DaoAuthenticationProvider提供者通過調用自己的authenticate(Authentication authentication)方法將用戶在登錄頁面輸入的用戶信息與這里從數據庫獲取的用戶信息進行匹配,如果匹配成功則將用戶的權限信息賦給Authentication對象并將其存放在SecurityContext中,供其它請求使用。
那么我們要擴展獲得更多的用戶信息,就要從這里下手了(數據庫表這里不在列出來,可以參考項目的WebRoot/db目錄下的schema.sql文件)。比如我們自己的數據庫設計中是通過一個loginId和用戶名來登錄或者我們需要額外ID,EMAIL地址等信息,MySecurityJdbcDaoImpl實現如下:
1 protectedclass UsersByUsernameMapping extends MappingSqlQuery {
2
3 protected UsersByUsernameMapping(DataSource ds) {
4
5 super(ds, usersByUsernameQuery);
6
7 declareParameter(new SqlParameter(Types.VARCHAR));
8
9 compile();
10
11 }
12
13 protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
14
15 // TODO Auto-generated method stub
16
17 String userName = rs.getString(1);
18
19 String passWord = rs.getString(2);
20
21 boolean enabled = rs.getBoolean(3);
22
23 Integer userId = rs.getInt(4);
24
25 String email = rs.getString(5);
26
27 MyUserDetails user = new MyUser(userName, passWord, enabled, true,
28 true,true, new GrantedAuthority[]{new
29 GrantedAuthorityImpl("HOLDER")});
30
31 user.setEmail(email);
32
33 user.setUserId(userId);
34
35 return user;
36
37 }
38
39 }
如果你已經看過源代碼,你會發現這里只是其中的一部分代碼 ,具體的實現請看項目的MySecurityJdbcDaoImpl類實現,以及MyUserDetails和MyUser類,這里步在一一列出。
如果使用Hibernate來操作數據庫,你也可以從你的DAO中獲取用戶信息,最后你只要將存放了用戶身份信息和權限信息的列表(List)返回給系統就可以。
提示:
這里沒有介紹更多的細節問題,主要還是想你自己能通過讀源代碼來加深理解,本人水平也有限,
相信你從源代碼中能悟出更多的有價值的東西。
|
|
每當用戶請求一個受保護的資源時,就會調用認證管理器以獲取用戶認證信息,但是如果我們的用戶信息保存在數據庫中,那么每次請求都從數據庫中獲取信息將會影響系統性能,那么將用戶信息進行緩存就有必要了,下面就介紹如何在Spring Security中使用緩存。
二緩存用戶信息
查看AuthenticationProvider接口的實現類AbstractUserDetailsAuthenticationProvider抽象類(我們配置文件中配置的DaoAuthenticationProvider類繼承了該類)的源代碼,會有一行代碼:
1 UserDetails user = this.userCache.getUserFromCache(username);
DaoAuthenticationProvider認證提供者使用UserCache接口的實現來實現對用戶信息的緩存,修改DaoAuthenticationProvider的配置如下:
1 <bean id="daoAuthenticationProvider"
2 class="org.springframework.security.providers.dao.DaoAuthenticationProvider"
3 p:userCache-ref="userCache"
4 p:passwordEncoder-ref="passwordEncoder"
5 p:userDetailsService- ref="userDetailsService"/>
這里我們加入了對userCache Bean的引用,userCache使用Ehcache來實現對用戶信息的緩存。userCache配置如下:
1 <bean id="userCache"
2 class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache"
3 p:cache-ref="cache"/>
4 <bean id="cache"
5 class="org.springframework.cache.ehcache.EhCacheFactoryBean"
6 p:cacheManager-ref="cacheManager"
7 p:cacheName="userCache"/>
8 <bean id="cacheManager"
9 class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
10 p:configLocation="classpath:ehcache.xml">
11 </bean>
我們這里使用的是EhCacheBasedUserCache,也就是用EhCache實現緩存的,另外系統還提供了一個默認的實現類NullUserCache類,我們可以通過源代碼了解到,無論上面使用這個類都返回一個null值,也就是不使用緩存。
三保護業務方法
從第一篇文章中我們已經了解到,Spring Security使用Servlet過濾器來攔截用戶的請求來保護WEB資源,而這里卻是使用Spring 框架的AOP來提供對方法的聲明式保護。它通過一個攔截器來攔截方法調用,并調用方法安全攔截器來保護方法。
在介紹之前,我們先回憶一下過濾器安全攔截器是如何工作的。過濾器安全攔截器首先調用AuthenticationManager認證管理器認證用戶信息,如果用過認證則調用AccessDecisionManager訪問決策管理器來驗證用戶是否有權限訪問objectDefinitionSource中配置的受保護資源。
首先看看如何配置方法安全攔截器,它和過濾器安全攔截器一方繼承自AbstractSecurityInterceptor抽象類(請看源代碼),如下:
1 <bean id="methodSecurityInterceptor"
2 class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor"
3 p:authenticationManager-ref="authenticationManager"
4 p:accessDecisionManager-ref="accessDecisionManager">
5 <property name="objectDefinitionSource">
6 <value>
com.test.service.UserService.get*=ROLE_SUPERVISOR
7 </value>
8 </property>
9 </bean>
這段代碼是不是很眼熟啊,哈哈~,這和我們配置的過濾器安全攔截器幾乎完全一樣,方法安全攔截器的處理過程實際和過濾器安全攔截器的實現機制是相同的,這里就在累述,詳細介紹請參考< Spring Security 學習總結一>中相關部分。但是也有不同的地方,那就是這里的objectDefinitionSource的配置,在等號前面的不在是URL資源,而是需要保護的業務方法,等號后面還是訪問該方法需要的用戶權限。我們這里配置的com.test.service.UserService.get*表示對com.test.service包下UserService類的所有以get開頭的方法都需要ROLE_SUPERVISOR權限才能調用。這里使用了提供的實現方法MethodSecurityInterceptor,系統還給我們提供了aspectj的實現方式,這里不在介紹(我也正在學…),讀者可以參考其它相關資料。
之前已經提到過了,Spring Security使用Spring 框架的AOP來提供對方法的聲明式保護,即攔截方法調用,那么接下來就是創建一個攔截器,配置如下:
1 <bean id="autoProxyCreator"
2 class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
3 <property name="interceptorNames">
4 <list>
5 <value>methodSecurityInterceptor</value>
6 </list>
7 </property>
8 <property name="beanNames">
9 <list>
10 <value>userService</value>
11 </list>
12 </property>
13 </bean>
userService是我們在applicationContext.xml中配置的一個Bean,AOP的知識不是本文介紹的內容。到這里保護業務方法的配置就介紹完了。
四將資源放在數據庫中
現在,你的用戶提出了新的需求,它們需要自己可以給系統用戶分配或者取消權限。其實這個并不是什么新鮮事,作為開發者,你也應該為用戶提供這樣的功能。那么我們就需要這些受保護的資源和用戶權限等信息都是動態的,你可以選擇把它們存放在數據庫中或者LDAP服務器上,本文以數據庫為例,介紹如何實現用戶權限的動態控制。
通過前面的介紹,你可能也注意到了,不管是MethodSecurityInterceptor還是FilterSecurityInterceptor都使用authenticationManager和accessDecisionManager屬性用于驗證用戶,并且都是通過使用objectDefinitionSource屬性來定義受保護的資源。不同的是過濾器安全攔截器將URL資源與權限關聯,而方法安全攔截器將業務方法與權限關聯。
你猜對了,我們要做的就是自定義這個objectDefinitionSource的實現,首先讓我們來認識一下系統為我們提供的ObjectDefinitionSource接口,objectDefinitionSource屬性正是指向此接口的實現類。該接口中定義了3個方法,ConfigAttributeDefinition getAttributes(Object
object)方法用戶獲取保護資源對應的權限信息,該方法返回一個ConfigAttributeDefinition對象(位于org.springframework.security包下),通過源代碼我們可以知道,該對象中實際就只有一個List列表,我們可以通過使用ConfigAttributeDefinition類的構造函數來創建這個List列表,這樣,安全攔截器就通過調用getAttributes(Object
object)方法來獲取ConfigAttributeDefinition對象,并將該對象和當前用戶擁有的Authentication對象傳遞給accessDecisionManager(訪問決策管理器,請查看org.springframework.security.vote包下的AffirmativeBased類,該類是訪問決策管理器的一個實現類,它通過一組投票者來決定用戶是否有訪問當前請求資源的權限),訪問決策管理器在將其傳遞給AffirmativeBased類維護的投票者,這些投票者從ConfigAttributeDefinition對象中獲取這個存放了訪問保護資源需要的權限信息的列表,然后遍歷這個列表并與Authentication對象中GrantedAuthority[]數據中的用戶權限信息進行匹配,如果匹配成功,投票者就會投贊成票,否則就投反對票,最后訪問決策管理器來統計這些投票決定用戶是否能訪問該資源。是不是又覺得亂了,還是那句話,如果你結合源代碼你現在一定更明白了。
說了這么些,那我們到底應該如何來實現這個ObjectDefinitionSource接口呢?
首先還是說說Acegi Seucrity 1.x版本,org.acegisecurity.intercept.web和org.acegisecurity.intercept.method包下AbstractFilterInvocationDefinitionSource和AbstractMethodDefinitionSource兩個抽象類,這兩個類分別實現了FilterInvocationDefinitionSource和MethodDefinitionSource接口,而這兩個接口都繼承自ObjectDefinitionSource接口并實現了其中的方法。兩個抽象類都使用方法模板模式來實現,將具體的實現方法交給了子類。
提示:
兩個抽象類實現了各自接口的
getAttributes(Object object)方法并在此方法中調用lookupAttributes
(Method method)方法,而實際該方法在抽象類中并沒有具體的實現,而是留給了子類去實現。
|
|
在Acegi Seucrity 1.x版本中,系統為我們提供了默認的實現,MethodDefinitionMap類用于返回方法的權限信息,而PathBasedFilterInvocationDefinitionMap類和RegExpBasedFilterInvocationDefinitionMap類用于返回URL資源對應的權限信息,也就是ConfigAttributeDefinition對象,現在也許明白一點兒了吧,我們只要按照這三個類的實現方式(也就是”模仿”,從后面的代碼中你可以看到)從數據庫中獲取用戶信息和權限信息然后封裝成一個ConfigAttributeDefinition對象返回即可(其實就是一個List列表,前面已經介紹過了),相信通過Hibernate從數據庫中獲取一個列表應該是再容易不過的了。
回到Spring Security,系統為我們提供的默認實現有些變化,DefaultFilterInvocationDefinitionSource和DelegatingMethodDefinitionSource兩個類,從名字也可以看出來它們分別是干什么的了。這兩個類分別實現了FilterInvocationDefinitionSource和MethodDefinitionSource接口,而這兩個接口都繼承自ObjectDefinitionSource接口并實現了其中的方法,這和1.x版本中一樣。它們都是從配置文件中得到資源和相應權限的信息。
通過上面的介紹,你或許更名白了一些,那我們下面要做的就是實現系統的FilterInvocationDefinitionSource和MethodDefinitionSource接口,只是數據源不是從配置文件中讀取配置信息是數據庫而已。
我們這里對比著Acegi
Seucrity 1.x版本中的實現,我個人認為它更好理解,還是請你好好看看源代碼。
1 自定義FilterInvocationDefinitionSource
在2.0中,系統沒有在系統抽象類,所以我們還是使用1.x中的實現方式,首先通過一個抽象類來實現ObjectDefinitionSource接口。代碼如下:
1 public ConfigAttributeDefinition getAttributes(Object object)
2
3 throws IllegalArgumentException {
4
5 if (object == null || !(this.supports(object.getClass()))) {
6
7 thrownew IllegalArgumentException("Object must be a FilterInvocation");
8
9 }
10
11 String url = ((FilterInvocation)object).getRequestUrl();
12
13 returnthis.lookupAttributes(url);
14
15 }
16
17 publicabstract ConfigAttributeDefinition lookupAttributes(String url);
18
19 @SuppressWarnings("unchecked")
20
21 publicabstract Collection getConfigAttributeDefinitions();
22
23 @SuppressWarnings("unchecked")
24
25 publicboolean supports(Class clazz) {
26
27 return FilterInvocation.class.isAssignableFrom(clazz);
28
29 }
這段代碼你也可以在1.0中找到,getAttributes方法的入口參數是一個Object對象,這是由系統傳給我們的,因為是URL資源的請求,所有可以將這個Object對象強制轉換為FilterInvocation對象,并通過調用它的getRequestUrl()方法來獲取用戶當前請求的URL地址,然后調用子類需要實現的lookupAttributes方法并將該URL地址作為參數傳給該方法,下面是具體的實現類DataBaseFilterInvocationDefinitionSource類的代碼,也就是我們需要實現抽象父類的lookupAttributes方法:
1 @Override
2
3 public ConfigAttributeDefinition lookupAttributes(String url) {
4
5 // TODO Auto-generated method stub
6
7 //初始化數據,從數據庫讀取
8
9 cacheManager.initResourceInCache();
10
11 if (isUseAntPath()) {
12
13 int firstQuestionMarkIndex = url.lastIndexOf("?");
14
15 if (firstQuestionMarkIndex != -1) {
16
17 url = url.substring(0, firstQuestionMarkIndex);
18
19 }
20
21 }
22
23 //將URL在比較前都轉換為小寫
24
25 if (isConvertUrlToLowercaseBeforeComprison()) {
26
27 url = url.toLowerCase();
28
29 }
30
31 //獲取所有的URL
32
33 List<String> urls = cacheManager.getUrlResources();
34
35 //倒敘排序--如果不進行排序,如果用戶使用瀏覽器的導航工具訪問頁面可能出現問題
36
37 //例如:訪問被拒絕后用戶刷新頁面
38
39 Collections.sort(urls);
40
41 Collections.reverse(urls);
42
43 GrantedAuthority[] authorities = new GrantedAuthority[0];
44
45 //將請求的URL與配置的URL資源進行匹配,并將正確匹配的URL資源對應的權限
46
47 //取出
48
49 for (String resourceName_url : urls) {
50
51 boolean matched = false;
52
53 //使用ant匹配URL
54
55 if (isUseAntPath()) {
56
57 matched = pathMatcher.match(resourceName_url, url);
58
59 } else {//perl5編譯URL
60
61 Pattern compliedPattern = null;
62
63 Perl5Compiler compiler = new Perl5Compiler();
64
65 try {
66
67 compliedPattern = compiler.compile(resourceName_url, Perl5Compiler.READ_ONLY_MASK);
68
69 } catch (MalformedPatternException e) {
70
71 e.printStackTrace();
72
73 }
74
75 matched = matcher.matches(url, compliedPattern);
76
77 }
78
79 //匹配正確,獲取響應權限
80
81 if (matched) {
82
83 //獲取正確匹配URL資源對應的權限
84
85 ResourcDetail detail = cacheManager.getResourcDetailFromCache(resourceName_url);
86
87 authorities = detail.getAuthorities();
88
89 break;
90
91 }
92
93 }
94
95 //將權限封裝成ConfigAttributeDefinition對象返回(使用ConfigAttributeEditor)
96
97 if (authorities.length > 0) {
98
99 String authTemp = "";
100
101 for (GrantedAuthority grantedAuthority : authorities) {
102
103 authTemp += grantedAuthority.getAuthority() + ",";
104
105 }
106
107 String authority = authTemp.substring(0, (authTemp.length() - 1));
108
109 System.out.println(authority);
110
111 ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
112
113 attributeEditor.setAsText(authority.trim());
114
115 return (ConfigAttributeDefinition)attributeEditor.getValue();
116
117 }
118
119 returnnull;
120
121 }
我們這里同樣使用了緩存,它參考自系統的UseCache接口的實現,這里不在介紹,你可以查看本例的源代碼和系統的實現和本例的配置文件。這里將用戶請求的URL地址與從數據庫中獲取的受保護的URL資源使用ant和perl5匹配(這取決與你的配置),如果匹配成功則從緩存中獲取訪問該資源需要的權限信息,并將其封裝成ConfigAttributeDefinition對象返回,這里使用org.springframework.security.ConfigAttributeEditor類,該類提供了一個setAsText(String s),該方法收取一個字符串作為參數,在該方法中創建ConfigAttributeDefinition對象并將字符串參數傳遞給ConfigAttributeDefinition類的構造函數來初始化該對象。詳細的實現還是請你看源代碼?,F在我們在配置文件添加自己的實現,如下:
1 <bean id="objectDefinitionSource"
2 class="org.security.intercept.web.DataBaseFilterInvocationDefinitionSource"
3 p:convertUrlToLowercaseBeforeComprison="true"
4 p:useAntPath="true"
5 p:cacheManager-ref="securityCacheManager"/>
convertUrlToLowercaseBeforeComprison屬性定義了在匹配之前將URL都轉換為小寫,useAntPath屬性定義使用Ant方式匹配URL,cacheManager屬性定義了指向另一個Bean的引用,我們使用它從緩存中獲取相應的信息。
2
自定義MethodDefinitionSource
將方法資源存放在數據庫中的實現與URL資源類似,這里不在累述,下面是DataBaseMethodInvocationDefinitionSource的源代碼,讀者可以參考注釋進行閱讀(該類也是繼承自一個自定義的抽象類AbstractMethodDefinitionSource):
1 public ConfigAttributeDefinition lookupAttributes(Method method, Class targetClass) {
2
3 // TODO Auto-generated method stub
4
5 //初始化資源并緩存
6
7 securityCacheManager.initResourceInCache();
8
9 //獲取所有方法資源
10
11 List<String> methods = securityCacheManager.getMethodResources();
12
13 //權限集合
14
15 Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
16
17 //遍歷方法資源,并獲取匹配的資源名稱,然后從緩存中獲取匹配正確
18
19 //的資源對應的權限(ResourcDetail對象的GrantedAuthority[]對象數據)
20
21 for (String resourceName_method : methods) {
22
23 if (isMatch(targetClass, method, resourceName_method)) {
24
25 ResourcDetail detail = securityCacheManager.getResourcDetailFromCache(resourceName_method);
26
27 if (detail == null) {
28
29 break;
30
31 }
32
33 GrantedAuthority[] authorities = detail.getAuthorities();
34
35 if (authorities == null || authorities.length == 0) {
36
37 break;
38
39 }
40
41 authSet.addAll(Arrays.asList(authorities));
42
43 }
44
45 }
46
47 if (authSet.size() > 0) {
48
49 String authString = "";
50
51 for (GrantedAuthority grantedAuthority : authSet) {
52
53 authString += grantedAuthority.getAuthority() + ",";
54
55 }
56
57 String authority = authString.substring(0, (authString.length() - 1));
58
59 System.out.println(">>>>>>>>>>>>>>>" + authority);
60
61 ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
62
63 attributeEditor.setAsText(authority.trim());
64
65 return (ConfigAttributeDefinition)attributeEditor.getValue();
66
67 }
68
69 returnnull;
70
71 }
isMatch方法用于對用戶當前調用的方法與受保護的方法進行匹配,與URL資源類似,請參考代碼。下面是applicationContext-security.xml文件中的配置,請查看該配置文件。
1 <bean id="methodDefinitionSource"
2 class="org.security.intercept.method.DataBaseMethodInvocationDefinitionSource"
3 p:securityCacheManager-ref="securityCacheManager"/>
securityCacheManager屬性定義了指向另一個Bean的引用,我們使用它從緩存中獲取相應的信息。這個Bean和前一節中介紹的一樣。只是這里我們獲取的是方法保護定義資源。
本文到此也結束了,還請各位多指教。
附件:springsecurity2.rar