spring security的原理及教程

spring security使用分類:

如何使用spring security,相信百度過的都知道,總共有四種用法,從簡到深爲:一、不用數據庫,所有數據寫在配置文件,這個也是官方文檔裏面的demo;二、使用數據庫,根據spring security默認實現代碼設計數據庫,也就是說數據庫已經固定了,這種方法不靈活,並且那個數據庫設計得很簡陋,實用性差;三、spring security和Acegi不一樣,它不能修改默認filter了,但支持插入filter,因此根據這個,咱們能夠插入本身的filter來靈活使用;四、暴力手段,修改源碼,前面說的修改默認filter只是修改配置文件以替換filter而已,這種是直接改了裏面的源碼,可是這種不符合OO設計原則,並且不實際,不可用。css

本文面向讀者:

由於本文準備介紹第三種方法,因此面向的讀者是已經具有了spring security基礎知識的。不過沒關係,讀者能夠先看一下這個教程,看完應該可使用第二種方法開發了。html

spring security的簡單原理:

使用衆多的攔截器對url攔截,以此來管理權限。可是這麼多攔截器,筆者不可能對其一一來說,主要講裏面核心流程的兩個。前端

首先,權限管理離不開登錄驗證的,因此登錄驗證攔截器AuthenticationProcessingFilter要講;還有就是對訪問的資源管理吧,因此資源管理攔截器AbstractSecurityInterceptor要講;但攔截器裏面的實現須要一些組件來實現,因此就有了AuthenticationManager、accessDecisionManager等組件來支撐。java

如今先大概過一遍整個流程,用戶登錄,會被AuthenticationProcessingFilter攔截,調用AuthenticationManager的實現,並且AuthenticationManager會調用ProviderManager(用來獲取用戶驗證信息,是對AuthenticationManager的實現)來獲取用戶驗證信息(不一樣的Provider調用的服務不一樣,由於這些信息能夠是在數據庫上,能夠是在LDAP服務器上,能夠是xml配置文件上等),若是驗證經過後會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolde(存放該用戶的權限信息)r中,以備後面訪問資源時使用。web

總結正則表達式

     用戶登錄步驟spring

     1  首先須要通過  AuthenticationProcessingFilter 攔截器sql

     2  調用AuthenticationManager的實現數據庫

         2.1  AuthenticationManager會調用ProviderManager來獲取用戶驗證信息express

     3  驗證經過後會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolder中

權限的管理

訪問資源(即受權管理),訪問url時,會經過AbstractSecurityInterceptor攔截器攔截,其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的所有權限,再調用受權管理器AccessDecisionManager,這個受權管理器會經過spring的全局緩存SecurityContextHolder(將認證經過的用戶放入到這個安全處理器中)獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的所有權限,而後根據所配的策略(有:一票決定,一票否認,少數服從多數等),若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。

總結:

  用戶發起訪問url的請求

  1   首先    該url會被AbstractSecurityInterceptor類的繼承攔截器攔截

  2   而後調用 FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的所有權限

  3  再調用受權管理器AccessDecisionManager  

      3.1   經過spring的全局緩存SecurityContextHolder(將認證經過的用戶放入到這個安全處理器中)獲取用戶的權限信息

      3.2   獲取被攔截的url和被攔截url所需的所有權限

      3.3   根據所配的策略(有:一票決定,一票否認,少數服從多數等),若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。

雖然講得好像好複雜,讀者們可能有點暈,不過不打緊,真正經過代碼的講解在後面,讀者能夠看完後面的代碼實現,再返回看這個簡單的原理,可能會有不錯的收穫。

spring security使用實現(基於spring security3.1.4):

