Shiro權限管理


1.簡介

Apache Shiro是Java的一個安全框架,對比Spring Security,沒有Spring Security功能強大,但在實際工作時可能並不需要那么復雜,所以使用小而簡單的Shiro就足夠了。

Shiro可以非常容易的開發出足夠好的應用,其不僅可以用在JavaSE環境,也可以用在JavaEE環境。

 

shiro滿足的功能:

 

Shiro可以幫助我們完成:認證、授權、會話管理、加密等,並且提供與web集成、緩存、rememberMed等功能。

 

 

2.Shiro的工作模型

 

 

 

應用程序直接與Shiro中的Subject進行交互,Shiro的對外API就是Subject。

Subject:代表當前用戶,所有Subject都將綁定到SecurityManager,與Subject的所有交互都會委托給SecurityManager。

SecurityManager:所有與安全相關的操作都由SecurityManager進行統一管理,負責與shiro的相關組件進行交互,類似前端控制器。

Realm:為SecurityManager提供安全數據,如用戶的身份、角色信息等,類似安全數據源。

SessionManager:會話管理,會話可以是普通JavaSE環境的,也可以是Web環境的。

SessionDAO:用於會話的CRUD。

CacheManager:緩存管理器,可以將從Realm中獲取的數據放入緩存中管理。

Cryptography:Shiro提供了常見的加密工具類用於密碼的加密。

 

 

3.Shiro的組件

 

3.1 Subject

 

Subject接口聲明了獲取當前用戶信息的方法,其只有DelegatingSubject實現類。

*SecurityUtils.getSubject()方法返回代表當前用戶的Subject對象。

 

Subject API

//獲取Session實例
Session getSession() 
//判斷subject是否已經認證 boolean isAuthenticated() //判斷當前用戶是否通過RememberMe形式登錄 boolean isRememberMe() //執行登錄,實際是調用SecurityManager的login方法 void login(AuthenticationToken token) //判斷subject是否擁有某個角色 boolean hasRole(String role) //判斷subject是否擁有某個功能 boolean isPermitted(String permission) //執行注銷操作 void logout()

 

 

3.2 SecurityManager 

 

SecurityManager接口聲明了用於認證、注銷、創建subject的方法。

*使用SecurityUtils.getSecurityManager()方法可以獲取SecurityManager實例。

 

//認證操作
Subject login(Subject subject, AuthenticationToken authenticationToken) //注銷操作
void logout(Subject subject); //Subject的創建
Subject createSubject(SubjectContext context);

 

AuthenticationToken

AuthenticationToken接口是用於封裝Principal與Credential,有UsernamePasswordToken實現類。

*Principal:代表用戶的身份,可以是用戶名、郵箱、手機號碼等。

*Credential:憑證,一般是密碼。

*在執行登錄操作時需要傳遞AuthenticationToken的實現類,其包含了用戶的身份以及憑證信息。

 

 

3.3 Realm

 

Realm接口為SecurityManager提供安全數據,Shiro中提供了很多Realm接口的抽象實現類。

 

 

AuthenticatingRealm抽象類聲明了用於認證的方法:

 

//認證操作
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)

 

AuthorizingRealm抽象類聲明了用於授權的方法:

 

//授權操作,PrincipalCollection封裝了用戶的身份信息,使用其getPrimaryPrincipal()方法獲取當前登錄用戶的身份,返回Object類型.
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)

 

*若應用中只需要認證的功能,則自定義Realm繼承AuthenticatingRealm,實現其doGetAuthenticationInfo方法,若應用中需要用到認證與授權功能,則自定義Realm繼承AuthorizingRealm,實現其doGetAuthenticationInfo、doGetAuthorizationInfo方法。

*doGetAuthenticationInfo(AuthenticationToken token) 與 doGetAuthorizationInfo(PrincipalCollection principals)方法都是由SecurityManager自動調用。

 

 

4.shiro的使用

 

4.1 導入相關依賴

 

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.3.17.RELEASE</version>
</dependency>

*由於shiro在認證和授權時會使用到緩存,其使用Ehcache緩存框架,因此需要添加Ehcache的依賴以及配置文件。

*一般使用Shiro時都會與Spring進行集成,因此需要導入Spring的依賴。

 

 

4.2 配置Spring提供的過濾器代理

 

