1. 程式人生 > >shiro許可權框架整理

shiro許可權框架整理

Shiro簡介

SpringMVC整合Shiro,Shiro是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理等功能。

 

Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份;

Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權;即判斷使用者是否能做事情,常見的如:驗證某個使用者是否擁有某個角色。或者細粒度的驗證某個使用者對某個資源是否具有某個許可權;

Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;

Cryptography:

加密,保護資料的安全性,如密碼加密儲存到資料庫,而不是明文儲存;

Web Support:Web支援,可以非常容易的整合到Web環境;

Caching:快取,比如使用者登入後,其使用者資訊、擁有的角色/許可權不必每次去查,這樣可以提高效率;

Concurrency:shiro支援多執行緒應用的併發驗證,即如在一個執行緒中開啟另一個執行緒,能把許可權自動傳播過去;

Testing:提供測試支援;

Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行訪問;

Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。

記住一點,Shiro不會去維護使用者、維護許可權;這些需要我們自己去設計/提供;然後通過相應的介面注入給Shiro即可。

首先,我們從外部來看Shiro吧,即從應用程式角度的來觀察如何使用Shiro完成工作。如下圖:

可以看到:應用程式碼直接互動的物件是Subject,也就是說Shiro的對外API核心就是Subject;其每個API的含義:

Subject:主體,代表了當前“使用者”,這個使用者不一定是一個具體的人,與當前應用互動的任何東西都是Subject,如網路爬蟲,機器人等;即一個抽象概念;所有Subject都繫結到SecurityManager,與Subject的所有互動都會委託給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者;

SecurityManager:

安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理著所有Subject;可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料來源。

       接下來我們來從Shiro內部來看下Shiro的架構,如下圖所示:

 

Subject:主體,可以看到主體可以是任何可以與應用互動的“使用者”;

SecurityManager:相當於SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心臟;所有具體的互動都通過SecurityManager進行控制;它管理著所有Subject、且負責進行認證和授權、及會話、快取的管理。

Authenticator:認證器,負責主體認證的,這是一個擴充套件點,如果使用者覺得Shiro預設的不好,可以自定義實現;其需要認證策略(Authentication Strategy),即什麼情況下算使用者認證通過了;

Authrizer:授權器,或者訪問控制器,用來決定主體是否有許可權進行相應的操作;即控制著使用者能訪問應用中的哪些功能;

Realm:可以有1個或多個Realm,可以認為是安全實體資料來源,即用於獲取安全實體的;可以是JDBC實現,也可以是LDAP實現,或者記憶體實現等等;由使用者提供;注意:Shiro不知道你的使用者/許可權儲存在哪及以何種格式儲存;所以我們一般在應用中都需要實現自己的Realm;

SessionManager:如果寫過Servlet就應該知道Session的概念,Session呢需要有人去管理它的生命週期,這個元件就是SessionManager;而Shiro並不僅僅可以用在Web環境,也可以用在如普通的JavaSE環境、EJB等環境;所有呢,Shiro就抽象了一個自己的Session來管理主體與應用之間互動的資料;這樣的話,比如我們在Web環境用,剛開始是一臺Web伺服器;接著又上了臺EJB伺服器;這時想把兩臺伺服器的會話資料放到一個地方,這個時候就可以實現自己的分散式會話(如把資料放到Memcached伺服器);

SessionDAO:DAO大家都用過,資料訪問物件,用於會話的CRUD,比如我們想把Session儲存到資料庫,那麼可以實現自己的SessionDAO,通過如JDBC寫到資料庫;比如想把Session放到Memcached中,可以實現自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache進行快取,以提高效能;

CacheManager:快取控制器,來管理如使用者、角色、許可權等的快取的;因為這些資料基本上很少去改變,放到快取中後可以提高訪問的效能

Cryptography:密碼模組,Shiro提高了一些常見的加密元件用於如密碼加密/解密的。

自定義Realm

public class ShiroRealm extends AuthorizingRealm{

}

1、ShiroRealm父類AuthorizingRealm將獲取Subject相關資訊分成兩步:獲取身份驗證資訊(doGetAuthenticationInfo)及授權資訊(doGetAuthorizationInfo);

2、doGetAuthenticationInfo獲取身份驗證相關資訊:首先根據傳入的使用者名稱獲取User資訊;然後如果user為空,那麼丟擲沒找到帳號異常UnknownAccountException;如果user找到但鎖定了丟擲鎖定異常LockedAccountException;最後生成AuthenticationInfo資訊,交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配,如果不匹配將丟擲密碼錯誤異常IncorrectCredentialsException;另外如果密碼重試此處太多將丟擲超出重試次數異常ExcessiveAttemptsException;在組裝SimpleAuthenticationInfo資訊時,需要傳入:身份資訊(使用者名稱)、憑據(密文密碼)、鹽(username+salt),CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進行匹配。

