1. 程式人生 > >Shiro學習小結

Shiro學習小結

What is Shiro?

  • Apache旗下的一個開源Java許可權框架,它將軟體系統的安全認證相關的功能抽取出來,實現使用者身份認證,許可權授權、加密、會話管理等功能,組成了一個通用的安全認證框架。

Why is Shiro?

  • 使用Shiro可以快速完成認證、授權等功能的開發;
  • Shiro不依賴於任何容器,可以執行在web應用、非web應用、叢集分散式等應用中。

How to use Shiro?

  • Shiro主要包括認證、授權、會話管理、加密、快取等。整體架構圖如下:Image
  • 重要名詞解釋:
    • Subject:應用程式碼直接互動的物件,代表當前“使用者”。
    • SecurityManager:核心安全管理器,管理著所有Subject,負責與其它元件進行互動。
      • Authenticator:認證器介面,可以自定義實現,是 Shiro API 中身份驗 證核心的入口點:驗證成功將返回AuthenticationInfo 驗證資訊;此資訊中包含了身份及憑證,若失敗則丟擲相應的 AuthenticationException 異常,同時可以配置認證策略即什麼情況下認證通過。
      • Authorizer:授權器,控制著使用者能夠訪問哪些資源。
      • Realm:Shiro從Realm獲取安全資料(使用者、角色、許可權等),SecurityManager要驗證使用者身份必須從Realm獲取相應的使用者進行比較身份。一般繼承 AuthorizingRealm(授權)即可,其繼承了 AuthenticatingRealm(即身份驗證),而且也間接繼承了 CachingRealm(帶有快取實現)。
      • Session Manager:會話管理。
      • Cache Manager:快取。

在web應用中使用Shiro

  • 加入Shiro的配置檔案 shiro.ini:
# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin

# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest

# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president

# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz

# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *

# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*

# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
# goodguy 對 winnebago 的 eagle5 有 drive 的許可權
goodguy = winnebago:drive:eagle5
  • Shiro通過一個 DelegatingFilterProxy 過濾器作為入口,負責讀取配置檔案如 shiro.ini,然後判斷 URL 是否需要登入/許可權等工作,修改 web.xml 檔案,新增配置如下:
<!-- DelegatingFilterProxy的作用是自動到 spring 容器查詢名稱為 shiroFilter 的 bean 並把所有 Filter 的操作委託於它 -->
<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>
  <filter-name>shiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
  • 修改Spring的配置檔案 applicationContext.xml,新增如下配置:
<!-- Shiro's main business-tier object for web-enabled applications
     (use DefaultSecurityManager instead when there is no web environment)-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
    <!-- 配置單個的realm -->
    <!--<property name="realm" ref="MD5Realm"/>-->
    <!-- 配置authenticator,可以包含realm的list集合,還可以配置認證策略 -->
    <property name="authenticator" ref="realms"></property>
    <!-- 授權時需要從securityManager讀取realms -->
    <property name="realms">
        <list>
            <ref bean="MD5Realm"></ref>
            <ref bean="SHA1Realm"></ref>
        </list>
    </property>

    <!-- 配置RememberMe的cookie的有效期,單位:秒 -->
    <property name="rememberMeManager">
        <bean class="org.apache.shiro.web.mgt.CookieRememberMeManager">
            <property name="cookie.maxAge" value="10"></property>
        </bean>
    </property>
</bean>