在web.xml中配置DelegatingFilterProxy並指定targetBeanName。

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
     <init-param>
          <param-name>targetBeanName</param-name>
          <param-value>shiroFilter</param-value>
     </init-param>
</filter>

<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

*DelegatingFilterProxy是一個標准的Filter代理,通過targetBeanName指定其要代理的Filter的bean的id (默認情況下將代理bean id為filter-name的Filter)

 

 

4.3 創建自定義Realm

 

1.認證

 

AuthenticatingRealm抽象類聲明了用於認證的方法:

//認證操作
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)

 

AuthenticationInfo接口用於封裝用戶的身份與憑證信息,Shiro為AuthenticationInfo提供了以下實現類:

 

 

一般使用SimpleAuthenticationInfo實現類,只需將AuthenticationToken包含的用戶名以及通過數據庫查詢得到的密碼進行封裝返回即可。

*SecurityManager會調用該Realm的doGetAuthenticationInfo方法獲取用戶認證信息,根據AuthenticationToken的Credential與從Realm中獲取的AuthenticationInfo中的Credential進行校驗 (密碼的比對是由shiro自動完成)

 

/**
 * SecurityManager會到該Reaml獲取安全數據.
 * @author ZHUANGHAOTANG
 *
 */
public class AuthenticationRealm extends AuthenticatingRealm {

    @Autowired
    private UserService userService;
    
    /**
     * 獲取Reaml中數據的方法,返回AuthencationInfo存放安全數據,供SecurityManager使用.
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //獲取用戶填寫的用戶名
        String username = ((UsernamePasswordToken)token).getUsername();
        //根據用戶名查詢數據庫用戶表的記錄.
        User user = userService.findByOne(username);
        //若不存在此用戶,則手動拋出未知身份異常.
         if(user == null){
            throw new UnknownAccountException();
        }else{
            //返回AuthencationInfo接口類型,使用SimpleAuthenticationInfo實現類,參數是AuthenticationToken傳遞的用戶名、通過用戶名查詢的密碼、Realm的名稱.
            return new SimpleAuthenticationInfo(username, user.getPassword(),this.getName());
        }
    }

}

 

<!-- 進行認證的Realm -->
<bean id="authenticationRealm" class="com.realm.AuthenticationRealm"/>

 

*可以在subject.login()方法進行異常的捕獲(控制層),當認證失敗時根據異常類型的不同返回相應的提示給前端。

//未知的賬號異常(需手動拋出)
UnknownAccountException //非法的憑證異常(校驗失敗自動拋出)
IncorrectCredentialsException

 

 

加密

由於數據庫中保存的密碼通常是密文,而用戶傳遞過來時是明文,因此就需要讓SecurityManager在進行認證時自動對AuthenticationToken中的Credential進行加密后再與AuthenticationInfo中的Credential進行比對。

*SecurityManager是通過AuthenticatingRealm的CredentialsMatcher(憑證匹配器)進行密碼的比對。

Shiro為CredentialsMatcher憑證匹配器提供了以下實現類:

 

 

*默認情況下AuthenticatingRealm使用SimpleCredentialsMatcher實現類,SecurityManager不會對AuthenticationToken中的Credential進行加密 ,若使用HashedCredentialsMatcher實現類,則會對AuthenticationToken中的Credential進行加密。

 

HashedCredentialsMatcher提供了hashAlgorithm、hashIterations屬性分別設置加密的算法以及次數。

<!-- 進行認證(加密)的Realm -->
<bean id="authenticationRealm" class="com.realm.AuthenticationRealm">
    <property name="CredentialsMatcher" >
       <!-- HashedCredentialsMatcher -->
       <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
        <!-- 支持md和sha系列 -->
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="1024"/>
       </bean>
     </property>
 </bean>

 

HashedCredentialsMatcher內部是使用SimpleHash工具類進行加密的,其提供的構造方法:

public SimpleHash(String algorithmName, Object source) public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) public SimpleHash(String algorithmName, Object source, Object salt) public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)

algorithmName:加密算法(MD系列或SHA系列)
source:目標
salt:鹽值
hashIterations:加密次數

使用SimpleHash實例的toHex()方法用於獲取加密后的值,其toString()方法內部調用toHex()方法。

*SimpleHash可用於在注冊時,對用戶提交的密碼進行加密,認證時把AuthenticationToken中的密碼使用與注冊時相同的加密規則加密后再與數據庫中的密碼進行比對。

 

 

鹽值加密

若兩個用戶的密碼一樣,則經過MD或SHA系列算法加密后的密文仍然一樣(哈希函數),數據庫中就保存着兩個相同的密文。

鹽值加密:在原有加密規則的基礎上添加自定義的鹽(salt,一般使用用戶的唯一標識代替),則可以保證兩個用戶的密碼相同,但經過散列后的密文不同,因為它們的鹽值不一樣。

 

在Realm中的doGetAuthenticationInfo方法中使用SimpleAuthenticationInfo帶鹽值的構造方法進行返回,SecurityManager就會使用指定的CredentialsMatcher對AuthenticationToken中的Credential進行鹽值加密,然后再與AuthenticationInfo中的Credential進行比對。

/**
 * 鹽值加密
 * @author ZHUANGHAOTANG
 */
public class EncryptRealm extends AuthenticatingRealm {