3、doGetAuthorizationInfo獲取授權資訊:PrincipalCollection是一個身份集合,因為我們現在就一個Realm,所以直接呼叫getPrimaryPrincipal得到之前傳入的使用者名稱即可;然後根據使用者名稱呼叫UserService介面獲取角色及許可權資訊。

AuthenticationToken

 

AuthenticationToken用於收集使用者提交的身份(如使用者名稱)及憑據(如密碼):

  1. public interface AuthenticationToken extends Serializable {  
  2.     Object getPrincipal(); //身份  
  3.     Object getCredentials(); //憑據  
  4. }  

擴充套件介面RememberMeAuthenticationToken:提供了“boolean isRememberMe()”現“記住我”的功能;

擴充套件介面是HostAuthenticationToken:提供了“String getHost()”方法用於獲取使用者“主機”的功能。

Shiro提供了一個直接拿來用的UsernamePasswordToken,用於實現使用者名稱/密碼Token組,另外其實現了RememberMeAuthenticationToken和HostAuthenticationToken,可以實現記住我及主機驗證的支援。

AuthenticationInfo

 

AuthenticationInfo有兩個作用:

1、如果Realm是AuthenticatingRealm子類,則提供給AuthenticatingRealm內部使用的CredentialsMatcher進行憑據驗證;(如果沒有繼承它需要在自己的Realm中自己實現驗證);

2、提供給SecurityManager來建立Subject(提供身份資訊);

MergableAuthenticationInfo用於提供在多Realm時合併AuthenticationInfo的功能,主要合併Principal、如果是其他的如credentialsSalt,會用後邊的資訊覆蓋前邊的。

比如HashedCredentialsMatcher,在驗證時會判斷AuthenticationInfo是否是SaltedAuthenticationInfo子類,來獲取鹽資訊。

Account相當於我們之前的User,SimpleAccount是其一個實現;在IniRealm、PropertiesRealm這種靜態建立帳號資訊的場景中使用,這些Realm直接繼承了SimpleAccountRealm,而SimpleAccountRealm提供了相關的API來動態維護SimpleAccount;即可以通過這些API來動態增刪改查SimpleAccount;動態增刪改查角色/許可權資訊。及如果您的帳號不是特別多,可以使用這種方式,具體請參考SimpleAccountRealm Javadoc。

其他情況一般返回SimpleAuthenticationInfo即可。

PrincipalCollection

 

因為我們可以在Shiro中同時配置多個Realm,所以呢身份資訊可能就有多個;因此其提供了PrincipalCollection用於聚合這些身份資訊:

  1. public interface PrincipalCollection extends Iterable, Serializable {  
  2.     Object getPrimaryPrincipal(); //得到主要的身份  
  3.     <T> T oneByType(Class<T> type); //根據身份型別獲取第一個  
  4.     <T> Collection<T> byType(Class<T> type); //根據身份型別獲取一組  
  5.     List asList(); //轉換為List  
  6.     Set asSet(); //轉換為Set  
  7.     Collection fromRealm(String realmName); //根據Realm名字獲取  
  8.     Set<String> getRealmNames(); //獲取所有身份驗證通過的Realm名字  
  9.     boolean isEmpty(); //判斷是否為空  

10. }  

因為PrincipalCollection聚合了多個,此處最需要注意的是getPrimaryPrincipal,如果只有一個Principal那麼直接返回即可,如果有多個Principal,則返回第一個(因為內部使用Map儲存,所以可以認為是返回任意一個);oneByType / byType根據憑據的型別返回相應的Principal;fromRealm根據Realm名字(每個Principal都與一個Realm關聯)獲取相應的Principal。

目前Shiro只提供了一個實現SimplePrincipalCollection,還記得之前的AuthenticationStrategy實現嘛,用於在多Realm時判斷是否滿足條件的,在大多數實現中(繼承了AbstractAuthenticationStrategy)afterAttempt方法會進行AuthenticationInfo(實現了MergableAuthenticationInfo)的merge,比如SimpleAuthenticationInfo會合並多個Principal為一個PrincipalCollection。

AuthorizationInfo

 

AuthorizationInfo用於聚合授權資訊的:

  1. public interface AuthorizationInfo extends Serializable {  
  2.     Collection<String> getRoles(); //獲取角色字串資訊  
  3.     Collection<String> getStringPermissions(); //獲取許可權字串資訊  
  4.     Collection<Permission> getObjectPermissions(); //獲取Permission物件資訊  
  5. }   

當我們使用AuthorizingRealm時,如果身份驗證成功,在進行授權時就通過doGetAuthorizationInfo方法獲取角色/許可權資訊用於授權驗證。