javaEE的入口:web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<? xml version = "1.0" encoding = "UTF-8" ?> 
< web-app version = "2.5" xmlns = "http://java.sun.com/xml/ns/javaee" 
     xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" >  
      <!--加載Spring XML配置文件 --> 
     < context-param
         < param-name >contextConfigLocation</ param-name
         < param-value > classpath:securityConfig.xml           </ param-value
     </ context-param >  
       <!-- Spring Secutiry3.1的過濾器鏈配置 --> 
     < filter
         < filter-name >springSecurityFilterChain</ filter-name
         < filter-class >org.springframework.web.filter.DelegatingFilterProxy</ filter-class
     </ filter
     < filter-mapping
         < filter-name >springSecurityFilterChain</ filter-name
         < url-pattern >/*</ url-pattern
     </ filter-mapping >  
        <!-- Spring 容器啓動監聽器 --> 
     < listener
         < listener-class >org.springframework.web.context.ContextLoaderListener</ listener-class
     </ listener >    
       <!--系統歡迎頁面 --> 
     < welcome-file-list
         < welcome-file >index.jsp</ welcome-file
     </ welcome-file-list
</ web-app >

上面那個配置不用多說了吧

直接上spring security的配置文件securityConfig.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version= "1.0" encoding= "UTF-8" ?> 
<b:beans xmlns= "http://www.springframework.org/schema/security" 
     xmlns:b= "http://www.springframework.org/schema/beans" 
     xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation="http: //www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
                         http: //www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> 
 
   <!--登陸頁面不過濾 --> 
     <http pattern= "/login.jsp" security= "none" /> 
     <http access-denied-page= "/accessDenied.jsp" > <!--當無權訪問某個頁面後,會調到該頁面提示-->
         <form-login login-page= "/login.jsp" /> 
         <!--訪問/admin.jsp資源的用戶必須具備ROLE_ADMIN的權限 --> 
         <!-- <intercept-url pattern= "/admin.jsp" access= "ROLE_ADMIN" /> --> 
         <!--訪問/**資源的用戶必須具備ROLE_USER的權限 --> 
         <!-- <intercept-url pattern= "/**" access= "ROLE_USER" /> --> 
        <!--session-management是用來防止多個用戶同時登錄一個帳號的。-->
         <session-management> 
             <concurrency-control max-sessions= "1" 
                 error- if -maximum-exceeded= "false" /> 
         </session-management> 
         <!--增長一個filter,這點與 Acegi是不同的,不能修改默認的filter了, 這個filter位於FILTER_SECURITY_INTERCEPTOR以前 --> 
         <custom-filter ref= "myFilter" before= "FILTER_SECURITY_INTERCEPTOR" /> 
     </http> 
     <!--一個自定義的filter,必須包含 authenticationManager,accessDecisionManager,securityMetadataSource三個屬性,  
         咱們的全部控制將在這三個類中實現,解釋詳見具體配置 --> 
     <b:bean id= "myFilter" 
         class = "com.erdangjiade.spring.security.MyFilterSecurityInterceptor"
         <b:property name= "authenticationManager" ref= "authenticationManager" /> 
         <b:property name= "accessDecisionManager" ref= "myAccessDecisionManagerBean" /> 
         <b:property name= "securityMetadataSource" ref= "securityMetadataSource" /> 
     </b:bean> 
     <!--驗證配置,認證管理器,實現用戶認證的入口,主要實現UserDetailsService接口便可 --> 
     <authentication-manager alias= "authenticationManager"
         <authentication-provider user-service-ref= "myUserDetailService"
             <!--若是用戶的密碼採用加密的話 <password-encoder hash= "md5" /> --> 
         </authentication-provider> 
     </authentication-manager> 
     <!--在這個類中,你就能夠從數據庫中讀入用戶的密碼,角色信息,是否鎖定,帳號是否過時等 --> 
     <b:bean id= "myUserDetailService" class = "com.erdangjiade.spring.security.MyUserDetailService" /> 
     <!--訪問決策器,決定某個用戶具備的角色,是否有足夠的權限去訪問某個資源 --> 
     <b:bean id= "myAccessDecisionManagerBean" 
         class = "com.erdangjiade.spring.security.MyAccessDecisionManager"
     </b:bean> 
     <!--資源源數據定義,將全部的資源和權限對應關係創建起來,即定義某一資源能夠被哪些角色訪問 --> 
     <b:bean id= "securityMetadataSource" 
         class = "com.erdangjiade.spring.security.MyInvocationSecurityMetadataSource" />  
 
  </b:beans>

其實全部配置都在<http></http>裏面,首先這個版本的spring security不支持了filter=none的配置了,改爲了獨立的<http pattern=」/login.jsp」 security=」none」/>,裏面你能夠配登錄頁面、權限不足的返回頁面、註銷頁面等,上面那些配置,我註銷了一些資源和權限的對應關係,筆者這裏不須要在這配死它,能夠本身寫攔截器來得到資源與權限的對應關係。

session-management是用來防止多個用戶同時登錄一個帳號的。

最重要的是筆者本身寫的攔截器myFilter(終於講到重點了),首先這個攔截器會加載在FILTER_SECURITY_INTERCEPTOR以前(配置文件上有說),最主要的是這個攔截器裏面配了三個處理類,第一個是authenticationManager,這個是處理驗證的,這裏須要特別說明的是:這個類不單隻這個攔截器用到,還有驗證攔截器AuthenticationProcessingFilter也用到 了,並且實際上的登錄驗證也是AuthenticationProcessingFilter攔截器調用authenticationManager來處理的,咱們這個攔截器只是爲了拿到驗證用戶信息而已(這裏不太清楚,由於authenticationManager筆者設了斷點,用戶登錄後再也沒調用這個類了,並且調用這個類時不是筆者本身寫的那個攔截器調用的,看了spring技術內幕這本書才知道是AuthenticationProcessingFilter攔截器調用的)。

securityMetadataSource這個用來加載資源與權限的所有對應關係的,並提供一個經過資源獲取全部權限的方法。

accessDecisionManager這個也稱爲受權器,經過登陸用戶的權限信息、資源、獲取資源所需的權限來根據不一樣的受權策略來判斷用戶是否有權限訪問資源。

authenticationManager類能夠有許多provider(提供者)提供用戶驗證信息,這裏筆者本身寫了一個類myUserDetailService來獲取用戶信息。

MyUserDetailService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.erdangjiade.spring.security; 
 
import java.util.ArrayList; 
import java.util.Collection; 
 
import org.springframework.dao.DataAccessException; 
import org.springframework.security.core.GrantedAuthority; 
import org.springframework.security.core.authority.GrantedAuthorityImpl; 
import org.springframework.security.core.userdetails.User; 
import org.springframework.security.core.userdetails.UserDetails; 
import org.springframework.security.core.userdetails.UserDetailsService; 
import org.springframework.security.core.userdetails.UsernameNotFoundException; 
 
public class MyUserDetailService implements UserDetailsService {  
 
     //登錄驗證時,經過username獲取用戶的全部權限信息, 
     //並返回User放到spring的全局緩存SecurityContextHolder中,以供受權器使用 
     public UserDetails loadUserByUsername(String username)  
             throws UsernameNotFoundException, DataAccessException {
        // GrantedAuthority  授予的權限對象  管理員權限,用戶權限    
         Collection<GrantedAuthority> auths= new ArrayList<GrantedAuthority>();  
 
         GrantedAuthorityImpl auth2= new GrantedAuthorityImpl( "ROLE_ADMIN" );  
         GrantedAuthorityImpl auth1= new GrantedAuthorityImpl( "ROLE_USER" );  
 
         if (username.equals( "lcy" )){  //若是用戶名爲 lcy 那麼就授予這兩個去權限
             auths= new ArrayList<GrantedAuthority>();  
             auths.add(auth1); 
             auths.add(auth2);       
         }      
 
         User user = new User(username, "lcy" , true , true , true , true , auths);  
         return user;   
         }  
     }

其中UserDetailsService接口是spring提供的,必須實現的。別看這個類只有一個方法,並且這麼簡單,其中內涵玄機。

讀者看到這裏可能就大感疑惑了,不是說好的用數據庫嗎?對,但別急,等筆者慢慢給大家解析。

首先,筆者爲何不用數據庫,還不是爲了讀者們測試方便,並簡化spring security的流程,讓讀者抓住主線,而不是還要煩其餘事(導入數據庫,配置數據庫,寫dao等)。

這裏筆者只是用幾個數據模擬了從數據庫中拿到的數據,也就是說ROLE_ADMIN、ROLE_USER、lcy(第一個是登錄帳號)、lcy(第二個是密碼)是從數據庫拿出來的,這個不難實現吧,若是須要數據庫時,讀者能夠用本身寫的dao經過參數username來查詢出這個用戶的權限信息(或是角色信息,就是那個ROLE_*,對必須是ROLE_開頭的,否則spring security不認帳的,實際上是spring security裏面作了一個判斷,必需要ROLE_開頭,讀者能夠百度改一下),再返回spring自帶的數據模型User便可。

這個寫應該比較清晰、靈活吧,總之數據讀者們經過什麼方法獲取都行,只要返回一個User對象就好了。(這也是筆者爲何要重寫這個類的緣由)

    經過MyUserDetailService拿到用戶信息後,authenticationManager對比用戶的密碼(即驗證用戶),而後這個AuthenticationProcessingFilter攔截器就過咯。

下面要說的是另一個攔截器,就是筆者本身寫的攔截器MyFilterSecurityInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.erdangjiade.spring.security; 
 
import java.io.IOException; 
 
import javax.servlet.Filter; 
import javax.servlet.FilterChain; 
import javax.servlet.FilterConfig; 
import javax.servlet.ServletException; 
import javax.servlet.ServletRequest; 
import javax.servlet.ServletResponse; 
 
import org.springframework.security.access.SecurityMetadataSource; 
import org.springframework.security.access.intercept.AbstractSecurityInterceptor; 
import org.springframework.security.access.intercept.InterceptorStatusToken; 
import org.springframework.security.web.FilterInvocation; 
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; 
 
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor  implements Filter {   
 
     //配置文件注入 
     private FilterInvocationSecurityMetadataSource securityMetadataSource; 
 
     //登錄後,每次訪問資源都經過這個攔截器攔截 
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {  
         FilterInvocation fi = new FilterInvocation(request, response, chain);  
         invoke(fi);   
        
 
     public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {   
         return this .securityMetadataSource;   
         }    
 
     public Class<? extends Object> getSecureObjectClass() {  
         return FilterInvocation. class ;     
         }   
 
     public void invoke(FilterInvocation fi) throws IOException, ServletException { 
         //fi裏面有一個被攔截的url 
         //裏面調用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的全部權限 
         //再調用MyAccessDecisionManager的decide方法來校驗用戶的權限是否足夠 
         InterceptorStatusToken token = super.beforeInvocation(fi);  
         try
             //執行下一個攔截器 
             fi.getChain().doFilter(fi.getRequest(), fi.getResponse());    
             } finally {  
                 super .afterInvocation(token, null );   
             }    
         }   
     public SecurityMetadataSource obtainSecurityMetadataSource() {  
         return this .securityMetadataSource;    
         }  
     public void setSecurityMetadataSource( 
             FilterInvocationSecurityMetadataSource newSource) 
     {  
         this .securityMetadataSource = newSource;  
     }  
     public void destroy() {   
 
     }    
     public void init(FilterConfig arg0) throws ServletException {   
 
     }   
}

繼承AbstractSecurityInterceptor、實現Filter是必須的。

首先,登錄後,每次訪問資源都會被這個攔截器攔截,會執行doFilter這個方法,這個方法調用了invoke方法,其中fi斷點顯示是一個url(可能重寫了toString方法吧,可是裏面還有一些方法的),最重要的是beforeInvocation這個方法,它首先會調用MyInvocationSecurityMetadataSource類的getAttributes方法獲取被攔截url所需的權限,在調用MyAccessDecisionManager類decide方法判斷用戶是否夠權限。弄完這一切就會執行下一個攔截器。

再看一下這個MyInvocationSecurityMetadataSource的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.erdangjiade.spring.security; 
 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.Map; 
 
import org.springframework.security.access.ConfigAttribute; 
import org.springframework.security.access.SecurityConfig; 
import org.springframework.security.web.FilterInvocation; 
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; 
 
import com.erdangjiade.spring.security.tool.AntUrlPathMatcher; 
import com.erdangjiade.spring.security.tool.UrlMatcher; 
 
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {  
     private UrlMatcher urlMatcher = new AntUrlPathMatcher();  
     private static Map<String, Collection<ConfigAttribute>> resourceMap = null
 
     //tomcat啓動時實例化一次 
     public MyInvocationSecurityMetadataSource() { 
         loadResourceDefine();   
         }    
     //tomcat開啓時加載一次,加載全部url和權限(或角色)的對應關係 
     private void loadResourceDefine() { 
         resourceMap = new HashMap<String, Collection<ConfigAttribute>>();  
         Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();  
         ConfigAttribute ca = new SecurityConfig( "ROLE_USER" ); 
         atts.add(ca);  
         resourceMap.put( "/index.jsp" , atts);   
         Collection<ConfigAttribute> attsno = new ArrayList<ConfigAttribute>(); 
         ConfigAttribute cano = new SecurityConfig( "ROLE_NO" ); 
         attsno.add(cano); 
         resourceMap.put( "/other.jsp" , attsno);    
         }   
 
     //參數是要訪問的url,返回這個url對於的全部權限(或角色) 
     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {  
         // 將參數轉爲url     
         String url = ((FilterInvocation)object).getRequestUrl();    
         Iterator<String>ite = resourceMap.keySet().iterator();  
         while (ite.hasNext()) {          
             String resURL = ite.next();   
             if (urlMatcher.pathMatchesUrl(resURL, url)) {  
                 return resourceMap.get(resURL);          
                 }        
             }  
         return null ;     
         }   
     public boolean supports(Class<?>clazz) {  
             return true ;   
             }  
     public Collection<ConfigAttribute> getAllConfigAttributes() {  
         return null ;   
        
     }

實現FilterInvocationSecurityMetadataSource接口也是必須的。

首先,這裏也是模擬了從數據庫中獲取信息。

其中loadResourceDefine方法不是必須的,這個只是加載全部的資源與權限的對應關係並緩存起來,避免每次獲取權限都訪問數據庫(提升性能),而後getAttributes根據參數(被攔截url)返回權限集合。

這種緩存的實現其實有一個缺點,由於loadResourceDefine方法是放在構造器上調用的,而這個類的實例化只在web服務器啓動時調用一次,那就是說loadResourceDefine方法只會調用一次,若是資源和權限的對應關係在啓動後發生了改變,那麼緩存起來的就是髒數據,而筆者這裏使用的就是緩存數據,那就會受權錯誤了。但若是資源和權限對應關係是不會改變的,這種方法性能會好不少。

如今說回有數據庫的靈活實現,讀者看到這,可能會說,這還不簡單,和上面MyUserDetailService類同樣使用dao靈活獲取數據就行啦。

若是讀者這樣想,那隻想到了一半,想一下spring的機制(依賴注入),dao須要依賴注入吧,但這是在啓動時候,那個dao可能都還沒加載,因此這裏須要讀者本身寫sessionFactory,本身寫hql或sql,對,就在loadResourceDefine方法裏面寫(這個應該會寫吧,基礎來的)。那若是說想用第二種方法呢(就是容許資源和權限的對應關係改變的那個),那更加簡單,根本不須要loadResourceDefine方法了,直接在getAttributes方法裏面調用dao(這個是加載完,後來纔會調用的,因此可使用dao),經過被攔截url獲取數據庫中的全部權限,封裝成Collection<ConfigAttribute>返回就好了。(靈活、簡單)

注意:接口UrlMatcher和實現類AntUrlPathMatcher是筆者本身寫的,這原本是spring之前版本有的,如今沒有了,可是以爲好用就用會來了,直接上代碼(讀者也能夠本身寫正則表達式驗證被攔截url和緩存或數據庫的url是否匹配):

1
2
3
4
5
6
7
8
package com.erdangjiade.spring.security.tool; 
 
public interface UrlMatcher{ 
     Object compile(String paramString); 
     boolean pathMatchesUrl(Object paramObject, String paramString); 
     String getUniversalMatchPattern();  
     boolean requiresLowerCaseUrl(); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.erdangjiade.spring.security.tool;  
import org.springframework.util.AntPathMatcher; 
import org.springframework.util.PathMatcher;  
 
   public class AntUrlPathMatcher implements UrlMatcher {   
       private boolean requiresLowerCaseUrl; 
       private PathMatcher pathMatcher;  
       public AntUrlPathMatcher()   {  
           this ( true );  
 
   }   
       public AntUrlPathMatcher( boolean requiresLowerCaseUrl)  
       {   
           this .requiresLowerCaseUrl = true
       this .pathMatcher = new AntPathMatcher();  
       this .requiresLowerCaseUrl = requiresLowerCaseUrl; 
       }  
 
       public Object compile(String path) {  
           if ( this .requiresLowerCaseUrl) {  
               return path.toLowerCase();   
               }    
           return path;   
       }   
 
       public void setRequiresLowerCaseUrl( boolean requiresLowerCaseUrl){ 
 
           this .requiresLowerCaseUrl = requiresLowerCaseUrl;  
       }  
 
       public boolean pathMatchesUrl(Object path, String url) {  
           if (( "/**" .equals(path)) || ( "**" .equals(path))) { 
               return true ;      
               }   
 
           return this .pathMatcher.match((String)path, url);  
       }  
 
       public String getUniversalMatchPattern() { 
           return "/**" ;   
      
 
       public boolean requiresLowerCaseUrl() {  
           return this .requiresLowerCaseUrl;   
       }   
 
       public String toString() {   
           return super .getClass().getName() + "[requiresLowerCase='"  
       + this .requiresLowerCaseUrl + "']" ;   
      
   }

而後MyAccessDecisionManager類的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.erdangjiade.spring.security; 
 
import java.util.Collection; 
import java.util.Iterator; 
 
import org.springframework.security.access.AccessDecisionManager; 
import org.springframework.security.access.AccessDeniedException; 
import org.springframework.security.access.ConfigAttribute; 
import org.springframework.security.access.SecurityConfig; 
import org.springframework.security.authentication.InsufficientAuthenticationException; 
import org.springframework.security.core.Authentication; 
import org.springframework.security.core.GrantedAuthority; 
 
public class MyAccessDecisionManager implements AccessDecisionManager { 
 
     //檢查用戶是否夠權限訪問資源 
     //參數authentication是從spring的全局緩存SecurityContextHolder中拿到的,裏面是用戶的權限信息 
     //參數object是url 
     //參數configAttributes所需的權限 
     public void decide(Authentication authentication, Object object,     
             Collection<ConfigAttribute> configAttributes)  
                     throws AccessDeniedException, InsufficientAuthenticationException { 
         if (configAttributes == null ){  
             return ;        
         }   
 
         Iterator<ConfigAttribute> ite=configAttributes.iterator(); 
         while (ite.hasNext()){ 
             ConfigAttribute ca=ite.next();   
             String needRole=((SecurityConfig)ca).getAttribute(); 
             for (GrantedAuthority ga : authentication.getAuthorities()){  
                 if (needRole.equals(ga.getAuthority())){   
 
                     return ;               
         }             
     }       
}  
         //注意:執行這裏,後臺是會拋異常的,可是界面會跳轉到所配的access-denied-page頁面 
         throw new AccessDeniedException( "no right" );    
}    
     public boolean supports(ConfigAttribute attribute) {  
         return true
     }   
     public boolean supports(Class<?>clazz) { 
         return true ;  
         }  
     }

接口AccessDecisionManager也是必須實現的。

decide方法裏面寫的就是受權策略了,筆者的實現是,沒有明說須要權限的(即沒有對應的權限的資源),能夠訪問,用戶具備其中一個或多個以上的權限的能夠訪問。這個就看需求了,須要什麼策略,讀者能夠本身寫其中的策略邏輯。經過就返回,不經過拋異常就好了,spring security會自動跳到權限不足頁面(配置文件上配的)。

就這樣,整個流程過了一遍。

剩下的頁面代碼

原本想給這個demo的源碼出來的,可是筆者以爲,經過這個教程一步一步讀下來,並本身敲一遍代碼,會比直接運行一遍demo印象更深入,而且更容易理解裏面的原理。

並且個人源碼其實都公佈出來了:

login.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<% @page language= "java" import = "java.util.*" pageEncoding= "UTF-8" %> 
<!DOCTYPEhtmlPUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
<html> 
<head> 
<title>登陸</title> 
</head> 
<body> 
     <form action = "j_spring_security_check" method= "POST"
     <table> 
         <tr> 
             <td>用戶:</td> 
             <td><input type = 'text' name= 'j_username' ></td> 
         </tr> 
         <tr> 
             <td>密碼:</td> 
             <td><input type = 'password' name= 'j_password' ></td> 
         </tr> 
         <tr> 
             <td><input name = "reset" type= "reset" ></td> 
             <td><input name = "submit" type= "submit" ></td> 
         </tr> 
     </table> 
     </form> 
</body> 
</html>

index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<% @page language= "java" import = "java.util.*" pageEncoding= "UTF-8" %>  
<% @taglib prefix= "sec" uri= "http://www.springframework.org/security/tags" %>  
<!DOCTYPEHTMLPUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 
<html> 
 
<head> 
 
<title>My JSP 'index.jsp' starting page</title>  
</head> 
 
<body> 
       <h3>這是首頁</h3>歡迎 
     <sec:authentication property = "name" /> ! 
 
        
     <a href= "admin.jsp" >進入admin頁面</a>  
     <a href= "other.jsp" >進入其它頁面</a>  
</body> 
 
</html>

admin.jsp:

1
2
3
4
5
6
7
8
9
10
11
<% @page language= "java" import = "java.util.*" pageEncoding= "utf-8" %> 
<!DOCTYPEHTMLPUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
<html> 
<head> 
<title>My JSP 'admin.jsp' starting page</title> 
</head> 
<body> 
     歡迎來到管理員頁面. 
       
</body> 
</html>

accessDenied.jsp:

1
2
3
4
5
6
7
8
9
10
11
<%@page language="java" import="java.util.*" pageEncoding="utf-8"%> 
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN"> 
< html
< head
< title >My JSP 'admin.jsp' starting page</ title
</ head
< body
     歡迎來到管理員頁面. 
       
</ body
</ html >

other.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> 
<% 
String path = request.getContextPath(); 
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; 
%> 
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> 
< html
   < head
     < base href="<%=basePath%>"> 
 
     < title >My JSP 'other.jsp' starting page</ title
 
     < meta http-equiv = "pragma" content = "no-cache"
     < meta http-equiv = "cache-control" content = "no-cache"
     < meta http-equiv = "expires" content = "0" >     
     < meta http-equiv = "keywords" content = "keyword1,keyword2,keyword3"
     < meta http-equiv = "description" content = "This is my page"
     <!--
     <link rel="stylesheet" type="text/css" href="styles.css">
     --> 
 
   </ head
 
   < body
     < h3 >這裏是Other頁面</ h3
   </ body
</ html >

項目圖:

最後的話:

雖然筆者沒給讀者們demo,可是全部源碼和jar包都在這個教程裏面,爲何不直接給?筆者的目的是讓讀者跟着教程敲一遍代碼,使印象深入(相信作這行的都知道,一樣一段代碼,看過和敲過的區別是多麼的大),因此不惜如此來強迫你們了。

因爲筆者有常常上csdn博客的習慣,因此讀者有什麼不懂的(或者指教的),筆者盡力解答。

轉載請標註本文連接:http://blog.csdn.net/u012367513/article/details/38866465

補充:

(2014年11月21日第一次補充):

第一點:

MyUserDetailService這個類負責的是隻是獲取登錄用戶的詳細信息(包括密碼、角色等),不負責和前端傳過來的密碼對比,只需返回User對象,後會有其餘類根據User對象對比密碼的正確性(框架幫咱們作)。

第二點:

記得MyInvocationSecurityMetadataSource這個類是負責的是獲取角色與url資源的全部對應關係,並根據url查詢對應的全部角色。

今天爲一個項目搭安全架構時,第一,發現上面MyInvocationSecurityMetadataSource這個類的代碼有個bug:

上面的代碼中,將全部的對應關係緩存到resourceMap,key是url,value是這個url對應全部角色。

getAttributes方法中,只要匹配到一個url就返回這個url對應全部角色,再也不匹配後面的url,問題來了,當url有交集時,就有可能漏掉一些角色了:若有兩個 url ,第一個是 /** ,第二個是 /role1/index.jsp ,第一個固然須要很高的權限了(由於能匹配全部 url ,便可以訪問全部 url ),假設它須要的角色是 ROLE_ADMIN (不是通常人擁有的),第二個所需的角色是 ROLE_1 。    當我用 ROLE_1 這個角色訪問 /role1/index.jsp 時,在getAttributes方法中,當先迭代了 /** 這個url,它就能匹配 /role1/index.jsp 這個url,並直接返回 /** 這個url對應的全部角色(在這,也就ROLE_ADMIN)給MyAccessDecisionManager這個投票類,  MyAccessDecisionManager這個類中再對比 用戶的角色 ROLE_1 ,就會發現不匹配。    最後,明明能夠有權訪問的 url ,卻不能訪問了。

第二,以前不是說緩存全部對應關係,須要讀者本身寫sessionFactory(由於在實例化這個類時,配置的sessionFactory可能還沒實例化或dao還沒加載好),既然這樣,那筆者能夠不在構造方法中加載對應關係,能夠在第一次調用getAttributes方法時再加載(用靜態變量緩存起來,第二次就不用再加載了,     注:其實這樣不是很嚴謹,不過筆者這裏的對應關係是不變的,單例性不需很強,更嚴謹的請參考筆者另外一篇博文設計模式之單件模式)。

修改過的MyInvocationSecurityMetadataSource類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.lcy.bookcrossing.springSecurity; 
 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.HashMap; 
import java.util.HashSet; 
import java.util.Iterator; 
import java.util.List; 
import java.util.Map; 
import java.util.Set; 
 
import javax.annotation.Resource; 
 
import org.springframework.security.access.ConfigAttribute; 
import org.springframework.security.access.SecurityConfig; 
import org.springframework.security.web.FilterInvocation; 
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; 
 
import com.lcy.bookcrossing.bean.RoleUrlResource; 
import com.lcy.bookcrossing.dao.IRoleUrlResourceDao; 
import com.lcy.bookcrossing.springSecurity.tool.AntUrlPathMatcher; 
import com.lcy.bookcrossing.springSecurity.tool.UrlMatcher; 
 
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {  
     private UrlMatcher urlMatcher = new AntUrlPathMatcher();  
//  private static Map<String, Collection<ConfigAttribute>> resourceMap = null; 
 
     //將全部的角色和url的對應關係緩存起來 
     private static List<RoleUrlResource> rus = null
 
     @Resource 
     private IRoleUrlResourceDao roleUrlDao; 
 
     //tomcat啓動時實例化一次 
     public MyInvocationSecurityMetadataSource() { 
//      loadResourceDefine();   
         }    
     //tomcat開啓時加載一次,加載全部url和權限(或角色)的對應關係 
     /*private void loadResourceDefine() {
         resourceMap = new HashMap<String, Collection<ConfigAttribute>>(); 
         Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>(); 
         ConfigAttribute ca = new SecurityConfig("ROLE_USER");
         atts.add(ca); 
         resourceMap.put("/index.jsp", atts);  
         Collection<ConfigAttribute> attsno =new ArrayList<ConfigAttribute>();
         ConfigAttribute cano = new SecurityConfig("ROLE_NO");
         attsno.add(cano);
         resourceMap.put("/other.jsp", attsno);   
         }  */ 
 
     //參數是要訪問的url,返回這個url對於的全部權限(或角色) 
     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {  
         // 將參數轉爲url     
         String url = ((FilterInvocation)object).getRequestUrl();    
 
         //查詢全部的url和角色的對應關係 
         if(rus == null){ 
         rus = roleUrlDao.findAll(); 
        
 
         //匹配全部的url,並對角色去重 
         Set<String> roles = new HashSet<String>(); 
         for(RoleUrlResource ru : rus){ 
             if (urlMatcher.pathMatchesUrl(ru.getUrlResource().getUrl(), url)) {  
                         roles.add(ru.getRole().getRoleName()); 
                 }      
        
         Collection<ConfigAttribute> cas = new ArrayList<ConfigAttribute>();  
         for(String role : roles){ 
             ConfigAttribute ca = new SecurityConfig(role); 
             cas.add(ca);  
        
         return cas; 
 
         /*Iterator<String> ite = resourceMap.keySet().iterator(); 
         while (ite.hasNext()) {         
             String resURL = ite.next();  
             if (urlMatcher.pathMatchesUrl(resURL, url)) { 
                 return resourceMap.get(resURL);         
                 }       
            
         return null;    */ 
         }   
     public boolean supports(Class<?>clazz) {  
             return true ;   
             }  
     public Collection<ConfigAttribute> getAllConfigAttributes() {  
         return null ;   
        
     }

以上代碼,在getAttributes方法中緩存起全部的對應關係(可使用依賴注入了),並匹配全部 url ,對角色進行去重(由於多個url可能有重複的角色),這樣就能修復那個bug了。

轉載請標註本文連接:http://blog.csdn.net/u012367513/article/details/38866465


(2014年12月10日第二次補充):

此次補充不是修上面的bug,而是添加新功能。

咱們知道,上面的實現的登錄界面只能傳遞兩個參數(j_username,j_password),並且是固定的。

老是有一個項目需求,咱們的角色(ROLE_)不是不少,只需在登錄界面選擇一種角色就好了,那麼如何將角色類型傳遞到spring security呢,如今筆者對配置文件再修改修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<? xml version = "1.0" encoding = "UTF-8" ?> 
< b:beans xmlns = "http://www.springframework.org/schema/security" 
     xmlns:b = "http://www.springframework.org/schema/beans" 
     xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
                         http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> 
 
  <!-- 配置不須要安全管理的界面 --> 
      < http pattern = "/jsp/css/**" security = "none" ></ http
      < http pattern = "/jsp/js/**" security = "none" ></ http
      < http pattern = "/jsp/images/**" security = "none" ></ http
      < http pattern = "/login.jsp" security = "none" /> 
      < http pattern = "/accessDenied.jsp" security = "none" /> 
          < http pattern = "/index.jsp" security = "none" /> 
 
         < http use-expressions = 'true' entry-point-ref = "myAuthenticationEntryPoint" access-denied-page = "/accessDenied.jsp"
 
                 <!-- 使用本身自定義的登錄認證過濾器 --> <!-- 這裏必定要註釋掉,由於咱們須要重寫它的過濾器 --> 
                 <!-- <form-login login-page="/login.jsp" 
                 authentication-failure-url="/accessDenied.jsp"     
         default-target-url="/index.jsp" 
                  /> --> 
                 <!--訪問/admin.jsp資源的用戶必須具備ROLE_ADMIN的權限 --> 
                 <!-- <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" /> --> 
                 <!--訪問/**資源的用戶必須具備ROLE_USER的權限 --> 
                 <!-- <intercept-url pattern="/**" access="ROLE_USER" /> --> 
                 < session-management
                         < concurrency-control max-sessions = "1" 
                                 error-if-maximum-exceeded = "false" /> 
                 </ session-management
 
                 <!-- 認證和受權 --> <!-- 重寫登錄認證的過濾器,使咱們能夠拿到任何參數  --> 
                 < custom-filter ref = "myAuthenticationFilter" position = "FORM_LOGIN_FILTER"  /> 
                 < custom-filter ref = "myFilter" before = "FILTER_SECURITY_INTERCEPTOR" /> 
 
                  <!-- 登出管理 --> 
         < logout invalidate-session = "true" logout-url = "/j_spring_security_logout" /> 
 
         </ http
 
         <!-- 未登陸的切入點 --> <!-- 須要有個切入點 --> 
     < b:bean id = "myAuthenticationEntryPoint" class = "org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"
         < b:property name = "loginFormUrl" value = "/login.jsp" ></ b:property
     </ b:bean
 
         <!-- 登陸驗證器:用戶有沒有登陸的資格 --> <!-- 這個就是重寫的認證過濾器 --> 
     < b:bean id = "myAuthenticationFilter" class = "com.lcy.springSecurity.MyAuthenticationFilter"
         < b:property name = "authenticationManager" ref = "authenticationManager" /> 
         < b:property name = "filterProcessesUrl" value = "/j_spring_security_check" /> 
         < b:property name = "authenticationSuccessHandler"
             < b:bean class = "org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"
                 < b:property name = "defaultTargetUrl" value = "/index.jsp" /> 
             </ b:bean
         </ b:property
         < b:property name = "authenticationFailureHandler"
             < b:bean class = "org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
                 < b:property name = "defaultFailureUrl" value = "/accessDenied.jsp" /> 
             </ b:bean
         </ b:property
     </ b:bean
 
         <!--一個自定義的filter,必須包含 authenticationManager,accessDecisionManager,securityMetadataSource三個屬性,咱們的全部控制將在這三個類中實現,解釋詳見具體配置 --> 
         < b:bean id = "myFilter" 
                 class = "com.lcy.springSecurity.MyFilterSecurityInterceptor"
                 < b:property name = "authenticationManager" ref = "authenticationManager" /> 
                 < b:property name = "accessDecisionManager" ref = "myAccessDecisionManagerBean" /> 
                 < b:property name = "securityMetadataSource" ref = "securityMetadataSource" /> 
         </ b:bean
         <!--驗證配置,認證管理器,實現用戶認證的入口,主要實現UserDetailsService接口便可 --> 
         < authentication-manager alias = "authenticationManager"
                 < authentication-provider user-service-ref = "myUserDetailService"
                         <!--若是用戶的密碼採用加密的話 <password-encoder hash="md5" /> --> 
                         <!-- <password-encoder hash="md5" /> --> 
                 </ authentication-provider
         </ authentication-manager
         <!--在這個類中,你就能夠從數據庫中讀入用戶的密碼,角色信息,是否鎖定,帳號是否過時等 --> 
         < b:bean id = "myUserDetailService" class = "com.lcy.springSecurity.MyUserDetailService" /> 
         <!--訪問決策器,決定某個用戶具備的角色,是否有足夠的權限去訪問某個資源 --> 
         < b:bean id = "myAccessDecisionManagerBean" 
                 class = "com.lcy.springSecurity.MyAccessDecisionManager"
         </ b:bean
         <!--資源源數據定義,將全部的資源和權限對應關係創建起來,即定義某一資源能夠被哪些角色訪問 --> 
         < b:bean id = "securityMetadataSource" 
                 class = "com.lcy.springSecurity.MyInvocationSecurityMetadataSource" />  
 
  </ b:beans >

我如今的項目須要的是,角色只要管理員、教師、學生,因此MyAuthenticationFilter(重寫的認證過濾器):

1
2
3
4
5
6
7
8
9
10
11
相關文章
相關標籤/搜索