    @Autowired
    private UserService userService;
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        
        String username = ((UsernamePasswordToken)token).getUsername();
        User user = userService.findByOne(username);
        if(user == null){
            throw new UnknownAccountException();
        }else{
            //構造鹽值
            ByteSource salt = ByteSource.Util.bytes(username);    
            return new SimpleAuthenticationInfo(username, user.getPassword(),salt,this.getName());
        }
    }
}

*使用ByteSource.Util.bytes(String str)方法構造鹽值,返回ByteSource類型。

 

<!-- 進行認證(加密)的Realm -->
<bean id="authenticationRealm" class="com.realm.AuthenticationRealm">
    <property name="CredentialsMatcher" >
       <!-- HashedCredentialsMatcher -->
       <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
        <!-- 支持md和sha系列 -->
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="1024"/>
       </bean>
     </property>
 </bean>

 

 

RememberMe

RememberMe就是cookie,如果是b/s架構,則cookie信息保存到客戶端瀏覽器特定的磁盤目錄中。

*可以使用AuthenticationToken的setRememberMe(boolean is)方法設置該用戶是否使用rememberMe,當關閉瀏覽器(sessionId cookie失效)再訪問服務器資源時屬於rememberMe,使用Subject的isAuthenticated()方法會返回false,isRememberMe()返回true。

*可以通過SecurityManager的rememberMeManager屬性的cookie屬性的maxAge屬性修改rememberMe的有效時間( 即cookie的有效時間,單位:秒)

<!-- 配置securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="authenticationRealm"/>
    <!-- 設置rememberMe的有效期 -->
    <property name="rememberMeManager.cookie.maxAge" value="604800"/>
</bean>

*rememberMe只能通過anno、user攔截器

*用戶的認證與授權僅在當前會話有效,一旦無法識別當前會話或會話失效時,則需要重新進行認證與授權。

 

 

2.授權

 

AuthorizingRealm抽象類聲明了用於授權的方法:

//授權操作,PrincipalCollection封裝了用戶的身份信息,使用其getPrimaryPrincipal()方法獲取當前登錄用戶的身份,返回Object類型.
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)

 

AuthorizationInfo接口用於封裝用戶的角色和行為信息,Shiro為AuthorizationInfo提供了以下實現類:

 

 一般使用SimpleAuthorizationInfo實現類,其Set<String> roles屬性用於存放用戶擁有的角色,其Set<String> stringPermissions用於存放用戶擁有的行為,只需根據用戶的身份從數據庫查詢用戶擁有的所有角色和行為進行封裝返回即可。

    /** * The internal roles collection. */
    protected Set<String> roles; /** * Collection of all string-based permissions associated with the account. */
    protected Set<String> stringPermissions; /** * Collection of all object-based permissions associaed with the account. */
    protected Set<Permission> objectPermissions;

 

*可以使用構造方法的形式賦值或者使用set方法進行賦值。

 

/**
 * 認證和授權
 * @author ZHUANGHAOTANG
 *
 */