<!-- Let's use some enterprise caching support for better performance.  You can replace this with any enterprise
     caching framework implementation that you like (Terracotta+Ehcache, Coherence, GigaSpaces, etc -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <!-- Set a net.sf.ehcache.CacheManager instance here if you already have one.  If not, a new one
         will be creaed with a default config:
         <property name="cacheManager" ref="ehCacheManager"/> -->
    <!-- If you don't have a pre-built net.sf.ehcache.CacheManager instance to inject, but you want
         a specific Ehcache configuration to be used, specify that here.  If you don't, a default
         will be used.:
    <property name="cacheManagerConfigFile" value="classpath:some/path/to/ehcache.xml"/> -->
</bean>

<bean id="realms" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    <property name="realms">
        <list>
            <ref bean="MD5Realm"></ref>
            <ref bean="SHA1Realm"></ref>
        </list>
    </property>
    <!--- 配置認證策略 -->
    <property name="authenticationStrategy">
        <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
    </property>
</bean>

<!-- Used by the SecurityManager to access security data (users, roles, etc).
     Many other realm implementations can be used too (PropertiesRealm,
     LdapRealm, etc. -->
<bean id="MD5Realm" class="cn.mbq.shiro.realm.MD5Realm">
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <!-- 使用MD5演算法進行加密,並設定加密次數 -->
            <property name="hashAlgorithmName" value="MD5"></property>
            <property name="hashIterations" value="1024"></property>
        </bean>
    </property>
</bean>
<bean id="SHA1Realm" class="cn.mbq.shiro.realm.SHA1Realm">
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <!-- 使用SHA1演算法進行加密 -->
            <property name="hashAlgorithmName" value="SHA1"></property>
            <property name="hashIterations" value="1024"></property>
        </bean>
    </property>
</bean>

<!-- =========================================================
     Shiro Spring-specific integration
     ========================================================= -->
<!-- Post processor that automatically invokes init() and destroy() methods
     for Spring-configured Shiro objects so you don't have to
     1) specify an init-method and destroy-method attributes for every bean
        definition and
     2) even know which Shiro objects require these methods to be
        called. -->
<!--  可以自動的來呼叫 配置在pring IOC 容器中 shiro bean的生命週期方法 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- Enable Shiro Annotations for Spring-configured beans.  Only run after
     the lifecycleBeanProcessor has run: -->
<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>

<!-- Define the Shiro Filter here (as a FactoryBean) instead of directly in web.xml -
     web.xml uses the DelegatingFilterProxy to access this bean.  This allows us
     to wire things with more control as well utilize nice Spring things such as
     PropertiesPlaceholderConfigurer and abstract beans or anything else we might need: -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/shiro/tologin"/>
    <property name="successUrl" value="/shiro/success"/>
    <property name="unauthorizedUrl" value="/shiro/unauthorized"/>
    <!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean
         defined will be automatically acquired and available via its beanName in chain
         definitions, but you can perform overrides or parent/child consolidated configuration
         here if you like: -->
    <!-- <property name="filters">
        <util:map>
            <entry key="aName" value-ref="someFilterPojo"/>
        </util:map>
    </property> -->
    <!--
       配置訪問資源所需要的許可權。
        url使用Ant風格:?匹配一個字元,*匹配零個或多個字元,**匹配路徑中的零個或多個路徑。
        採用第一次匹配優先的方式,配置的資源需要放在 /** 前。       預設的許可權過濾器見列舉:DefaultFilter(下面程式碼) 
    -->
    <!--<property name="filterChainDefinitions">
        <value>
            /shiro/login = anon
            /shiro/logout = logout
            /shiro/admin = roles[admin]
            # allow WebStart to pull the jars for the swing app:
            /*.jar = anon
            # everything else requires authentication:
            /** = authc
        </value>
    </property>-->
    <!-- 使用自定義filterChainDefinition的方式替換上面固定的過濾器 -->
    <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>

<!-- 配置工廠建立filterChainDefinitionMap的方法 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionFactory" factory-method="buildFilterChainDefinition"></bean>

<!-- 配置filterChainDefinition工廠 -->
<bean id="filterChainDefinitionFactory" class="cn.mbq.shiro.factory.FilterChainDefinitionFactory"></bean>
  • 預設的許可權過濾器如下:
package org.apache.shiro.web.filter.mgt;

public enum DefaultFilter {

    /* 身份驗證相關的 */
    // 可以匿名使用
    anon(AnonymousFilter.class),
    // 需要認證(登入)才能使用
    authc(FormAuthenticationFilter.class),
    // 需要通過 HttpBasic 驗證
    authcBasic(BasicHttpAuthenticationFilter.class),
    // 登出登入,同時銷燬所有 Session,web 應用程式中 RembemberMe 的 cookie 也將被刪除
    logout(LogoutFilter.class),
    // 必須存在使用者
    user(UserFilter.class);

    /* 授權相關的 */
    // 判斷當前使用者是否具有指定許可權,perms["user.add:*,user.modify:*"],多個引數必須全部通過才行
    perms(PermissionsAuthorizationFilter.class),
    // 指定請求訪問的埠,port[8081]
    port(PortFilter.class),
    // 根據 restful 風格相應,rest[user.method](method為post、get、put、delete等)
    rest(HttpMethodPermissionFilter.class),
    // 判斷當前使用者是否指定角色,roles["admin,guest"]多個引數必須全部通過才行
    roles(RolesAuthorizationFilter.class),
    // 使用 HTTPS,表示安全的 URL 請求
    ssl(SslFilter.class),
   
    // 阻止在請求期間建立新的會話,以保證無狀態的體驗
    noSessionCreation(NoSessionCreationFilter.class),

}

