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用于收集使用者送出的身份(如使用者名)及憑據(如密碼):
- 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" target="_blank" rel="external nofollow" >登入</a>
- </shiro:guest>
使用者沒有身份驗證時顯示相應資訊,即遊客通路資訊。
user标簽
Java代碼
- <shiro:user>
- 歡迎[<shiro:principal/>]登入,<a href="${pageContext.request.contextPath}/logout" target="_blank" rel="external nofollow" >退出</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>
@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;
}
@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;
}
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);
}
}
}
}
轉載:https://www.cnblogs.com/maofa/p/6407102.html