public class AuthorizationRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    
    @Autowired
    private RoleService roleService;
    
    @Autowired
    private MenuService menuService;

    //授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //返回實體
        SimpleAuthorizationInfo result = new SimpleAuthorizationInfo();
        //獲取當前認證用戶的身份        
        String username = ((String)principal.getPrimaryPrincipal());
        //獲取該用戶擁有的角色
        Set<String> roles = roleService.findByUsername(username);
        //設置角色
        result.setRoles(roles);
        for(String role : roles){
            List<String> permissions = munuService.findByRoleName(role);
            if(CollectionUtils.isNotEmpty(permissions)){
                for(String permission : permissions){
                  //設置行為
                  result.addStringPermission(permission);
                }
            }
        }
        return result;
    }

    //認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //獲取用戶填寫的用戶名
        String username = ((UsernamePasswordToken)token).getUsername();
        //根據用戶名查詢數據庫用戶表的記錄.
        User user = userService.findByOne(username);
        //若不存在此用戶,則手動拋出未知身份異常.
        if(user == null){
            throw new UnknownAccountException();
        }else{
            //返回AuthencationInfo接口類型,使用SimpleAuthenticationInfo實現類,參數是Subject傳遞的用戶名、通過用戶名查詢的密碼、Realm的名稱.
            return new SimpleAuthenticationInfo(username, user.getPassword(),this.getName());
        }
    }

}

 

<!-- 認證(加密)和授權的Realm -->
<bean id="authorizationRealm" class="com.realm.AuthorizationRealm">
      <property name="CredentialsMatcher" >
          <!-- HashedCredentialsMatcher -->
          <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="md5"/>
              <property name="hashIterations" value="1024"/>
          </bean>
      </property>
</bean>

 

*Shiro內部會對PermissionString按照“:”進行切分,最終形成Permission實例,當用戶擁有的Permission中的每個字符都與目標Permission成功匹配后就return true了,即當用戶擁有A:B:C權限時,相當於擁有了A:B:C:xxx的權限。

 

 

3.緩存

 

 

CachingRealm實現類中提供了私有的CacheManager屬性且實現了CacheManagerAware接口,該接口聲明了void setCacheManager(CacheManager cacheManager)方法。

SecurityManager會檢測Realm是否實現了CacheManagerAware接口,若實現了則對其注入CacheManager。

*AuthorizingRealm繼承CachingRealm,因此Shiro會把第一次認證和授權后的AuthenticationInfo、AuthorizationInfo實體放人到緩存,當SecurityManager再次使用時直接從緩存中獲取。

*當用戶退出登錄時,會清除緩存中該用戶對應的AuthenticationInfo和AuthorizationInfo實體。 

 

在Spring中配置緩存管理器並把緩存管理器注入到SecurityManager的cacheManger屬性中。

<!-- 配置securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- 注入realm -->
    <property name="realm" ref="authorizationRealm"/>
    <!-- 注入緩存管理器 -->
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 配置緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <!-- 指定緩存配置文件的位置 -->
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>    

 

 

*默認情況下Realm會使用ehcache.xml文件中默認的緩存配置,可以通過Realm的authenticationCacheName、authorizationCacheName屬性指定使用的緩存配置。

<bean id="authorizationRealm" class="com.realm.AuthorizationRealm">
  <property name="authenticationCacheName" value="authenticationCache" />
  <property name="authorizationCacheName" value="authorizationCache" />
</bean>

 

 

4.4 注入Realm 

 

單Realm

單realm情況下直接把realm實例注入到SecurityManager中的realm屬性中。

<!-- 配置securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- 注入realm -->
    <property name="realm" ref="authorizationRealm"/>
</bean>   

<!-- 注冊Realm --> 
<bean id="authorizationRealm" class="com.zhuanght.realm.AuthorizationRealm"/> 

 

多Realm

用戶可以通過用戶名/密碼、郵箱/密碼、手機號/密碼進行登錄( 即可以有多種身份進行認證 ),或者用戶的數據保存到不同數據庫中,此時可以使用多Realm進行驗證。

單Realm情況下:SecurityManager會使用SingleRealmAuthencator認證器對用戶進行認證。

多Realm情況下:SecurityManager會使用ModularRealmAuthenticator認證器對用戶進行認證。

將ModularRealmAuthenticator注入到SecurityManager的authenticator屬性中,然后把所有的Realm注入到SecurityManager的Collection<Realm> realms屬性中。

*SecurityManager內部最終將自身Collection<Realm> 中的reaml注入到ModularRealmAuthenticator的Collection<Realm> realms屬性中。

*使用多Realm的情況下,SecurityManager就會依次去各個realm中獲取數據(認證與授權),此時也會涉及認證的策略。

 

多Realm情況下的認證策略:

//只要有一個Realm認證成功即可,只返回第一個Realm認證成功的信息,其他的忽略.
FirstSuccessfulStrategy //至少有一個Realm認證成功即可,返回所有Realm認證成功的認證信息. 
AtLeastOneSuccessfulStrategy //所有Realm認證成功才算成功,返回所有Realm認證成功的信息.
AllSuccessfulStrategy

 

*ModularRealmAuthenticator默認是使用AtLeastOneSuccessfulStrategy認證策略,可以在ModularRealmAuthenticator的authenticationStrategy屬性進行修改。 

 

 <!-- 配置securityManager -->
 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
     <property name="authenticator" ref="modularRealmAuthenticator"/>
     <property name="realms">
      <list>
        <ref bean="usernameRealm"/>
        <ref bean="emailRealm"/>
        <ref bean="phoneNumberRealm"/>
      </list>
     </property>
 </bean>

 <bean id="modularRealmAuthenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> 
    <!-- 修改認證策略 -->
    <property name="authenticationStrategy">
      <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy"/>
    </property>
 </bean>

 <!-- 根據用戶名認證和授權的Realm -->
 <bean id="usernameRealm" class="com.realm.UsernameRealm">
    <property name="CredentialsMatcher" >
         <!-- HashedCredentialsMatcher -->
         <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
           <property name="hashAlgorithmName" value="md5"/>
           <property name="hashIterations" value="1024"/>
         </bean>
    </property>
 </bean>
<!-- 根據郵箱認證和授權的Realm --> <bean id="emailRealm" class="com.realm.EmailRealm"> <property name="CredentialsMatcher" > <!-- HashedCredentialsMatcher --> <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="1024"/> </bean> </property> </bean> <!-- 根據手機號碼認證和授權的Realm --> <bean id="phoneNumberRealm" class="com.realm.PhoneNumberRealm"> <property name="CredentialsMatcher" > <!-- HashedCredentialsMatcher --> <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="1024"/> </bean> </property> </bean>

*若SecurityManager中配置了多個Realm,那么只要其中有一個realm授權成功就算成功。

 

 

4.5 對資源設置權限

 

方法一:在ShiroFilterFactoryBean的filterChainDefinitions屬性中使用Shiro提供的攔截器對資源設置權限( URL=攔截器 ),指定某些資源需要擁有特定的權限的用戶才能訪問。

 

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- 注入securityManager -->
    <property name="securityManager" ref="securityManager"/>
    <!-- 設置登錄URL,當用戶未認證,但訪問了需要認證后才能訪問的頁面,就會自動重定向到登錄URL -->
    <property name="loginUrl" value="/index.html"/>
    <!-- 設置沒有權限的URL,當認證后的用戶訪問一個頁面卻沒有權限時,就會自動重定向到沒有權限的URL -->
    <property name="unauthorizedUrl" value="/unauthorized.html"/>
    <property name="filterChainDefinitions">
        <value> /login = anon /logout = logout /** = authc </value>
    </property>
</bean>
 

 

 

從數據庫中初始化資源權限信息

ShiroFilterFactoryBean中的filterChainDefinitions屬性最終會把權限的配置以key-value的形式保存到LinkedHashMap實例中,其中key為url,value為攔截器,最后把該實例注入到ShiroFilterFactoryBean中的filterChainDefinitionMap屬性中。

*因此可以從數據庫中查詢資源的權限分配信息並以key-value的形式保存進一個LinkedHashMap,最后把LinkedHashMap注入到ShiroFilterFactoryBean的filterChainDefinitionMap屬性中。

 

*IOC容器中有兩種bean,一種是普通的bean,另一種是工廠bean。

<!-- 普通bean,IOC容器會自動調用類的構造方法創建一個實例 -->
<bean id="" class="" />

<!-- 工廠bean,IOC容器會調用指定工廠類的指定方法,方法會返回一個實例 -->
<bean id="" factory-bean="" factory-method=""/>

 

/** * @Description: 工廠類,返回LinkedHashMap * @author ZHUANGHAOTANG */ @Component public class FilterChainDefinitionMapBuilder { @Autowired private FunctionService functionService; public LinkedHashMap<String,String> builderFilterChainDefinitionMap(){ return functionService.findAuthResource(); } }

 

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- 注入securityManager -->
    <property name="securityManager" ref="securityManager"/>
    <!-- 設置登錄URL,當用戶未認證,但訪問了需要認證后才能訪問的頁面,就會自動重定向到登錄URL -->
    <property name="loginUrl" value="/index.html"/>
    <!-- 設置沒有權限的URL,當認證后的用戶訪問一個頁面卻沒有權限時,就會自動重定向到沒有權限的URL -->
    <property name="unauthorizedUrl" value="/unauthorized.html"/>
    <property name="filterChainDefinitionMap" ref="linkedHashMap"/>