Shiro提供了一個實現SimpleAuthorizationInfo,大多數時候使用這個即可。

對於Account及SimpleAccount,之前的【6.3 AuthenticationInfo】已經介紹過了,用於SimpleAccountRealm子類,實現動態角色/許可權維護的。

Subject

 

Subject是Shiro的核心物件,基本所有身份驗證、授權都是通過Subject完成。

1、身份資訊獲取

Java程式碼  

  1. Object getPrincipal(); //Primary Principal  
  2. PrincipalCollection getPrincipals(); // PrincipalCollection   

2、身份驗證

Java程式碼  

  1. void login(AuthenticationToken token) throws AuthenticationException;  
  2. boolean isAuthenticated();  
  3. boolean isRemembered();  

通過login登入,如果登入失敗將丟擲相應的AuthenticationException,如果登入成功呼叫isAuthenticated就會返回true,即已經通過身份驗證;如果isRemembered返回true,表示是通過記住我功能登入的而不是呼叫login方法登入的。isAuthenticated/isRemembered是互斥的,即如果其中一個返回true,另一個返回false。

3、角色授權驗證 

Java程式碼  

  1. boolean hasRole(String roleIdentifier);  
  2. boolean[] hasRoles(List<String> roleIdentifiers);  
  3. boolean hasAllRoles(Collection<String> roleIdentifiers);  
  4. void checkRole(String roleIdentifier) throws AuthorizationException;  
  5. void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;  
  6. void checkRoles(String... roleIdentifiers) throws AuthorizationException;   

hasRole*進行角色驗證,驗證後返回true/false;而checkRole*驗證失敗時丟擲AuthorizationException異常。 

4、許可權授權驗證

Java程式碼  

  1. boolean isPermitted(String permission);  
  2. boolean isPermitted(Permission permission);  
  3. boolean[] isPermitted(String... permissions);  
  4. boolean[] isPermitted(List<Permission> permissions);  
  5. boolean isPermittedAll(String... permissions);  
  6. boolean isPermittedAll(Collection<Permission> permissions);  
  7. void checkPermission(String permission) throws AuthorizationException;  
  8. void checkPermission(Permission permission) throws AuthorizationException;  
  9. void checkPermissions(String... permissions) throws AuthorizationException;  

10. void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;  

isPermitted*進行許可權驗證,驗證後返回true/false;而checkPermission*驗證失敗時丟擲AuthorizationException。

5、會話

Java程式碼  

  1. Session getSession(); //相當於getSession(true)  
  2. Session getSession(boolean create);    

類似於Web中的會話。如果登入成功就相當於建立了會話,接著可以使用getSession獲取;如果create=false如果沒有會話將返回null,而create=true如果沒有會話會強制建立一個。

6、退出 

Java程式碼  

  1. void logout();  

7、RunAs  

Java程式碼  

  1. void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;  
  2. boolean isRunAs();  
  3. PrincipalCollection getPreviousPrincipals();  
  4. PrincipalCollection releaseRunAs();   

RunAs即實現“允許A假設為B身份進行訪問”;通過呼叫subject.runAs(b)進行訪問;接著呼叫subject.getPrincipals將獲取到B的身份;此時呼叫isRunAs將返回true;而a的身份需要通過subject. getPreviousPrincipals獲取;如果不需要RunAs了呼叫subject. releaseRunAs即可。

8、多執行緒

Java程式碼  

  1. <V> V execute(Callable<V> callable) throws ExecutionException;  
  2. void execute(Runnable runnable);  
  3. <V> Callable<V> associateWith(Callable<V> callable);  
  4. Runnable associateWith(Runnable runnable);   

實現執行緒之間的Subject傳播,因為Subject是執行緒繫結的;因此在多執行緒執行中需要傳播到相應的執行緒才能獲取到相應的Subject。最簡單的辦法就是通過execute(runnable/callable例項)直接呼叫;或者通過associateWith(runnable/callable例項)得到一個包裝後的例項;它們都是通過:1、把當前執行緒的Subject繫結過去;2、線上程執行結束後自動釋放。

Subject自己不會實現相應的身份驗證/授權邏輯,而是通過DelegatingSubject委託給SecurityManager實現;及可以理解為Subject是一個面門。

對於Subject的構建一般沒必要我們去建立;一般通過SecurityUtils.getSubject()獲取:

Java程式碼  

  1. public static Subject getSubject() {  
  2.     Subject subject = ThreadContext.getSubject();  
  3.     if (subject == null) {  
  4.         subject = (new Subject.Builder()).buildSubject();  
  5.         ThreadContext.bind(subject);  
  6.     }  
  7.     return subject;  
  8. }   

