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:
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用於收集使用者提交的身份(如使用者名稱)及憑據(如密碼):
- public interface AuthenticationToken extends Serializable {
- Object getPrincipal(); //身份
- Object getCredentials(); //憑據
- }
擴充套件介面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用於聚合這些身份資訊:
- public interface PrincipalCollection extends Iterable, Serializable {
- Object getPrimaryPrincipal(); //得到主要的身份
- <T> T oneByType(Class<T> type); //根據身份型別獲取第一個
- <T> Collection<T> byType(Class<T> type); //根據身份型別獲取一組
- List asList(); //轉換為List
- Set asSet(); //轉換為Set
- Collection fromRealm(String realmName); //根據Realm名字獲取
- Set<String> getRealmNames(); //獲取所有身份驗證通過的Realm名字
- 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用於聚合授權資訊的:
- public interface AuthorizationInfo extends Serializable {
- Collection<String> getRoles(); //獲取角色字串資訊
- Collection<String> getStringPermissions(); //獲取許可權字串資訊
- Collection<Permission> getObjectPermissions(); //獲取Permission物件資訊
- }
當我們使用AuthorizingRealm時,如果身份驗證成功,在進行授權時就通過doGetAuthorizationInfo方法獲取角色/許可權資訊用於授權驗證。
Shiro提供了一個實現SimpleAuthorizationInfo,大多數時候使用這個即可。
對於Account及SimpleAccount,之前的【6.3 AuthenticationInfo】已經介紹過了,用於SimpleAccountRealm子類,實現動態角色/許可權維護的。
Subject
Subject是Shiro的核心物件,基本所有身份驗證、授權都是通過Subject完成。
1、身份資訊獲取
Java程式碼
- Object getPrincipal(); //Primary Principal
- PrincipalCollection getPrincipals(); // PrincipalCollection
2、身份驗證
Java程式碼
- void login(AuthenticationToken token) throws AuthenticationException;
- boolean isAuthenticated();
- boolean isRemembered();
通過login登入,如果登入失敗將丟擲相應的AuthenticationException,如果登入成功呼叫isAuthenticated就會返回true,即已經通過身份驗證;如果isRemembered返回true,表示是通過記住我功能登入的而不是呼叫login方法登入的。isAuthenticated/isRemembered是互斥的,即如果其中一個返回true,另一個返回false。
3、角色授權驗證
Java程式碼
- boolean hasRole(String roleIdentifier);
- boolean[] hasRoles(List<String> roleIdentifiers);
- boolean hasAllRoles(Collection<String> roleIdentifiers);
- void checkRole(String roleIdentifier) throws AuthorizationException;
- void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
- void checkRoles(String... roleIdentifiers) throws AuthorizationException;
hasRole*進行角色驗證,驗證後返回true/false;而checkRole*驗證失敗時丟擲AuthorizationException異常。
4、許可權授權驗證
Java程式碼
- boolean isPermitted(String permission);
- boolean isPermitted(Permission permission);
- boolean[] isPermitted(String... permissions);
- boolean[] isPermitted(List<Permission> permissions);
- boolean isPermittedAll(String... permissions);
- boolean isPermittedAll(Collection<Permission> permissions);
- void checkPermission(String permission) throws AuthorizationException;
- void checkPermission(Permission permission) throws AuthorizationException;
- void checkPermissions(String... permissions) throws AuthorizationException;
10. void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
isPermitted*進行許可權驗證,驗證後返回true/false;而checkPermission*驗證失敗時丟擲AuthorizationException。
5、會話
Java程式碼
- Session getSession(); //相當於getSession(true)
- Session getSession(boolean create);
類似於Web中的會話。如果登入成功就相當於建立了會話,接著可以使用getSession獲取;如果create=false如果沒有會話將返回null,而create=true如果沒有會話會強制建立一個。
6、退出
Java程式碼
- void logout();
7、RunAs
Java程式碼
- void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
- boolean isRunAs();
- PrincipalCollection getPreviousPrincipals();
- PrincipalCollection releaseRunAs();
RunAs即實現“允許A假設為B身份進行訪問”;通過呼叫subject.runAs(b)進行訪問;接著呼叫subject.getPrincipals將獲取到B的身份;此時呼叫isRunAs將返回true;而a的身份需要通過subject. getPreviousPrincipals獲取;如果不需要RunAs了呼叫subject. releaseRunAs即可。
8、多執行緒
Java程式碼
- <V> V execute(Callable<V> callable) throws ExecutionException;
- void execute(Runnable runnable);
- <V> Callable<V> associateWith(Callable<V> callable);
- Runnable associateWith(Runnable runnable);
實現執行緒之間的Subject傳播,因為Subject是執行緒繫結的;因此在多執行緒執行中需要傳播到相應的執行緒才能獲取到相應的Subject。最簡單的辦法就是通過execute(runnable/callable例項)直接呼叫;或者通過associateWith(runnable/callable例項)得到一個包裝後的例項;它們都是通過:1、把當前執行緒的Subject繫結過去;2、線上程執行結束後自動釋放。
Subject自己不會實現相應的身份驗證/授權邏輯,而是通過DelegatingSubject委託給SecurityManager實現;及可以理解為Subject是一個面門。
對於Subject的構建一般沒必要我們去建立;一般通過SecurityUtils.getSubject()獲取:
Java程式碼
- public static Subject getSubject() {
- Subject subject = ThreadContext.getSubject();
- if (subject == null) {
- subject = (new Subject.Builder()).buildSubject();
- ThreadContext.bind(subject);
- }
- return subject;
- }
即首先檢視當前執行緒是否綁定了Subject,如果沒有通過Subject.Builder構建一個然後繫結到現場返回。
如果想自定義建立,可以通過:
Java程式碼
- new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
這種可以建立相應的Subject例項了,然後自己繫結到執行緒即可。在new Builder()時如果沒有傳入SecurityManager,自動呼叫SecurityUtils.getSecurityManager獲取;也可以自己傳入一個例項。
Shiro的jstl標籤
Shiro提供了JSTL標籤用於在JSP/GSP頁面進行許可權控制,如根據登入使用者顯示相應的頁面按鈕。
匯入標籤庫
Java程式碼
- <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
標籤庫定義在shiro-web.jar包下的META-INF/shiro.tld中定義。
guest標籤
Java程式碼
- <shiro:guest>
- 歡迎遊客訪問,<a href="${pageContext.request.contextPath}/login.jsp">登入</a>
- </shiro:guest>
使用者沒有身份驗證時顯示相應資訊,即遊客訪問資訊。
user標籤
Java程式碼
- <shiro:user>
- 歡迎[<shiro:principal/>]登入,<a href="${pageContext.request.contextPath}/logout">退出</a>
- </shiro:user>
使用者已經身份驗證/記住我登入後顯示相應的資訊。
authenticated標籤
Java程式碼
- <shiro:authenticated>
- 使用者[<shiro:principal/>]已身份驗證通過
- </shiro:authenticated>
使用者已經身份驗證通過,即Subject.login登入成功,不是記住我登入的。
notAuthenticated標籤
<shiro:notAuthenticated>
未身份驗證(包括記住我)
</shiro:notAuthenticated>
使用者已經身份驗證通過,即沒有呼叫Subject.login進行登入,包括記住我自動登入的也屬於未進行身份驗證。
principal標籤
<shiro: principal/>
顯示使用者身份資訊,預設呼叫Subject.getPrincipal()獲取,即Primary Principal。
Java程式碼
- <shiro:principal type="java.lang.String"/>
相當於Subject.getPrincipals().oneByType(String.class)。
Java程式碼
- <shiro:principal type="java.lang.String"/>
相當於Subject.getPrincipals().oneByType(String.class)。
Java程式碼
- <shiro:principal property="username"/>
相當於((User)Subject.getPrincipals()).getUsername()。
hasRole標籤
Java程式碼
- <shiro:hasRole name="admin">
- 使用者[<shiro:principal/>]擁有角色admin<br/>
- </shiro:hasRole>
如果當前Subject有角色將顯示body體內容。
hasAnyRoles標籤
Java程式碼
- <shiro:hasAnyRoles name="admin,user">
- 使用者[<shiro:principal/>]擁有角色admin或user<br/>
- </shiro:hasAnyRoles>
如果當前Subject有任意一個角色(或的關係)將顯示body體內容。
lacksRole標籤
Java程式碼
- <shiro:lacksRole name="abc">
- 使用者[<shiro:principal/>]沒有角色abc<br/>
- </shiro:lacksRole>
如果當前Subject沒有角色將顯示body體內容。
hasPermission標籤
Java程式碼
- <shiro:hasPermission name="user:create">
- 使用者[<shiro:principal/>]擁有許可權user:create<br/>
- </shiro:hasPermission>
如果當前Subject有許可權將顯示body體內容。
lacksPermission標籤
Java程式碼
- <shiro:lacksPermission name="org:create">
- 使用者[<shiro:principal/>]沒有許可權org:create<br/>
- </shiro:lacksPermission>
如果當前Subject沒有許可權將顯示body體內容。
另外又提供了幾個許可權控制相關的標籤:
Shiro與web
與spring整合:在Web.xml中
- <filter>
- <filter-name>shiroFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- <init-param>
- <param-name>targetFilterLifecycle</param-name>
- <param-value>true</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- 10. <filter-name>shiroFilter</filter-name>
- 11. <url-pattern>/*</url-pattern>
12. </filter-mapping>
DelegatingFilterProxy作用是自動到spring容器查詢名字為shiroFilter(filter-name)的bean並把所有Filter的操作委託給它。然後將ShiroFilter配置到spring容器即可:
Shiro整合spring
- <!-- 快取管理器 使用Ehcache實現 -->
- <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
- <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
- </bean>
- <!-- 憑證匹配器 -->
- <bean id="credentialsMatcher" class="
- com.github.zhangkaitao.shiro.chapter12.credentials.RetryLimitHashedCredentialsMatcher">
- <constructor-arg ref="cacheManager"/>
- 10. <property name="hashAlgorithmName" value="md5"/>
- 11. <property name="hashIterations" value="2"/>
- 12. <property name="storedCredentialsHexEncoded" value="true"/>
13. </bean>
- 14.
15. <!-- Realm實現 -->
16. <bean id="userRealm" class="com.github.zhangkaitao.shiro.chapter12.realm.UserRealm">
- 17. <property name="userService" ref="userService"/>
- 18. <property name="credentialsMatcher" ref="credentialsMatcher"/>
- 19. <property name="cachingEnabled" value="true"/>
- 20. <property name="authenticationCachingEnabled" value="true"/>
- 21. <property name="authenticationCacheName" value="authenticationCache"/>
- 22. <property name="authorizationCachingEnabled" value="true"/>
- 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">
- 31. <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
- 32. <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
33. </bean>
34. <!-- 會話驗證排程器 -->
35. <bean id="sessionValidationScheduler"
36. class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
- 37. <property name="sessionValidationInterval" value="1800000"/>
- 38. <property name="sessionManager" ref="sessionManager"/>
39. </bean>
40. <!-- 會話管理器 -->
41. <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
- 42. <property name="globalSessionTimeout" value="1800000"/>
- 43. <property name="deleteInvalidSessions" value="true"/>
- 44. <property name="sessionValidationSchedulerEnabled" value="true"/>
- 45. <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
- 46. <property name="sessionDAO" ref="sessionDAO"/>
47. </bean>
48. <!-- 安全管理器 -->
49. <bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">
- 50. <property name="realms">
- 51. <list><ref bean="userRealm"/></list>
- 52. </property>
- 53. <property name="sessionManager" ref="sessionManager"/>
- 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"/>
- 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。
- <!-- 會話Cookie模板 -->
- <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
- <constructor-arg value="sid"/>
- <property name="httpOnly" value="true"/>
- <property name="maxAge" value="180000"/>
- </bean>
- <!-- 會話管理器 -->
- <bean id="sessionManager"
- class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
- 10. <property name="globalSessionTimeout" value="1800000"/>
- 11. <property name="deleteInvalidSessions" value="true"/>
- 12. <property name="sessionValidationSchedulerEnabled" value="true"/>
- 13. <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
- 14. <property name="sessionDAO" ref="sessionDAO"/>
- 15. <property name="sessionIdCookieEnabled" value="true"/>
- 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"/>
- 21. <property name="sessionManager" ref="sessionManager"/>
- 22. <property name="cacheManager" ref="cacheManager"/>
23. </bean>
1、sessionIdCookie是用於生產Session ID Cookie的模板;
2、會話管理器使用用於web環境的DefaultWebSessionManager;
3、安全管理器使用用於web環境的DefaultWebSecurityManager。
- <!-- 基於Form表單的身份驗證過濾器 -->
- <bean id="formAuthenticationFilter"
- class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
- <property name="usernameParam" value="username"/>
- <property name="passwordParam" value="password"/>
- <property name="loginUrl" value="/login.jsp"/>
- </bean>
- <!-- Shiro的Web過濾器 -->
- <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
- 10. <property name="securityManager" ref="securityManager"/>
- 11. <property name="loginUrl" value="/login.jsp"/>
- 12. <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
- 13. <property name="filters">
- 14. <util:map>
- 15. <entry key="authc" value-ref="formAuthenticationFilter"/>
- 16. </util:map>
- 17. </property>
- 18. <property name="filterChainDefinitions">
- 19. <value>
- 20. /index.jsp = anon
- 21. /unauthorized.jsp = anon
- 22. /login.jsp = authc
- 23. /logout = logout
- 24. /** = user
- 25. </value>
- 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許可權註解的支援:
- <aop:config proxy-target-class="true"></aop:config>
- <bean class="
- org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
- <property name="securityManager" ref="securityManager"/>
- </bean>
如上配置用於開啟Shiro Spring AOP許可權註解的支援;<aop:configproxy-target-class="true">表示代理類。
接著就可以在相應的控制器(AnnotationController)中使用如下方式進行註解:
- @RequiresRoles("admin")
- @RequestMapping("/hello2")
- public String hello2() {
- return "success";
- }
訪問hello2方法的前提是當前使用者有admin角色。
當驗證失敗,其會丟擲UnauthorizedException異常,此時可以使用Spring的ExceptionHandler(DefaultExceptionHandler)來進行攔截處理:
- @ExceptionHandler({UnauthorizedException.class})
- @ResponseStatus(HttpStatus.UNAUTHORIZED)
- public ModelAndView processUnauthenticatedException(NativeWebRequest request, UnauthorizedException e) {
- ModelAndView mv = new ModelAndView();
- mv.addObject("exception", e);
- mv.setViewName("unauthorized");
- return mv;
- }
許可權註解
Java程式碼
- @RequiresAuthentication
表示當前Subject已經通過login進行了身份驗證;即Subject.isAuthenticated()返回true。
Java程式碼
- @RequiresUser
表示當前Subject已經身份驗證或者通過記住我登入的。
Java程式碼
- @RequiresGuest
表示當前Subject沒有身份驗證或通過記住我登入過,即是遊客身份。
Java程式碼
- @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
表示當前Subject需要角色admin和user。
Java程式碼
- @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);
}
}
}
}