</bean>
    
<bean id="linkedHashMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="builderFilterChainDefinitionMap"/>

 

 

方法二:使用Shiro提供的權限注解對資源設置權限,標注在Controller或Service中。

 

 

*若用戶未認證但訪問需要認證和授權的資源時,則會跳轉到shiroFilter中loginUrl屬性指定的URL。

*若用戶已認證但訪問其沒有權限的資源時,則會跳轉到shiroFilter中unauthorizedUrl屬性指定的URL。

 

 

Shiro認證與授權流程:

 

1.請求首先到達ShiroFilter。

2.若用戶訪問需認證的URL(非user攔截器),由於未進行認證,Subject的isAuthenticated()方法返回false,則Shiro將請求重定向到ShiroFilter配置好的loginUrl。

3.輸入用戶名/密碼進行登錄,最終執行Subject.login()方法。

4.SecurityManager首先判斷緩存中是否存在用戶對應的AuthenticationInfo實體,若不存在則調用Realm的doGetAuthenticationInfo方法進行獲取並將其放入到緩存中,然后執行認證操作(密碼比對),最終將authenticated屬性設置為true表示用戶已進行登錄並將用戶的身份信息放入Subject的PrincipalCollection實體。

5.若用戶訪問的資源需要權限,此時Shiro就會調用Subject的isPermitted(String str)方法來檢驗用戶的權限,首先判斷Subject中的PincipalCollection實體是否包用戶的身份信息 (已登錄才有),若包含則判斷緩存中是否存在用戶對應的AuthorizationInfo實體,若存在則從緩存中獲取,否則將調用Realm的doGetAuthorizationInfo方法進行獲取,最終將AuthorizationInfo實體放入緩存。

6.若用戶具有特定的權限則允許訪問資源,否則將跳轉到ShiroFilter配置好的unauthorizedUrl。

*Shiro是通過Subject的isAuthenticated()方法判斷當前用戶是否已經登錄的,當執行登錄操作后會將Subject的authenticated屬性設值為true並將用戶的身份信息放入Subject的PrincipalCollection實體中。

*若用戶訪問的URL是user攔截器的,則Subject根據isAuthenticated()方法和isRememberMe()方法判斷用戶是否需要進行登錄,若任意一個方法返回true則表示用戶不需進行登錄。 

*當關閉瀏覽器重新訪問時將產生新的Subject對象,isAuthenticated()方法返回false (Subject的生命周期與Session一致)

*Shiro是通過Subject的isPermitted方法判斷當前用戶是否具有特定的權限的,其底層先判斷Subject中的PincipalCollection實體是否包用戶的身份信息 (已登錄才有),若包含則通過緩存或Realm的doGetAuthorizationInfo方法獲取用戶的權限信息。

*Shiro的認證與授權是獨立執行的,當執行Subject的login方法時才會調用Realm的doGetAuthenticationInfo方法,當訪問需要權限的資源時,才會調用Realm的doGetAuthorizationInfo方法 (緩存有則從緩存獲取)

 

 

4.6 Session和SessionManager

 

Shiro中的Session與Java Web中的HttpSession區別:

 

相同點:

1.都表示客戶端與服務器的一次會話。

2.HttpSession與Web環境下Shiro Session的屬性是共通的,因為Session的setAttribute、getAttribute、removeArrtibute方法其內部都是調用HttpSession的。

 

不同點:

1.Java Web中的HttpSession依賴於Servlet容器,Shiro中的Session不依賴於Web容器,可以在Web和JavaSE環境下使用。

2.HttpSession保存屬性時,key只能是String類型,Shiro中的Session保存屬性時key可以是任何類型。

3.HttpSession在首次執行request.getSession(true)時自動創建,Web環境下在用戶首次進入shiroFilter時自動創建Shiro Session,每次進入shiroFilter都會自動調用session.touch()方法去更新最后訪問時間。

 