Authenticator:身份認證

  • 基本認證流程圖如下:

Image

image

  1. 首先呼叫 Subject.login(token) 進行登入,其會自動委託給 SecurityManager;
  2. Authenticator 才是真正的身份驗證者,Shiro API 中核心的身份認證入口點,此處可以自定義插入自己的實現;
  3. SecurityManager 負責真正的身份驗證邏輯;它會委託給 Authenticator 進行身份驗證,它繼承了Authenticator,另外還有一個 ModularRealmAuthenticator實現,其委託給多個Realm 進行 驗證,驗證規則通過 AuthenticationStrategy 介面指定;
  4. Authenticator 可能會委託給相應的 AuthenticationStrategy 進 行多 Realm 身份驗證,預設 ModularRealmAuthenticator 會呼叫 AuthenticationStrategy 進行多 Realm 身份驗證;
  5. Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證資訊,如果沒有返回/丟擲異常表示身份驗證失敗了。此處可以配置多個Realm,將按照相應的順序及策略進行訪問。
  • AuthenticationStrategy:認證策略
    • FirstSuccessfulStrategy:只要有一個 Realm 驗證成功即可,只返回第一個 Realm 身份驗證成功的認證資訊,其他的忽略。
    • AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功即可,和 FirstSuccessfulStrategy 不同,將返回所有Realm身份驗證成功的認證資訊。
    • AllSuccessfulStrategy:所有Realm驗證成功才算成功,且返回所有 Realm身份驗證成功的認證資訊,如果有一個失敗就失敗了。
    • ModularRealmAuthenticator 預設是 AtLeastOneSuccessfulStrategy 策略。
  • 程式碼示例:
if (!currentUser.isAuthenticated()) {
    // 當前使用者未認證,將使用者名稱和密碼封裝成一個token進行登入
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    token.setRememberMe(true);
    try {
        currentUser.login(token);
    } catch (UnknownAccountException uae) {
        System.out.println("使用者名稱不存在:" + token.getPrincipal());
    } catch (IncorrectCredentialsException ice) {
        System.out.println("密碼不正確: " + token.getPrincipal());
    } catch (LockedAccountException lae) {
        System.out.println("使用者名稱被鎖定:" + token.getPrincipal());
    }
    // other exceptions 
    catch (AuthenticationException ae) {
        ae.printStackTrace();
    }
}

