Shiro 安全框架
-
Shiro安全框架簡介
-
Shiro概述
Shiro是apache旗下一個開源安全框架(http://shiro.apache.org/),它將軟體系統的安全認證相關的功能抽取出來,實現使用者身份認證,許可權授權、加密、會話管理等功能,組成了一個通用的安全認證框架。使用shiro就可以非常快速的完成認證、授權等功能的開發,降低系統成本。
使用者在進行資源訪問時,要求系統要對使用者進行許可權控制,其具體流程如圖-1所示:
-
Shiro概要架構
在概念層面,Shiro 架構包含三個主要的理念,如圖-2所示:
其中:
-
Subject :主體物件,負責提交使用者認證和授權資訊。
-
SecurityManager:安全管理器,負責認證,授權等業務實現。
-
Realm:領域物件,負責從資料層獲取業務資料。
-
Shiro詳細架構
Shiro框架進行許可權管理時,要涉及到的一些核心物件,主要包括:認證管理物件,授權管理物件,會話管理物件,快取管理物件,加密管理物件以及Realm管理物件(領域物件:負責處理認證和授權領域的資料訪問題)等,其具體架構如圖-3所示:
其中:
-
Subject(主體):與軟體互動的一個特定的實體(使用者、第三方服務等)。
-
SecurityManager(安全管理器) :Shiro 的核心,用來協調管理元件工作。
-
Authenticator(認證管理器):負責執行認證操作。
-
Authorizer(授權管理器):負責授權檢測。
-
SessionManager(會話管理):負責建立並管理使用者 Session 生命週期,提供一個強有力的 Session 體驗。
-
SessionDAO:代表 SessionManager 執行 Session 持久(CRUD)動作,它允許任何儲存的資料掛接到 session 管理基礎上。
-
CacheManager(快取管理器):提供建立快取例項和管理快取生命週期的功能。
-
Cryptography(加密管理器):提供了加密方式的設計及管理。
-
Realms(領域物件):是shiro和你的應用程式安全資料之間的橋樑。
-
Shiro框架認證攔截實現(filter)
-
Shiro基本環境配置
-
新增shiro依賴
實用spring整合shiro時,需要在pom.xml中新增如下依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.2</version> </dependency>
-
Shiro核心物件配置
基於SpringBoot 實現的專案中,沒有提供shiro的自動化配置,需要我們自己配置。
第一步:建立SpringShiroConfig類。關鍵程式碼如下:
package com.cy.pj.common.config; /**@Configuration 註解描述的類為一個配置物件, * 此物件也會交給spring管理 */ @Configuration public class SpringShiroConfig { }
第二步:在Shiro配置類中新增SecurityManager配置(這裡一定要使用org.apache.shiro.mgt.SecurityManager這個介面物件),關鍵程式碼如下:
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); return sManager; }
第三步: 在Shiro配置類中新增ShiroFilterFactoryBean物件的配置。通過此物件設定資源匿名訪問、認證訪問。關鍵程式碼如下
-
-
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager,) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //定義map指定請求過濾規則(哪些資源允許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //靜態資源允許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); //除了匿名訪問的資源,其它都要認證("authc")後訪問 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
其配置過程中,物件關係如下圖-4所示:
-
Shiro登陸頁面呈現
-
服務端Controller實現
-
業務描述及設計實現
當服務端攔截到使用者請求以後,判定此請求是否已經被認證,假如沒有認證應該先跳轉到登入頁面。
-
關鍵程式碼分析及實現.
第一步:在PageController中新增一個呈現登入頁面的方法,關鍵程式碼如下:
@RequestMapping("doLoginUI") public String doLoginUI(){ return "login"; }
第二步:修改SpringShiroConfig類中shiroFilterFactorybean的配置,新增登陸url的設定。關鍵程式碼見sfBean.setLoginUrl("/doLoginUI") 第7行 部分。
1 @Bean 2 public ShiroFilterFactoryBean shiroFilterFactory ( 3 @Autowired SecurityManager securityManager) { 4 ShiroFilterFactoryBean sfBean= 5 new ShiroFilterFactoryBean(); 6 sfBean.setSecurityManager(securityManager); 7 sfBean.setLoginUrl("/doLoginUI"); 8 //定義map指定請求過濾規則(哪些資源允許匿名訪問, 9 哪些必須認證訪問) 10 LinkedHashMap<String,String> map= 11 new LinkedHashMap<>(); 12 //靜態資源允許匿名訪問:"anon" 13 map.put("/bower_components/**","anon"); 14 map.put("/modules/**","anon"); 15 map.put("/dist/**","anon"); 16 map.put("/plugins/**","anon"); 17 //除了匿名訪問的資源,其它都要認證("authc")後訪問 18 map.put("/**","authc"); 19 sfBean.setFilterChainDefinitionMap(map); 20 return sfBean; 21 }
-
Shiro框架認證業務實現
-
認證流程分析
身份認證即判定使用者是否是系統的合法使用者,使用者訪問系統資源時的認證(對使用者身份資訊的認證)流程圖-5所示:
步驟1:應用程式程式碼呼叫該Subject.login
方法,並傳入AuthenticationToken
表示終端使用者的主體和憑據的構造例項。
第2步:Subject
例項(通常是一個DelegatingSubject
(或子類))SecurityManager
通過呼叫來委託應用程式,例項securityManager.login(token)
從此處開始實際的身份驗證工作。
步驟3:SecurityManager
作為基本的“傘”元件,接收令牌並Authenticator
通過呼叫來簡單地委派給其內部例項authenticator.authenticate(token)
。這幾乎總是一個ModularRealmAuthenticator
例項,它支援Realm
在身份驗證期間協調一個或多個例項。在ModularRealmAuthenticator
本質上提供一個PAM為Apache四郎(其中每個樣式的範例Realm
是在PAM術語一個“模組”)。
步驟4:如果Realm
為該應用程式配置了多個,則該ModularRealmAuthenticator
例項將Realm
使用其configureed發起多身份驗證嘗試AuthenticationStrategy
。在Realms
呼叫進行身份驗證之前,期間和之後,將呼叫,AuthenticationStrategy
以允許它對每個Realm的結果做出反應。我們將AuthenticationStrategies
儘快覆蓋。
第5步:Realm
諮詢每個配置,以檢視是否supports
已提交AuthenticationToken
。如果是這樣,支援的RealmgetAuthenticationInfo
方法將與Submitted一起呼叫token
。該getAuthenticationInfo
方法有效地表示針對該特定物件的單個身份驗證嘗試Realm
。我們將Realm
很快介紹身份驗證行為。
簡單說就是
-
系統呼叫subject的login方法將使用者資訊提交給SecurityManager
-
SecurityManager將認證操作委託給認證器物件Authenticator
-
Authenticator將使用者輸入的身份資訊傳遞給Realm。
-
Realm訪問資料庫獲取使用者資訊然後對資訊進行封裝並返回。
-
Authenticator 對realm返回的資訊進行身份認證。
-
認證服務端實現
-
核心業務分析
認證業務API處理流程分析,如圖-6所示:
-
業務描述及設計實現。
在使用者資料層物件SysUserDao中,按特定條件查詢使用者資訊,並對其進行封裝。
-
關鍵程式碼分析及實現。
在SysUserDao介面中,新增根據使用者名稱獲取使用者物件的方法,關鍵程式碼如下:
SysUser findUserByUserName(String username)。
-
Mapper元素定義
-
業務描述及設計實現。
根據SysUserDao中定義的方法,在SysUserMapper檔案中新增元素定義。
-
關鍵程式碼分析及實現。
基於使用者名稱獲取使用者物件的方法,關鍵程式碼如下:
<select id="findUserByUserName" resultType="com.cy.pj.sys.entity.SysUser"> select * from sys_users where username=#{username} </select>
-
Service介面及實現
-
業務描述及設計實現。
本模組的業務在Realm型別的物件中進行實現,我們編寫realm時,要繼承
AuthorizingRealm並重寫相關方法,完成認證及授權業務資料的獲取及封裝。
-
關鍵程式碼分析及實現。
第一步:定義ShiroUserRealm類,關鍵程式碼如下:
package com.cy.pj.sys.service.realm; @Service public class ShiroUserRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; /** * 設定憑證匹配器(與使用者新增操作使用相同的加密演算法) */ @Override public void setCredentialsMatcher( CredentialsMatcher credentialsMatcher) { //構建憑證匹配物件 HashedCredentialsMatcher cMatcher= new HashedCredentialsMatcher(); //設定加密演算法 cMatcher.setHashAlgorithmName("MD5"); //設定加密次數 cMatcher.setHashIterations(1); super.setCredentialsMatcher(cMatcher); } /** * 通過此方法完成認證資料的獲取及封裝,系統 * 底層會將認證資料傳遞認證管理器,由認證 * 管理器完成認證操作。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //1.獲取使用者名稱(使用者頁面輸入) UsernamePasswordToken upToken= (UsernamePasswordToken)token; String username=upToken.getUsername(); //2.基於使用者名稱查詢使用者資訊 SysUser user= sysUserDao.findUserByUserName(username); //3.判定使用者是否存在 if(user==null) throw new UnknownAccountException(); //4.判定使用者是否已被禁用。 if(user.getValid()==0) throw new LockedAccountException(); //5.封裝使用者資訊 ByteSource credentialsSalt= ByteSource.Util.bytes(user.getSalt()); //記住:構建什麼物件要看方法的返回值 SimpleAuthenticationInfo info= new SimpleAuthenticationInfo( user,//principal (身份) user.getPassword(),//hashedCredentials credentialsSalt, //credentialsSalt getName());//realName //6.返回封裝結果 return info;//返回值會傳遞給認證管理器(後續 //認證管理器會通過此資訊完成認證操作) } .... }
第二步:對此realm,需要在SpringShiroConfig配置類中,注入給SecurityManager物件,修改securityManager方法,見黃色背景部分,例如:
1 @Bean 2 public SecurityManager securityManager(Realm realm) { 3 DefaultWebSecurityManager sManager= 4 new DefaultWebSecurityManager(); 5 sManager.setRealm(realm); 6 return sManager; 7 }
-
Controller 類實現
-
業務描述及設計實現。
在此物件中定義相關方法,處理客戶端的登陸請求,例如獲取使用者名稱,密碼等然後提交該shiro框架進行認證。
-
關鍵程式碼分析及實現。
第一步:在SysUserController中新增處理登陸的方法。關鍵程式碼如下:
1 @RequestMapping("doLogin") 2 public JsonResult doLogin(String username,String password){ 3 //1.獲取Subject物件 4 Subject subject=SecurityUtils.getSubject(); 5 //2.通過Subject提交使用者資訊,交給shiro框架進行認證操作 6 //2.1對使用者進行封裝 7 UsernamePasswordToken token= 8 new UsernamePasswordToken( 9 username,//身份資訊 10 password);//憑證資訊 11 //2.2對使用者資訊進行身份認證 12 subject.login(token); 13 //分析: 14 //1)token會傳給shiro的SecurityManager 15 //2)SecurityManager將token傳遞給認證管理器 16 //3)認證管理器會將token傳遞給realm 17 return new JsonResult("login ok"); 18 }
第二步:修改shiroFilterFactory的配置,對/user/doLogin這個路徑進行匿名訪問的配置,檢視如下黃色標記部分的程式碼:
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //假如沒有認證請求先訪問此認證的url sfBean.setLoginUrl("/doLoginUI"); //定義map指定請求過濾規則(哪些資源允許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //靜態資源允許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); map.put("/user/doLogin","anon"); //authc表示,除了匿名訪問的資源,其它都要認證("authc")後才能訪問訪問 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
第三步:當我們在執行登入操作時,為了提高使用者體驗,可對系統中的異常資訊進行處理,例如,在統一異常處理類中新增如下方法:
@ExceptionHandler(ShiroException.class) @ResponseBody public JsonResult doHandleShiroException( ShiroException e) { JsonResult r=new JsonResult(); r.setState(0); if(e instanceof UnknownAccountException) { r.setMessage("賬戶不存在"); }else if(e instanceof LockedAccountException) { r.setMessage("賬戶已被禁用"); }else if(e instanceof IncorrectCredentialsException) { r.setMessage("密碼不正確"); }else if(e instanceof AuthorizationException) { r.setMessage("沒有此操作許可權"); }else { r.setMessage("系統維護中"); } e.printStackTrace(); return r; }
-