*當用戶訪問logout攔截器時,內部執行當前subject的logout方法,然后自動調用session的stop()方法來銷毀會話,若HttpSession調用了invalidate()方法銷毀,則Shiro Session也會自動調用stop()方法來銷毀。

*在Controller層推薦使用HttpSession,在service層中使用Shiro的Session。

 

Session監聽器

Shiro中提供了SessionListener監聽器,用於監聽Session的創建、過期以及銷毀事件。

SessionListener中聲明了3個事件方法:

//Session啟動時自動觸發
onStart(Session session ) //Session銷毀時自動觸發
onStop(Session session) //Session超時時自動觸發
onExpiration(Session session)

 

SessionManager

 SessionManager用於管理Shiro的Session,可以設置Session的有效時間、監聽器等,最后將SessionManager注入到SecurityManager中的sessionManager屬性即可。

<!-- 配置securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
   <property name="realm" ref="authenticationRealm"/>
   <property name="sessionManager" ref="sessionManager"/>
</bean>
    
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
   <!-- 設置全局的session超時時間 -->
   <property name="globalSessionTimeout" value="1800"/>
   <!-- 會話過期時刪除過期的會話,默認為true -->
   <property name="deleteInvalidSessions" value="true"/>
   <!-- 開啟會話的驗證 -->
   <property name="sessionValidationSchedulerEnabled" value="true" />
   <!-- session監聽器 -->
   <property name="sessionListeners" >
      <list>
       <ref bean="shiroListener"/>
      </list>
   </property>
</bean>


<!-- 注冊監聽器 -->    
<bean id="shiroListener" class="com.listener.ShiroListener"/>

 

 

4.7 在Spring-framework.xml配置Shiro相關組件(完整配置)

 

 <!-- 配置securityManager -->
 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
     <!-- 修改認證器 -->
     <property name="authenticator" ref="modularRealmAuthenticator"/>
     <!-- 注入多Realm -->
     <property name="realms">
        <list>
          <ref bean="usernameRealm"/>
          <ref bean="emailRealm"/>
          <ref bean="phoneNumberRealm"/>
        </list>
     </property>
     <!-- 注入緩存管理器 -->
     <property name="cacheManager" ref="cacheManager"/>
     <!-- 設置RememberMe Cookie的有效期 -->
     <property name="rememberMeManager.cookie.maxAge" value="604800"/>
     <!-- 注入SessionManager -->
     <property name="sessionManager" ref="sessionManager"/>
 </bean>
 
 <!-- 多Realm下的認證器 -->
 <bean id="modularRealmAuthenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> 
    <!-- 修改認證策略 -->
    <property name="authenticationStrategy">
      <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy"/>
    </property>
 </bean>

 <!-- 根據用戶名認證和授權的Realm -->
 <bean id="usernameRealm" class="com.realm.UsernameRealm">
    <property name="CredentialsMatcher" >
         <!-- HashedCredentialsMatcher -->
         <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
           <property name="hashAlgorithmName" value="md5"/>
           <property name="hashIterations" value="1024"/>
         </bean>
    </property>
 </bean>

 <!-- 根據郵箱認證和授權的Realm -->
 <bean id="emailRealm" class="com.realm.EmailRealm">
    <property name="CredentialsMatcher" >
         <!-- HashedCredentialsMatcher -->
         <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
           <property name="hashAlgorithmName" value="md5"/>
           <property name="hashIterations" value="1024"/>
         </bean>
    </property>
 </bean>

 <!-- 根據手機號碼認證和授權的Realm -->
 <bean id="phoneNumberRealm" class="com.realm.PhoneNumberRealm">
      <property name="CredentialsMatcher" >
           <!-- HashedCredentialsMatcher -->
           <bean class="org.apache.shiro.authc.Credential.HashedCredentialsMatcher">
             <property name="hashAlgorithmName" value="md5"/>
             <property name="hashIterations" value="1024"/>
           </bean>
      </property>
 </bean>


<!-- 配置緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <!-- 指定緩存配置文件的位置 -->
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>      

<!-- SessionManager -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
    <!-- 設置全局的session超時時間 -->
    <property name="globalSessionTimeout" value="1800"/>
    <!-- 會話過期時刪除過期的會話,默認為true -->
    <property name="deleteInvalidSessions" value="true"/>
    <!-- 開啟會話的驗證 -->
    <property name="sessionValidationSchedulerEnabled" value="true" />
    <!-- session監聽器 -->
    <property name="sessionListeners" >
       <list>
         <ref bean="shiroListener"/>
       </list>
    </property>