Authorizer:授權

  • 也叫訪問控制,即在應用中控制誰訪問哪些資源。
  • Subject:主體,代表使用者,使用者只有授權後才能訪問資源。
  • Resource:資源,在應用中使用者可以訪問的URL。
  • Permission:許可權,表示使用者能不能訪問某個資源。
  • Role:角色,許可權的集合,一般情況下賦予使用者角色而不是許可權。
  • 授權流程:

           image

    1. 首先呼叫 Subject.isPermitted*/hasRole* 介面,其會委託給 SecurityManager,而 SecurityManager 接著會委託給 Authorizer;
    2. Authorizer是真正的授權者,如果呼叫如isPermitted(“user:view”),其首先會通過PermissionResolver 把字串轉換成相應的 Permission 例項;
    3. 在進行授權之前,其會呼叫相應的 Realm 獲取 Subject 相應的角色/許可權用於匹配傳入的角色/許可權;
    4. Authorizer 會判斷 Realm 的角色/許可權是否和傳入的匹配,如果有多個Realm,會委託給 ModularRealmAuthorizer 進行迴圈判斷,如果匹配如 isPermitted*/hasRole* 會返回true,否則返回false表示授權失敗。
  • 三種授權方式:
    1. 程式設計式,不推薦使用。
    2. 註解式,推薦
      • @RequiresAuthentication:表示當前 Subject 已經通過 login 進行了身份驗證,即 Subject.isAuthenticated() 為 true。
      • @RequiresUser:表示當前 Subject 已經身份驗證或者通過記住我登入的。

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

      • @RequiresRoles(value={“admin”,““user”}, logical=Logical.AND):表示當前 Subject 需要角色 admin 和 user。

      • @RequiresPermissions (value={“user:a”,“user:b”}, logical=Logical.OR):表示當前 Subject 需要許可權 user:a 或 user:b。

    3. jsp標籤,標準形式如:<shiro:xxx></shiro:xxx>
      • guest 標籤:使用者沒有身份驗證時顯示相應資訊,即遊客訪問資訊。
      • user 標籤:使用者已經經過認證/記住我登入後顯示相應的資訊。

      • authenticated 標籤:使用者已經身份驗證通過,即 Subject.login登入成功,不是 remerberMe 登入的。

      • notAuthenticated 標籤:使用者未進行身份驗證,即沒有呼叫Subject.login進行登入,包括記住我自動登入的也屬於未進行身份驗證。

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

      • hasRole 標籤:如果當前 Subject 有角色將顯示 body 體內容。

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

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

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

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

  • Permissions的配置
    • 規則:資源識別符號:操作:物件例項 ID 即對哪個資源的哪個例項可以進行什麼操作. 其預設支援萬用字元許可權字串,: 表 示資源/操作/例項的分割;, 表示操作的分割,* 表示任意資 源/操作/例項。
    • 多層次管理:
      • 如:user:query、user:edit。
      • 冒號是一個特殊字元,它用來分隔許可權字串的下一部件:第一部分是許可權被操作的領域(印表機),第二部分是被執行的操作。
      • 多個值:每個部件能夠保護多個值。除了授予使用者 user:query和 user:edit 許可權外,也可以簡單地授予他們一個 user:query, edit,還可以用 * 號代替所有的值,如:user:*,或:*:query,表示某個使用者在所有的領域都有 query 的許可權。
    • 例項級訪問控制:
      • 這種情況通常會使用三個部件:域、操作、被付諸實施的例項。如 user:edit:manager。
      • 也可以使用萬用字元來定義,如 user:edit:*,user:*:*,user:*:manager。
      • 部分省略萬用字元:缺少的部件意味著使用者可以訪問所有與之匹配的值,比如 user:edit 等價於 user:edit :*,user 等價於 user:*:*。
      • 注意:萬用字元只能從字串的結尾處省略部件,也就是說 user:edit 並不等價於 user:*:edit。

會話 & 快取

  • Shiro 內部相應的元件(DefaultSecurityManager)會自動檢測相應的物件(如Realm)是否實現了 CacheManagerAware 並自動注入相應的 CacheManager。
  • Shiro 提供了 CachingRealm,其實現了 CacheManagerAware 介面,提供了快取的一些基礎實現。
  • AuthenticatingRealm 及 AuthorizingRealm 也分別提供了對AuthenticationInfo 和 AuthorizationInfo 資訊的快取。
  • Session,不依賴於 web 容器,會自動將 HttpSession 中的資料存放到 org.apache.shiro.session 中。
  • 如 SecurityManager 實現了 SessionSecurityManager,其會判斷 SessionManager 是否實現了 CacheManagerAware 介面,如果實現了會把 CacheManager 設定給它。
  • SessionManager 也會判斷相應的 SessionDAO(如繼承自CachingSessionDAO)是否實現了 CacheManagerAware,如果實現了會把 CacheManager 設定給它。
  • 設定了快取的 SessionManager,查詢時會先查快取,如果找不到才查資料庫。

RememberMe

  • 將Cookie寫入到客戶端,但不記錄重要的資訊,如檢視訂單等仍需重新登入。
  • 認證和RememberMe:
    • subject.isAuthenticated() 表示使用者進行了身份驗證登入的,即使用 Subject.login() 進行了登入;
    • subject.isRemembered():表示使用者是通過記住我登入的,存在cookie欺騙、竊取的風險。
    • 兩者只能二選一,即 subject.isAuthenticated()==true,則 subject.isRemembered()==false;反之亦然。