即首先檢視當前執行緒是否綁定了Subject,如果沒有通過Subject.Builder構建一個然後繫結到現場返回。

如果想自定義建立,可以通過:

Java程式碼  

  1. new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()  

這種可以建立相應的Subject例項了,然後自己繫結到執行緒即可。在new Builder()時如果沒有傳入SecurityManager,自動呼叫SecurityUtils.getSecurityManager獲取;也可以自己傳入一個例項。

Shiro的jstl標籤

Shiro提供了JSTL標籤用於在JSP/GSP頁面進行許可權控制,如根據登入使用者顯示相應的頁面按鈕。

匯入標籤庫

Java程式碼  

  1. <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>  

標籤庫定義在shiro-web.jar包下的META-INF/shiro.tld中定義。

guest標籤 

Java程式碼  

  1. <shiro:guest>  
  2. 歡迎遊客訪問,<a href="${pageContext.request.contextPath}/login.jsp">登入</a>  
  3. </shiro:guest>   

使用者沒有身份驗證時顯示相應資訊,即遊客訪問資訊。

user標籤 

Java程式碼  

  1. <shiro:user>  
  2. 歡迎[<shiro:principal/>]登入,<a href="${pageContext.request.contextPath}/logout">退出</a>  
  3. </shiro:user>   

使用者已經身份驗證/記住我登入後顯示相應的資訊。

authenticated標籤 

Java程式碼  

  1. <shiro:authenticated>  
  2.     使用者[<shiro:principal/>]已身份驗證通過  
  3. </shiro:authenticated>   

使用者已經身份驗證通過,即Subject.login登入成功,不是記住我登入的。    

notAuthenticated標籤

<shiro:notAuthenticated>

    未身份驗證(包括記住我)

</shiro:notAuthenticated> 

使用者已經身份驗證通過,即沒有呼叫Subject.login進行登入,包括記住我自動登入的也屬於未進行身份驗證。 

principal標籤 

<shiro: principal/>

顯示使用者身份資訊,預設呼叫Subject.getPrincipal()獲取,即Primary Principal。

Java程式碼 

  1. <shiro:principal type="java.lang.String"/>  

相當於Subject.getPrincipals().oneByType(String.class)。 

Java程式碼 

  1. <shiro:principal type="java.lang.String"/>  

相當於Subject.getPrincipals().oneByType(String.class)。

Java程式碼 

  1. <shiro:principal property="username"/>  

相當於((User)Subject.getPrincipals()).getUsername()。   

hasRole標籤 

Java程式碼 

  1. <shiro:hasRole name="admin">  
  2.     使用者[<shiro:principal/>]擁有角色admin<br/>  
  3. </shiro:hasRole>   

如果當前Subject有角色將顯示body體內容。

hasAnyRoles標籤 

Java程式碼 

  1. <shiro:hasAnyRoles name="admin,user">  
  2.     使用者[<shiro:principal/>]擁有角色admin或user<br/>  
  3. </shiro:hasAnyRoles>   

如果當前Subject有任意一個角色(或的關係)將顯示body體內容。 

lacksRole標籤 

Java程式碼 

  1. <shiro:lacksRole name="abc">  
  2.     使用者[<shiro:principal/>]沒有角色abc<br/>  
  3. </shiro:lacksRole>   

如果當前Subject沒有角色將顯示body體內容。 

hasPermission標籤

Java程式碼 

  1. <shiro:hasPermission name="user:create">  
  2.     使用者[<shiro:principal/>]擁有許可權user:create<br/>  
  3. </shiro:hasPermission>   

如果當前Subject有許可權將顯示body體內容。 

lacksPermission標籤

Java程式碼 

  1. <shiro:lacksPermission name="org:create">  
  2.     使用者[<shiro:principal/>]沒有許可權org:create<br/>  
  3. </shiro:lacksPermission>   

如果當前Subject沒有許可權將顯示body體內容。

另外又提供了幾個許可權控制相關的標籤:

Shiro與web