</bean>
    
<!-- SessionListener -->    
<bean id="shiroListener" class="com.listener.ShiroListener"/>

<!-- 配置lifecycleBeanPostProcessor,shiro bean的生命周期管理器,可以自動調用Spring IOC容器中shiro bean的生命周期方法(初始化/銷毀)-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 為了支持Shiro的注解需要定義DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor兩個bean -->

<!-- 配置DefaultAdvisorAutoProxyCreator,必須配置了lifecycleBeanPostProcessor才能使用 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  

<!-- 配置AuthorizationAttributeSourceAdvisor -->     
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean> 

<!-- 配置shiroFilterFactoryBean,bean的id默認情況下必須與web.xml文件中DelegatingFilterProxy過濾器的filter-name相同,可以通過filter的targetBeanName初始化參數進行修改 -->  
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- 注入securityManager -->
    <property name="securityManager" ref="securityManager"/>
    <!-- 設置登錄URL,當用戶未認證,但訪問了需要認證后才能訪問的頁面,就會自動重定向到登錄URL -->
    <property name="loginUrl" value="/index.html"/>
    <!-- 設置沒有權限的URL,當用戶認證后,訪問一個頁面卻沒有權限時,就會自動重定向到沒有權限的URL,若用戶未認證訪問一個需要權限的URL時,會跳轉到登錄URL -->
    <property name="unauthorizedUrl" value="/unauthorized.html"/>
    <!-- 配置哪些請求需要受保護,以及訪問這些頁面需要的權限 -->
    <property name="filterChainDefinitions">
       <value> /login = anon /registry = anno /logout = logout /** = authc </value>
   </property>
</bean>

 

 

5.集群間實現Session共享

 

一般在生產環境中WebApp應用都是通過Nginx進行負載均衡的,如果每個WebApp應用都是使用Shiro進行權限認證,那么就需要進行集群間的Session共享。

通常的做法是將Session放入到一個共享區域,可以是數據庫,也可以是分布式緩存等。

 

5.1 Subject與Session的關系

用戶的請求首先到達ShiroFilter,然后調用WebSubject的buildWebSubject方法創建Subject,在創建Subject時會獲取Session,判斷Session中是否存在用戶的認證狀態,若不存在則跳過,否則將保存在Session中的認證狀態設置到Subject的authenticated屬性中。

*由此可以知道當用戶的請求進入到ShiroFilter后,首先創建Subject,在創建Subject時會嘗試從Session中恢復認證狀態。

 

Shiro是根據Subject中的authenticated屬性判斷用戶是否已經登錄的,且根據Subject的isPermission方法進行權限的判斷,當用戶訪問了需要認證或授權的資源時,需要執行Shiro的認證與授權。

Shiro在調用isPermission方法校驗權限時,首先會判斷Session中是否存在用戶的PrincipalCollection實體,若存在則直接從Session中獲取,否則調用SecurityManager的isPermitted()方法,將根據緩存或Realm進行獲取。

 

 

當用戶認證成功后,會將Subject中的信息保存到Session中,包括認證狀態以及身份信息。

 

 

當用戶授權成功,會將用戶對應的權限信息PrincipalCollection實體放入到Session中。

 

 

*由此可以知道,當用戶進行認證和授權成功后,會將認證和授權信息放入到Session中,當Session中已經存在用戶的認證和授權信息后,當用戶訪問其他WebApp創建Subject時會根據Session中的認證狀態進行賦值,在進行授權時會從Session中讀取信息,因此只需要解決Session間在集群的共享,就能任意在一個系統進行認證和授權后,在另外一個系統訪問時,直接通過Session中的信息進行授權。

 

Shiro在獲取Session時,是通過調用DelegatingSubject的getSession(boolean create)方法,當入參為true時,最終將會調用DefaultWebSessionManager的SessionDao的create()方法創建Session,因此可以自定義類,實現AbstractSessionDao,然后實現其創建、獲取Session等方法,在方法中將Session實例放入到Redis中,最后將該SessionDao注入到DefaultWebSessionManager中即可。

 

5.2 Session共享流程圖

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM