Shiro框架基礎知識歸納
/login.jsp = anon
- anno:(anonymous)攔截器表示匿名訪問(即不登入即可訪問)。
- authc:(authentication)攔截器表示需要身份認證後才能訪問。
URL配置模式使用了Ant風格模式 :Ant路徑萬用字元支援?、*、**需要注意的是匹配符不包含目錄分割符/。
- _?:匹配一個字元。
- _* :匹配零個或多個字串。
- _** :匹配路徑中的零個或多個路徑。
注意:URL許可權採取第一次匹配優先的方式。
2、身份驗證:在Shrio中使用者需要提高principals(身份)和credentials(證明)給Shiro,從而能驗證使用者身份。
基本流程:1)、呼叫securityManager獲取subject。
2)、驗證當前使用者是否登入,subject.isAuthenticated()。
3)、若未驗證,則將使用者名稱和密碼封裝為UsernamePasswordToken物件。
4)、呼叫subject.login(AutenticationToken)方法驗證,會呼叫5中的方法,實現使用者認證操作。
5)、自定義Realm,從資料庫中獲取使用者記錄,返回給Shiro。
5.1)實際上只需繼承 org.apache.shiro.realm.AuthenticatingRealm 類。
5.2)實現doGetAuthenticationInfo(AuthenticationToken) 方法。---->方法中的Token==login方法傳過來的Token。
6)、由Shiro完成密碼比對。
3、因為Shiro快取的原因,當用戶Token登入成功之後,你回退重新登入Token但是密碼錯誤時,發現扔能夠登入成功。所以當每次結束時需要出發logout命令,簡單辦法如下,在Shiro攔截其中新增如下程式碼。/shiro/logout==需要傳送的URL。
4、身份認證流程:/shiro/logout = logout
- 首先呼叫Subject.login(token)進行登入,其會委託給SecurityManager
- securityManager負責真正的身份驗證邏輯;他會委託給Authenticator進行身份認證。
- Authentication才是真正的身份驗證者,Shrio API中核心的身份認證入口點,此處可以插入自己的實現。
- Authenticator可能會委託給相應的AuthenticationStrategy進行多Realm身份認證,預設ModularRealmAuthenticator會呼叫AuthenticationStrategy進行多Real身份認證。
- Authenticator會把相應的token傳入Realm,從realm獲取身份認證資訊,如果沒有返回/丟擲異常表示認證失敗。此處可以配置多個realm,將按照相應的順序及策略進行訪問。
5、Realm:Shiro從Realm中獲取安全資料(如使用者,角色,許可權)即SecurityManager要驗證使用者身份,需要從Realm中獲取相應的使用者進行比較以確定使用者的身份是否合法;需要從使用者中得要相應的角色和許可權判斷使用者是否可以操作。
6、Realm一般繼承AuthorizingRealm(授權)即可;其繼承了AuthenticatingRealm(即身份認證),而他也間接繼承了CachingRealm(帶有快取實現)。
7、Authenticator的職責是驗證使用者賬號,是Shiro API中身份認證的核心入口點,如果驗證成功。將返回AuthenticationInfo驗證資訊;此資訊中包含身份及憑證;如果異常則會丟擲相應的AuthenticationException異常。
8、AuthenticationStrategy介面的預設實現:
- FirstSuccessfulStrategy:只要有一個Realm驗證成功即可,只返回第一個Realm身份認證成功的認證資訊,其它忽略。
- AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功即可,和FirstSuccessfulStrategy不同的是。將返回所有認證成功的Realm資訊。
- AllSuccessfulStrategy:所有Realm認證成功才算成功,且返回所有Realm身份認證資訊,如果一個失敗就是失敗。
**ModularRealmAuthenticator預設是AtLeastOneSusccessfulStrategy策略。
修改認證策略:
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
</bean>
9、當用戶輸入密碼時,我們通常會對密碼進行加密:在Shiro中我們需要對輸入的密碼加密後認證比較時,只需要在配置檔案中配置密碼的加密方式:
替換當前 Realm 的 credentialsMatcher 屬性. 直接使用 HashedCredentialsMatcher 物件, 並設定加密演算法即可.
<bean id="jdbcRealm" class="com.yintong.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>//加密方式
<property name="hashIterations" value="1024"></property>//加密次數
</bean>
</property>
</bean>
10、如何使兩個原始密碼相同的密碼,加密後不一樣。(MD5鹽值加密)我們將使用者名稱看做是唯一的元素。 ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
** realmName: 當前 realm 物件的 name. 呼叫父類的 getName() 方法即可** String realmName = getName();
- 使用 ByteSource.Util.bytes() 來計算鹽值.
- 鹽值需要唯一: 一般使用隨機字串或 user id
- 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 來計算鹽值加密後的密碼的值.
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
11、有時候我們會將使用者密碼存入多個庫,用不同的加密方式。例如:
<bean id="jdbcRealm" class="com.yintong.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<bean id="secondRealm" class="com.yintong.shiro.realms.SecondRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
當配置多個Realm時,需要配置autenticator來存放。
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
最後將authentication配置在SecurityManager中。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
</bean>
12、通常我們會將多個Reals配置在SecurityManager中:(因為系統初始化的時候,會通過認證是否為ModularRealmAuthenticator,如果是則將Realms的值賦給Authencator)
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>
13、授權:也叫訪問控制,即在應用中控制誰訪問那些資源(如訪問頁面、編輯資料、頁面操作等)在授權中需要了解基本關鍵物件。
- 主體(Subject):訪問應用的使用者。
- 資源(Resource):在應用中使用者可以訪問的URL。
- 許可權(Permission):在應用中使用者能不能訪問某個資源。
- 角色(Role):許可權的集合。
Shrio支援三種授權方式:
- 程式設計式:通過if/else程式碼塊來完成。
- 註解式:通過在執行的方法上放置相應的註解來完成,沒有許可權則丟擲相應異常。(@RequiresRoles("admin"))
- JSP/GSP:在JSP/GSP頁面通過相應的標籤完成。(<Shrio:hasRole name = "admin">)
14、Shrio內建了許多預設攔截器,比如身份認證、授權等相關的。(可參考org.apache.shiro.web.filter.mgt.DefaultFilter中的列舉攔截器)。授權相關攔截器如下:
15、Permissions:規則---->資源識別符號:操作:物件實力ID------>即對那個資源的那個例項可以進行什麼樣的操作。預設支援萬用字元:(:表示資源/操作/例項的分割)
- ,表示操作的分割。(user:query, edit)例項訪問控制(user:edit:manager)
- *表示任意資源/操作/例項。(user:*)例項訪問使用萬用字元(user:edit:*)
流程如下:
- 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表示授權失敗。
**** 授權需要繼承 AuthorizingRealm 類, 並實現其 doGetAuthorizationInfo 方法。AuthorizingRealm 類繼承自 AuthenticatingRealm, 但沒有實現 AuthenticatingRealm 中的 doGetAuthenticationInfo, 所以認證和授權只需要繼承 AuthorizingRealm 就可以了. 同時實現他的兩個抽象方法.
16、ModularRealmAuthorizer 進行多 Realm 匹配流程:
– 1、首先檢查相應的 Realm 是否實現了實現了Authorizer;
– 2、如果實現了 Authorizer,那麼接著呼叫其相應的isPermitted*/hasRole* 介面進行匹配;
– 3、如果有一個Realm匹配那麼將返回 true,否則返回 false。
//授權會被 shiro 回撥的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1. 從 PrincipalCollection 中來獲取登入使用者的資訊
Object principal = principals.getPrimaryPrincipal();
//2. 利用登入的使用者的資訊來偽造當前使用者的角色或許可權(可能需要查詢資料庫)
Set<String> roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
}
//3. 建立 SimpleAuthorizationInfo, 並設定其 reles 屬性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4. 返回 SimpleAuthorizationInfo 物件.
return info;
}
17、Shiro提供了JSTL標籤用於在JSP頁面進行許可權控制,如通過登入使用者顯示相應的按鈕。
- guest標籤:使用者沒有登入時顯示相應資訊,即遊客訪問資訊。<shiro:guest>歡迎遊客訪問</shiro:guest>
- user標籤:使用者已經認證/記住我登入之後顯示相應資訊。<shiro:user>歡迎xx登入</shiro:user>
- authenticated標籤:使用者已經身份認證通過,即shiro.login登入成功,不是記住我登入的。<shiro:authenticated>使用者已認證</shiro:authenticated>
- notAuthenticated標籤:使用者未進行身份認證,即沒有呼叫Subject.login登入,包括記住我登入的也輸入未認證登入。<shiro:noAuthenticated></shiro:noAuthenticated>
- pincipal標籤:顯示使用者身份資訊,預設呼叫Subject.getPrincipal()獲取,即Primary Principal。<shrio:principal property="username"/>
- hasRole標籤:如果當前Subject有角色,將顯示body體內容:<shiro:hasRole name="admin">使用者[<shiro:principal>]使用者角色admin<br/></shiro:hasRole>
- hasAnyRole標籤:如果Subject有任意一個角色(或的關係)將顯示body中的內容。<shiro:hasAnyRole name="admin,user">使用者XX角色</shiro:hasAmyRole>
- lacksRole標籤:如果當前Subject沒有角色將顯示body中的內容。<shiro:lacksRole>沒有xx角色</shiro:lacksRole>
- hasPermission:如果當前Subject有許可權將顯示body中的內容。<shiro:hasPermission>擁有許可權user:create</br></shiro:hasPermission>
- lacksPermission:如果當前Subject沒有許可權將顯示body中的內容<shiro:lacksPermission>沒有許可權org:create</br></shiro:lacksPermission>
18、許可權註解:
- @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。
19、自定義攔截器:通過自定義攔截器可以擴充套件功能,例如:動態url-角色/許可權訪問控制的實現、根據 Subject 身份資訊獲取使用者資訊繫結到 Request(即設定通用資料)、驗證碼驗證、線上使用者資訊的儲存等。
我們一般在獲取使用者許可權的時候,都是從資料庫中獲取的。所以在配置檔案中會通過工廠方式創建出需要的物件。根據程式碼跟蹤,我們發現獲取到使用者許可權的時候會呼叫filterChainDefinitionMap方法,得到我們在配置中配置的所有許可權,已LinkedHashMap的方式收集。所以我們需要重新配置filterChainDefinitionMap。如下:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
</bean>
<!-- 配置一個 bean, 該 bean 實際上是一個 Map. 通過例項工廠方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.yintong.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
建立相應的實體類,建立一個方法用來返回我們需要的LinkedHashMap實體。在方法中通過獲取資料庫,拿到對應使用者的角色/許可權。
package com.yintong.shiro.factory;
import java.util.LinkedHashMap;
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user");
map.put("/**", "authc");
return map;
}
}
20、會話管理:Shiro 提供了完整的企業級會話管理功能,不依賴於底層容器(如web容器tomcat),不管 JavaSE 還是 JavaEE 環境都可以使用,提供了會話管理、會話事件監聽、會話儲存/持久化、容器無關的叢集、失效/過期支援、對Web 的透明支援、SSO 單點登入的支援等特性。
- Subject.getSession():即可獲取會話;
- session.getId():獲取當前會話的唯一標識
- session.getHost():獲取當前Subject的主機地址
- session.getTimeout() & session.setTimeout(毫秒):獲取/設定當前Session的過期時間
- session.getStartTimestamp() & session.getLastAccessTime():獲取會話的啟動時間及最後訪問時間;如果是 JavaSE 應用需要自己定期呼叫 session.touch() 去更新最後訪問時間;如果是 Web 應用,每次進入 ShiroFilter 都會自動呼叫 session.touch() 來更新最後訪問時間。
- session.touch() & session.stop():更新會話最後訪問時間及銷燬會話;當Subject.logout()時會自動呼叫 stop 方法來銷燬會話。如果在web中,呼叫 HttpSession. invalidate()也會自動呼叫Shiro Session.stop 方法進行銷燬Shiro 的會話
- session.setAttribute(key, val) &session.getAttribute(key) &session.removeAttribute(key):設定/獲取/刪除會話屬性;在整個會話範圍內都可以對這些屬性進行操作
主要特點:web層的session通常在service層不能夠獲取,但是shiro的session能夠在service層獲取到。
SessionDao的種類:
- AbstractSessionDAO 提供了 SessionDAO 的基礎實現,如生成會話ID等
- CachingSessionDAO 提供了對開發者透明的會話快取的功能,需要設定相應的 CacheManager
- MemorySessionDAO 直接在記憶體中進行會話維護
- EnterpriseCacheSessionDAO 提供了快取功能的會話維護,預設情況下使用 MapCache 實現,內部使用ConcurrentHashMap 儲存快取的會話
會話驗證:
- Shiro 提供了會話驗證排程器,用於定期的驗證會話是否已過期,如果過期將停止會話
- 出於效能考慮,一般情況下都是獲取會話時來驗證會話是否過期並停止會話的;但是如在 web 環境中,如果使用者不主動退出是不知道會話是否過期的,因此需要定期的檢測會話是否過期,Shiro 提供了會話驗證排程器SessionValidationScheduler
- Shiro 也提供了使用Quartz會話驗證排程器:QuartzSessionValidationScheduler
21、快取:CacheManagerAware 介面
- Shiro 內部相應的元件(DefaultSecurityManager)會自動檢測相應的物件(如Realm)是否實現了CacheManagerAware 並自動注入相應的CacheManager。
- Shiro 提供了 CachingRealm,其實現了CacheManagerAware 介面,提供了快取的一些基礎實現;
- AuthenticatingRealm 及 AuthorizingRealm 也分別提供了對AuthenticationInfo 和 AuthorizationInfo 資訊的快取。
Session快取:
- 如 SecurityManager 實現了 SessionSecurityManager,其會判斷 SessionManager 是否實現了CacheManagerAware 介面,如果實現了會把CacheManager 設定給它。
- SessionManager 也會判斷相應的 SessionDAO(如繼承自CachingSessionDAO)是否實現了CacheManagerAware,如果實現了會把 CacheManager設定給它
- 設定了快取的 SessionManager,查詢時會先查快取,如果找不到才查資料庫
22、RememberMe:Shiro 提供了記住我(RememberMe)的功能,基本流程如下:
- 首先在登入頁面選中 RememberMe 然後登入成功;如果是瀏覽器登入,一般會把 RememberMe 的Cookie 寫到客戶端並儲存下來;
- 關閉瀏覽器再重新開啟;會發現瀏覽器還是記住你的;
- 訪問一般的網頁伺服器端還是知道你是誰,且能正常訪問;
- 但是比如我們訪問淘寶時,如果要檢視我的訂單或進行支付時,此時還是需要再進行身份認證的,以確保當前使用者還是你。
重點:認證和記住我的區別:
- subject.isAuthenticated() 表示使用者進行了身份驗證登入的,即使有 Subject.login 進行了登入;
- subject.isRemembered():表示使用者是通過記住我登入的,此時可能並不是真正的你(如你的朋友使用你的電腦,或者你的cookie 被竊取)在訪問的。
- 兩者二選一,即 subject.isAuthenticated()==true,則subject.isRemembered()==false;反之一樣。
建議:
- 訪問一般網頁:如個人在主頁之類的,我們使用user 攔截器即可,user 攔截器只要使用者登入(isRemembered() || isAuthenticated())過即可訪問成功;
- 訪問特殊網頁:如我的訂單,提交訂單頁面,我們使用authc 攔截器即可,authc 攔截器會判斷使用者是否是通過Subject.login(isAuthenticated()==true)登入的,如果是才放行,否則會跳轉到登入頁面叫你重新登入。
實現:如果要自己做RememeberMe,需要在登入之前這樣建立Token:UsernamePasswordToken(使用者名稱,密碼,是否記住我),且呼叫UsernamePasswordToken 的:token.setRememberMe(true); 方法。
預設攔截器user表示記住我:
map.put("/list.jsp", "user");
表示list.jsp可以通過記住我登入後訪問。
23、設定記住我的有效時間:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="rememberMeManager.cookie.maxAge" value="10"></property>
</bean>