與spring整合:在Web.xml中

  1. <filter>  
  2.     <filter-name>shiroFilter</filter-name>  
  3.     <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
  4.     <init-param>  
  5.         <param-name>targetFilterLifecycle</param-name>  
  6.         <param-value>true</param-value>  
  7.     </init-param>  
  8. </filter>  
  9. <filter-mapping>  
  10. 10.     <filter-name>shiroFilter</filter-name>  
  11. 11.     <url-pattern>/*</url-pattern>  

12. </filter-mapping>   

DelegatingFilterProxy作用是自動到spring容器查詢名字為shiroFilter(filter-name)的bean並把所有Filter的操作委託給它。然後將ShiroFilter配置到spring容器即可:

Shiro整合spring

  1. <!-- 快取管理器 使用Ehcache實現 -->  
  2. <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">  
  3.     <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>  
  4. </bean>  
  5. <!-- 憑證匹配器 -->  
  6. <bean id="credentialsMatcher" class="  
  7. com.github.zhangkaitao.shiro.chapter12.credentials.RetryLimitHashedCredentialsMatcher">  
  8.     <constructor-arg ref="cacheManager"/>  
  9. 10.     <property name="hashAlgorithmName" value="md5"/>  
  10. 11.     <property name="hashIterations" value="2"/>  
  11. 12.     <property name="storedCredentialsHexEncoded" value="true"/>  

13. </bean>  

  1. 14.   

15. <!-- Realm實現 -->  

16. <bean id="userRealm" class="com.github.zhangkaitao.shiro.chapter12.realm.UserRealm">  

  1. 17.     <property name="userService" ref="userService"/>  
  2. 18.     <property name="credentialsMatcher" ref="credentialsMatcher"/>  
  3. 19.     <property name="cachingEnabled" value="true"/>  
  4. 20.     <property name="authenticationCachingEnabled" value="true"/>  
  5. 21.     <property name="authenticationCacheName" value="authenticationCache"/>  
  6. 22.     <property name="authorizationCachingEnabled" value="true"/>  
  7. 23.     <property name="authorizationCacheName" value="authorizationCache"/>  

24. </bean>  

25. <!-- 會話ID生成器 -->  

26. <bean id="sessionIdGenerator"   

27. class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>  

28. <!-- 會話DAO -->  

29. <bean id="sessionDAO"   

30. class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">  

  1. 31.     <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>  
  2. 32.     <property name="sessionIdGenerator" ref="sessionIdGenerator"/>  

33. </bean>  

34. <!-- 會話驗證排程器 -->  

35. <bean id="sessionValidationScheduler"   

36. class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">  

  1. 37.     <property name="sessionValidationInterval" value="1800000"/>  
  2. 38.     <property name="sessionManager" ref="sessionManager"/>  

39. </bean>  

40. <!-- 會話管理器 -->  

41. <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">  

  1. 42.     <property name="globalSessionTimeout" value="1800000"/>  
  2. 43.     <property name="deleteInvalidSessions" value="true"/>  
  3. 44.     <property name="sessionValidationSchedulerEnabled" value="true"/>  
  4. 45.    <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>  
  5. 46.     <property name="sessionDAO" ref="sessionDAO"/>  

47. </bean>  

48. <!-- 安全管理器 -->  

49. <bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">  

  1. 50.     <property name="realms">  
  2. 51.         <list><ref bean="userRealm"/></list>  
  3. 52.     </property>  
  4. 53.     <property name="sessionManager" ref="sessionManager"/>  
  5. 54.     <property name="cacheManager" ref="cacheManager"/>  

55. </bean>  

56. <!-- 相當於呼叫SecurityUtils.setSecurityManager(securityManager) -->  

57. <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">  

58. <property name="staticMethod"   

59. value="org.apache.shiro.SecurityUtils.setSecurityManager"/>  

  1. 60.     <property name="arguments" ref="securityManager"/>  

61. </bean>  

62. <!-- Shiro生命週期處理器-->  

63. <bean id="lifecycleBeanPostProcessor"   

64. class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>  

可以看出,只要把之前的ini配置翻譯為此處的spring xml配置方式即可,無須多解釋。LifecycleBeanPostProcessor用於在實現了Initializable介面的Shiro bean初始化時呼叫Initializable介面回撥,在實現了Destroyable介面的Shiro bean銷燬時呼叫 Destroyable介面回撥。如UserRealm就實現了Initializable,而DefaultSecurityManager實現了Destroyable。具體可以檢視它們的繼承關係。

Web應用:

Web應用和普通JavaSE應用的某些配置是類似的,此處只提供一些不一樣的配置,詳細配置可以參考spring-shiro-web.xml。 

  1. <!-- 會話Cookie模板 -->  
  2. <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">  
  3.     <constructor-arg value="sid"/>  
  4.     <property name="httpOnly" value="true"/>  
  5.     <property name="maxAge" value="180000"/>  
  6. </bean>  
  7. <!-- 會話管理器 -->  
  8. <bean id="sessionManager"   
  9. class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">  
  10. 10.     <property name="globalSessionTimeout" value="1800000"/>  
  11. 11.     <property name="deleteInvalidSessions" value="true"/>  
  12. 12.     <property name="sessionValidationSchedulerEnabled" value="true"/>  
  13. 13.     <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>  
  14. 14.     <property name="sessionDAO" ref="sessionDAO"/>  
  15. 15.     <property name="sessionIdCookieEnabled" value="true"/>  
  16. 16.     <property name="sessionIdCookie" ref="sessionIdCookie"/>  

17. </bean>  

18. <!-- 安全管理器 -->  

19. <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  

20. <property name="realm" ref="userRealm"/>  

  1. 21.     <property name="sessionManager" ref="sessionManager"/>  
  2. 22.     <property name="cacheManager" ref="cacheManager"/>  

23. </bean>   


1、sessionIdCookie是用於生產Session ID Cookie的模板;

2、會話管理器使用用於web環境的DefaultWebSessionManager;

3、安全管理器使用用於web環境的DefaultWebSecurityManager。

  1. <!-- 基於Form表單的身份驗證過濾器 -->  
  2. <bean id="formAuthenticationFilter"   
  3. class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">  
  4.     <property name="usernameParam" value="username"/>  
  5.     <property name="passwordParam" value="password"/>  
  6.     <property name="loginUrl" value="/login.jsp"/>  
  7. </bean>  
  8. <!-- Shiro的Web過濾器 -->  
  9. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
  10. 10.     <property name="securityManager" ref="securityManager"/>  
  11. 11.     <property name="loginUrl" value="/login.jsp"/>  
  12. 12.     <property name="unauthorizedUrl" value="/unauthorized.jsp"/>  
  13. 13.     <property name="filters">  
  14. 14.         <util:map>  
  15. 15.             <entry key="authc" value-ref="formAuthenticationFilter"/>  
  16. 16.         </util:map>  
  17. 17.     </property>  
  18. 18.     <property name="filterChainDefinitions">  
  19. 19.         <value>  
  20. 20.             /index.jsp = anon  
  21. 21.             /unauthorized.jsp = anon  
  22. 22.             /login.jsp = authc  
  23. 23.             /logout = logout  
  24. 24.             /** = user  
  25. 25.         </value>  
  26. 26.     </property>  

27. </bean>   

1、formAuthenticationFilter為基於Form表單的身份驗證過濾器;此處可以再新增自己的Filter bean定義;

2、shiroFilter:此處使用ShiroFilterFactoryBean來建立ShiroFilter過濾器;filters屬性用於定義自己的過濾器,即ini配置中的[filters]部分;filterChainDefinitions用於宣告url和filter的關係,即ini配置中的[urls]部分。

Shiro許可權註解

注意:

在spring中需要開啟許可權註解與aop:

<!-- AOP式方法級許可權檢查  -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
      depends-on="lifecycleBeanPostProcessor"/>

<!-- 啟用shrio授權註解攔截方式 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

Shiro提供了相應的註解用於許可權控制,如果使用這些註解就需要使用AOP的功能來進行判斷,如Spring AOP;Shiro提供了Spring AOP整合用於許可權註解的解析和驗證。

為了測試,此處使用了Spring MVC來測試Shiro註解,當然Shiro註解不僅僅可以在web環境使用,在獨立的JavaSE中也是可以用的,此處只是以web為例了。

在spring-mvc.xml配置檔案新增Shiro Spring AOP許可權註解的支援:

  1. <aop:config proxy-target-class="true"></aop:config>  
  2. <bean class="  
  3. org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  
  4.     <property name="securityManager" ref="securityManager"/>  
  5. </bean>   

如上配置用於開啟Shiro Spring AOP許可權註解的支援;<aop:configproxy-target-class="true">表示代理類。

接著就可以在相應的控制器(AnnotationController)中使用如下方式進行註解: 

  1. @RequiresRoles("admin")  
  2. @RequestMapping("/hello2")  
  3. public String hello2() {  
  4.     return "success";  
  5. }  

訪問hello2方法的前提是當前使用者有admin角色。

當驗證失敗,其會丟擲UnauthorizedException異常,此時可以使用Spring的ExceptionHandler(DefaultExceptionHandler)來進行攔截處理:

  1. @ExceptionHandler({UnauthorizedException.class})  
  2. @ResponseStatus(HttpStatus.UNAUTHORIZED)  
  3. public ModelAndView processUnauthenticatedException(NativeWebRequest request, UnauthorizedException e) {  
  4.     ModelAndView mv = new ModelAndView();  
  5.     mv.addObject("exception", e);  
  6.     mv.setViewName("unauthorized");  
  7.     return mv;  
  8. }   

許可權註解     

Java程式碼  

  1. @RequiresAuthentication  

表示當前Subject已經通過login進行了身份驗證;即Subject.isAuthenticated()返回true。 

Java程式碼  

  1. @RequiresUser  

表示當前Subject已經身份驗證或者通過記住我登入的。

Java程式碼  

  1. @RequiresGuest  

表示當前Subject沒有身份驗證或通過記住我登入過,即是遊客身份。  

Java程式碼  

  1. @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)  

表示當前Subject需要角色admin和user。

Java程式碼  

  1. @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)  

表示當前Subject需要許可權user:a或user:b。  

Shiro 完整專案配置

第一步:配置web.xml

<!-- 配置Shiro過濾器,先讓Shiro過濾系統接收到的請求 -->  

<!-- 這裡filter-name必須對應applicationContext.xml中定義的<bean id="shiroFilter"/> -->  

<!-- 使用[/*]匹配所有請求,保證所有的可控請求都經過Shiro的過濾 -->  

<!-- 通常會將此filter-mapping放置到最前面(即其他filter-mapping前面),以保證它是過濾器鏈中第一個起作用的 -->  

<filter>  

    <filter-name>shiroFilter</filter-name>  

    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  

    <init-param>  

    <!-- 該值預設為false,表示生命週期由SpringApplicationContext管理,設定為true則表示由ServletContainer管理 -->  

    <param-name>targetFilterLifecycle</param-name>  

    <param-value>true</param-value>  

    </init-param>  

</filter>  

<filter-mapping>  

        <filter-name>shiroFilter</filter-name>  

        <url-pattern>/*</url-pattern>  

</filter-mapping>

第二步:配置applicationContext.xml

<!-- 繼承自AuthorizingRealm的自定義Realm,即指定Shiro驗證使用者登入的類為自定義的ShiroDbRealm.java -->  

<bean id="myRealm" class="com.jadyer.realm.MyRealm"/>  

<!-- Shiro預設會使用Servlet容器的Session,可通過sessionMode屬性來指定使用Shiro原生Session -->  

<!-- 即<property name="sessionMode" value="native"/>,詳細說明見官方文件 -->  

<!-- 這裡主要是設定自定義的單Realm應用,若有多個Realm,可使用'realms'屬性代替 -->  

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  

    <property name="realm" ref="myRealm"/>  

</bean>  

<!-- Shiro主過濾器本身功能十分強大,其強大之處就在於它支援任何基於URL路徑表示式的、自定義的過濾器的執行 -->  

<!-- Web應用中,Shiro可控制的Web請求必須經過Shiro主過濾器的攔截,Shiro對基於Spring的Web應用提供了完美的支援 -->  

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  

    <!-- Shiro的核心安全介面,這個屬性是必須的 -->  

    <property name="securityManager" ref="securityManager"/>  

    <!-- 要求登入時的連結(可根據專案的URL進行替換),非必須的屬性,預設會自動尋找Web工程根目錄下的"/login.jsp"頁面 -->  

    <property name="loginUrl" value="/"/>  

    <!-- 登入成功後要跳轉的連線(本例中此屬性用不到,因為登入成功後的處理邏輯在LoginController裡硬編碼為main.jsp了) -->  

    <!-- <property name="successUrl" value="/system/main"/> -->  

    <!-- 使用者訪問未對其授權的資源時,所顯示的連線 -->  

    <!-- 若想更明顯的測試此屬性可以修改它的值,如unauthor.jsp,然後用[玄玉]登入後訪問/admin/listUser.jsp就看見瀏覽器會顯示unauthor.jsp -->  

    <property name="unauthorizedUrl" value="/"/>  

    <!-- Shiro連線約束配置,即過濾鏈的定義 -->  

    <!-- 此處可配合我的這篇文章來理解各個過濾連的作用http://blog.csdn.net/jadyer/article/details/12172839 -->  

    <!-- 下面value值的第一個'/'代表的路徑是相對於HttpServletRequest.getContextPath()的值來的 -->  

    <!-- anon:它對應的過濾器裡面是空的,什麼都沒做,這裡.do和.jsp後面的*表示引數,比方說login.jsp?main這種 -->  

    <!-- authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內建的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter -->  

    <property name="filterChainDefinitions">  

        <value>  

             /mydemo/login=anon  

             /mydemo/getVerifyCodeImage=anon  

             /main**=authc  

             /user/info**=authc  

             /admin/listUser**=authc,perms[admin:manage]  

        </value>  

    </property>

</bean>  

<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->  

<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>  

<!-- 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 -->  

<!-- 配置以下兩個bean即可實現此功能 -->  

<!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  

<!-- 由於本例中並未使用Shiro註解,故註釋掉這兩個bean(個人覺得將許可權通過註解的方式硬編碼在程式中,檢視起來不是很方便,沒必要使用) -->  

<!--   

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  

  <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  

    <property name="securityManager" ref="securityManager"/>  

  </bean>  

-->

第三步:自定義的Realm類

public class MyRealm extends AuthorizingRealm {  

    /** 

     * 為當前登入的Subject授予角色和許可權 

     * @see  經測試:本例中該方法的呼叫時機為需授權資源被訪問時 

     * @see  經測試:並且每次訪問需授權資源時都會執行該方法中的邏輯,這表明本例中預設並未啟用AuthorizationCache 

     * @see  個人感覺若使用了Spring3.1開始提供的ConcurrentMapCache支援,則可靈活決定是否啟用AuthorizationCache 

     * @see  比如說這裡從資料庫獲取許可權資訊時,先去訪問Spring3.1提供的快取,而不使用Shior提供的AuthorizationCache 

     */  

    @Override  

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){  

        //獲取當前登入的使用者名稱,等價於(String)principals.fromRealm(this.getName()).iterator().next()  

        String currentUsername = (String)super.getAvailablePrincipal(principals);  

//      List<String> roleList = new ArrayList<String>();  

//      List<String> permissionList = new ArrayList<String>();  

//      //從資料庫中獲取當前登入使用者的詳細資訊  

//      User user = userService.getByUsername(currentUsername);  

//      if(null != user){  

//          //實體類User中包含有使用者角色的實體類資訊  

//          if(null!=user.getRoles() && user.getRoles().size()>0){  

//              //獲取當前登入使用者的角色  

//              for(Role role : user.getRoles()){  

//                  roleList.add(role.getName());  

//                  //實體類Role中包含有角色許可權的實體類資訊  

//                  if(null!=role.getPermissions() && role.getPermissions().size()>0){  

//                      //獲取許可權  

//                      for(Permission pmss : role.getPermissions()){  

//                          if(!StringUtils.isEmpty(pmss.getPermission())){  

//                              permissionList.add(pmss.getPermission());  

//                          }  

//                      }  

//                  }  

//              }  

//          }  

//      }else{  

//          throw new AuthorizationException();  

//      }  

//      //為當前使用者設定角色和許可權  

//      SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();  

//      simpleAuthorInfo.addRoles(roleList);  

//      simpleAuthorInfo.addStringPermissions(permissionList);  

        SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();  

        //實際中可能會像上面註釋的那樣從資料庫取得  

        if(null!=currentUsername && "mike".equals(currentUsername)){  

            //新增一個角色,不是配置意義上的新增,而是證明該使用者擁有admin角色    

            simpleAuthorInfo.addRole("admin");  

            //新增許可權  

            simpleAuthorInfo.addStringPermission("admin:manage");  

            System.out.println("已為使用者[mike]賦予了[admin]角色和[admin:manage]許可權");  

            return simpleAuthorInfo;  

        }

        //若該方法什麼都不做直接返回null的話,就會導致任何使用者訪問/admin/listUser.jsp時都會自動跳轉到unauthorizedUrl指定的地址  

        //詳見applicationContext.xml中的<bean id="shiroFilter">的配置  

        return null;  

    }  

    /** 

     * 驗證當前登入的Subject 

     * @see  經測試:本例中該方法的呼叫時機為LoginController.login()方法中執行Subject.login()時 

     */  

    @Override  

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {  

        //獲取基於使用者名稱和密碼的令牌  

        //實際上這個authcToken是從LoginController裡面currentUser.login(token)傳過來的  

        //兩個token的引用都是一樣的

        UsernamePasswordToken token = (UsernamePasswordToken)authcToken;  

        System.out.println("驗證當前Subject時獲取到token為" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));  

//      User user = userService.getByUsername(token.getUsername());  

//      if(null != user){  

//          AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), user.getNickname());  

//          this.setSession("currentUser", user);  

//          return authcInfo;  

//      }else{  

//          return null;  

//      }  

        //此處無需比對,比對的邏輯Shiro會做,我們只需返回一個和令牌相關的正確的驗證資訊  

        //說白了就是第一個引數填登入使用者名稱,第二個引數填合法的登入密碼(可以是從資料庫中取到的,本例中為了演示就硬編碼了)  

        //這樣一來,在隨後的登入頁面上就只有這裡指定的使用者和密碼才能通過驗證  

        if("mike".equals(token.getUsername())){  

            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo("mike", "mike", this.getName());  

            this.setSession("currentUser", "mike");  

            return authcInfo;  

        }

        //沒有返回登入使用者名稱對應的SimpleAuthenticationInfo物件時,就會在LoginController中丟擲UnknownAccountException異常  

        return null;  

    }  

    /** 

     * 將一些資料放到ShiroSession中,以便於其它地方使用 

     * @see  比如Controller,使用時直接用HttpSession.getAttribute(key)就可以取到 

     */  

    private void setSession(Object key, Object value){  

        Subject currentUser = SecurityUtils.getSubject();  

        if(null != currentUser){  

            Session session = currentUser.getSession();  

            System.out.println("Session預設超時時間為[" + session.getTimeout() + "]毫秒");  

            if(null != session){  

                session.setAttribute(key, value);  

            }  

        }  

    }  

}