天天看點

Shiro 架構

Shiro簡介

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

Shiro 架構

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

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

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

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

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

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

Concurrency:shiro支援多線程應用的并發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去;

Testing:提供測試支援;

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

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

記住一點,Shiro不會去維護使用者、維護權限;這些需要我們自己去設計/提供;然後通過相應的接口注入給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的架構,如下圖所示:

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

Shiro 架構

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

Shiro 架構

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 架構

因為我們可以在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

Shiro 架構

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

Shiro 架構

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" target="_blank" rel="external nofollow" >登入</a>  
  3. </shiro:guest>   

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

user标簽 

Java代碼  

  1. <shiro:user>  
  2. 歡迎[<shiro:principal/>]登入,<a href="${pageContext.request.contextPath}/logout" target="_blank" rel="external nofollow" >退出</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>  

        @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

繼續閱讀