javascript
Shiro从入门到实战(整合进SpringBoot)
文章目錄
- 前言
- 一、Shiro是什么?
- 二、Shiro 的功能介紹
- 1.基本功能點(diǎn)
- 2.功能點(diǎn)介紹
- 三、Shiro 的架構(gòu)
- 1.Shrio的應(yīng)用層面架構(gòu)
- 2.Shiro的核心架構(gòu)
- 四、Shiro入門案例
- 1.shiro.ini
- 2.Quickstart.class
- 五、Shiro實(shí)戰(zhàn)-整合進(jìn)Spring Boot
- 1.創(chuàng)建父工程shiroDemo
- 2.創(chuàng)建shiroAuth子模塊(Maven)
- 3.代碼結(jié)構(gòu)講解
- 4.各個(gè)類詳細(xì)講解
- 5.補(bǔ)充多Realm認(rèn)證
前言
這篇文章主要是為了輔助記憶Shiro的,下面的代碼主要復(fù)用了我看的一個(gè)視頻里面的代碼:https://www.bilibili.com/video/BV1Tf4y1w7Yo,所以有些地方如果不太了解的話可以去看看這個(gè)視頻,不過這個(gè)視頻有些地方可能有點(diǎn)繞,所以如果有shiro基礎(chǔ),只是想回顧一下的話,我覺得看這篇文章應(yīng)該就可以很快回顧起來,如果沒有shiro基礎(chǔ)的話可能還是得跟著視頻一起看。由于本人水平有限,所以可能有些地方寫的不是很好,有發(fā)現(xiàn)什么問題的話,大家都可以指出來,共同進(jìn)步!
一、Shiro是什么?
- Apache Shiro是Java的一個(gè)安全(權(quán)限)框架。
- Shiro可以非常容易的開發(fā)出足夠好的應(yīng)用,其不僅可以用在JavaSE環(huán)境,也可以用在JavaEE環(huán)境。
- Shiro可以完成:認(rèn)證、授權(quán)、加密、會(huì)話管理、與Web集成、緩存等。
Shiro官網(wǎng):http://shiro.apache.org/
二、Shiro 的功能介紹
1.基本功能點(diǎn)
2.功能點(diǎn)介紹
- Authentication(認(rèn)證):判斷一個(gè)用戶是否為合法用戶的處理過程。最常用的身份認(rèn)證方式是核對用戶輸入的用戶名和密碼是否與系統(tǒng)中存儲(chǔ)的用戶名和密碼一致。還可以采用指紋和人臉識別等方式認(rèn)證;
- Authorization(授權(quán)):訪問控制或權(quán)限驗(yàn)證,簡單來說就是給不同的角色授予不同的權(quán)限,以及驗(yàn)證某個(gè)已認(rèn)證的用戶是否擁有某個(gè)權(quán)限;
- Session Manager(會(huì)話管理):管理所有用戶登錄后的會(huì)話信息。用戶登錄后用Subject.getSession() 即可獲取會(huì)話,在沒有退出之前,用戶的所有信息都在會(huì)話中;會(huì)話可以是普通JavaSE環(huán)境,也可以是Web環(huán)境的;
- Cryptography(加密):保護(hù)數(shù)據(jù)的安全性,如密碼加密存儲(chǔ)到數(shù)據(jù)庫,而不是明文存儲(chǔ);
- Web Support(Web支持):可以非常容易的集成到Web環(huán)境;
- Caching(緩存):用戶登錄后,其用戶信息、擁有的角色/權(quán)限不必每次去查,直接從緩存中獲取,這樣可以提高效率;
- Concurrency:Shiro支持多線程應(yīng)用的并發(fā)驗(yàn)證,即如在一個(gè)線程中開啟另一個(gè)線程,能把權(quán)限自動(dòng)傳播過去;
- Testing:提供測試支持;
- Run As:允許一個(gè)用戶假裝為另一個(gè)用戶(如果他們允許)的身份進(jìn)行訪問;
- Remember Me(記住我):非常常見的功能,即一次登錄后,下次無需登錄;
三、Shiro 的架構(gòu)
1.Shrio的應(yīng)用層面架構(gòu)
- Subject:任何直接與應(yīng)用代碼交互的對象。Subject代表了當(dāng)前“用戶”,這個(gè)用戶不具體指代某個(gè)人,只要與當(dāng)前應(yīng)用交互的任何東西都是Subject,如第三方服務(wù)、網(wǎng)絡(luò)爬蟲、機(jī)器人等;所有Subject實(shí)例都必須被綁定到一個(gè)SecurityManager上,SecurityManager才是實(shí)際的執(zhí)行者;
- SecurityManager(安全管理器):與安全有關(guān)的操作都會(huì)與SecurityManager進(jìn)行交互;管理著所有的Subject;它是Shiro的核心,負(fù)責(zé)與Shiro的其他組件進(jìn)行交互,相當(dāng)于SpringMVC中DispatcherServlet的角色;
- Realm:擔(dān)當(dāng)Shiro和你的應(yīng)用程序的安全數(shù)據(jù)之間的“橋梁”或“連接器”。Shiro可以從Realm中獲取安全數(shù)據(jù)(如用戶、角色、權(quán)限),即認(rèn)證和授權(quán)等操作所需要的安全數(shù)據(jù)都需要從Realm中獲得;
2.Shiro的核心架構(gòu)
- Subject:任何直接與應(yīng)用代碼交互的對象;
- SecurityManager(安全管理器):所有與安全相關(guān)的操作都會(huì)與SecurityManager進(jìn)行交互;它管理著所有的Subject;它是Shiro的核心,負(fù)責(zé)與Shiro的其他組件進(jìn)行交互,相當(dāng)于SpringMVC中DispatcherServlet的角色;
- Authenticator:負(fù)責(zé)Subject認(rèn)證,當(dāng)一個(gè)用戶嘗試登錄時(shí),該邏輯被 Authenticator執(zhí)行;且它是一個(gè)擴(kuò)展點(diǎn),可以自定義實(shí)現(xiàn);可以使用認(rèn)證策略(Authentication Strategy),即什么情況下算用戶認(rèn)證通過;
- Authorizer(授權(quán)器):即訪問控制器,用來決定主體(Subject)是否有權(quán)限進(jìn)行相應(yīng)的操作;即控制著用戶能訪問應(yīng)用中的哪些功能;
- Realm:可以有1個(gè)或多個(gè)Realm,可以認(rèn)為是安全實(shí)體數(shù)據(jù)源,即用于獲取安全數(shù)據(jù);可以是JDBC實(shí)現(xiàn),也可以是內(nèi)存實(shí)現(xiàn)等等;它由用戶提供,所以一般在應(yīng)用中都需要實(shí)現(xiàn)自己的Realm;
- SessionManager:管理所有用戶登錄后的會(huì)話信息。用戶登錄后用Subject.getSession() 即可獲取會(huì)話,在沒有退出之前,用戶的所有信息都在會(huì)話中;會(huì)話可以是普通JavaSE環(huán)境,也可以是Web環(huán)境的;
- CacheManager(緩存控制器):管理如用戶、角色、權(quán)限等信息的緩存;因?yàn)檫@些數(shù)據(jù)
基本上很少改變,放到緩存中后可以提高訪問的性能; - Cryptography:密碼模塊,Shiro提供了一些常見的加密組件用于如密碼加密/解密;
四、Shiro入門案例
以下是Shiro官網(wǎng)的一個(gè)入門小案例,供我們參考學(xué)習(xí),我們主要關(guān)注其中的Quickstart.class以及shiro.ini中的部分代碼就好。shiro.ini中主要放的就是用戶、角色以及權(quán)限等數(shù)據(jù),Quickstart.class就是簡單的模擬一下認(rèn)證及授權(quán)的過程,其中所用到的數(shù)據(jù)就來源于shiro.ini中,實(shí)際開發(fā)中我們的這些數(shù)據(jù)肯定是從數(shù)據(jù)庫中獲得。
1.shiro.ini
[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] # '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 'delete' (action) the User(type) with # license plate 'zhangsan' (instance specific id) goodguy = User:delete:zhangsan2.Quickstart.class
package com.m33.shiro.helloworld;import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory;public class Quickstart {private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);public static void main(String[] args) {// 創(chuàng)建SecurityManager(下面這三行看看就好)Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);// get the currently executing user// 獲取當(dāng)前的 Subject!調(diào)用 SecurityUtils.getSubject();Subject currentUser = SecurityUtils.getSubject();// Do some stuff with a Session (no need for a web or EJB container!!!)// 測試使用 Session // 首先獲取 Session: Subject的getSession()Session session = currentUser.getSession();session.setAttribute("someKey", "aValue");String value = (String) session.getAttribute("someKey");if (value.equals("aValue")) {log.info("----> Retrieved the correct value! [" + value + "]");}// let's login the current user so we can check against roles and permissions:// 測試當(dāng)前的用戶是否已經(jīng)被認(rèn)證. 即是否已經(jīng)登錄. // 調(diào)用 Subject 的 isAuthenticated() ,若未認(rèn)證,執(zhí)行if里面的代碼if (!currentUser.isAuthenticated()) {// 把用戶名和密碼封裝為 UsernamePasswordToken 對象UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");// remembermetoken.setRememberMe(true);try {// 執(zhí)行登錄. currentUser.login(token);} // 若沒有指定的賬戶, 則 shiro 將會(huì)拋出 UnknownAccountException 異常. catch (UnknownAccountException uae) {log.info("----> There is no user with username of " + token.getPrincipal());return; } // 若賬戶存在, 但密碼不匹配, 則 shiro 會(huì)拋出 IncorrectCredentialsException 異常。 catch (IncorrectCredentialsException ice) {log.info("----> Password for account " + token.getPrincipal() + " was incorrect!");return; } // 用戶被鎖定的異常 LockedAccountExceptioncatch (LockedAccountException lae) {log.info("The account for username " + token.getPrincipal() + " is locked. " +"Please contact your administrator to unlock it.");}// ... catch more exceptions here (maybe custom ones specific to your application?// 所有認(rèn)證時(shí)異常的父類. catch (AuthenticationException ae) {//unexpected condition? error?}}// 成功登錄log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully.");//test a role:// 測試是否有某一個(gè)角色. 調(diào)用 Subject 的 hasRole 方法. if (currentUser.hasRole("schwartz")) {log.info("----> May the Schwartz be with you!");} else {log.info("----> Hello, mere mortal.");return; }//test a typed permission (not instance-level)// 測試用戶是否具備某一個(gè)行為. 調(diào)用 Subject 的 isPermitted() 方法。 if (currentUser.isPermitted("lightsaber:weild")) {log.info("----> You may use a lightsaber ring. Use it wisely.");} else {log.info("Sorry, lightsaber rings are for schwartz masters only.");}//a (very powerful) Instance Level permission:// 測試用戶是否具備某一個(gè)行為. User:delete:zhangsan: 表示允許刪除User類型的zhangsanif (currentUser.isPermitted("User:delete:zhangsan")) {log.info("----> You are permitted to 'delete'(action) the User(type)with license plate (id) 'zhangsan'. " +"Here are the keys - have fun!");} else {log.info("Sorry, you aren't allowed to 'delete' the 'zhangsan' User!");}//all done - log out!// 執(zhí)行登出. 調(diào)用 Subject 的 Logout() 方法. System.out.println("---->" + currentUser.isAuthenticated()); // truecurrentUser.logout();System.out.println("---->" + currentUser.isAuthenticated()); // falseSystem.exit(0);} }五、Shiro實(shí)戰(zhàn)-整合進(jìn)Spring Boot
這里先簡單介紹一下我們這次實(shí)戰(zhàn)所要完成的功能,其實(shí)主要就是對用戶登錄進(jìn)行認(rèn)證以及授予某些權(quán)限,讓不同用戶登錄的時(shí)候看到不同資源。下面代入實(shí)際的應(yīng)用場景給大家講解一下:
首先我們有三個(gè)用戶admin、manager、worker,它們分別具有role(角色):admin、manager、worker;admin角色擁有訪問mobile、salary資源的權(quán)限,manager角色擁有訪問salary資源的權(quán)限;woker角色不具備任何權(quán)限;當(dāng)我們用admin用戶登錄的時(shí)候就可以查看mobile和salary,manager只能查看salary,worker則什么都查看不了
1.創(chuàng)建父工程shiroDemo
以下為父工程的pom.xml文件內(nèi)容,需要引入以下依賴
<properties><java.version>1.8</java.version> </properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.3.3.RELEASE</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId><version>1.6.0</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies> </dependencyManagement>2.創(chuàng)建shiroAuth子模塊(Maven)
以下為子工程的pom.xml文件內(nèi)容,需要引入以下依賴,具體版本由父工程管理,這里只需添加我們需要用到的依賴即可
<dependencies><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId></dependency> </dependencies>3.代碼結(jié)構(gòu)講解
下圖是我們本次實(shí)戰(zhàn)項(xiàng)目的代碼結(jié)構(gòu),其中藍(lán)色劃掉的部分先暫時(shí)不用管它,后續(xù)我會(huì)講解的
下面帶大家先簡單的了解下這些類所要實(shí)現(xiàn)的功能,后面再進(jìn)行更詳細(xì)的講解
| User | 實(shí)體類,主要就是定義一些屬性:用戶名、密碼、角色和權(quán)限等 |
| MyRealm | 一個(gè)自定義的Realm,主要負(fù)責(zé)獲取認(rèn)證和授權(quán)所需要的信息 |
| ShiroConfig | shiro的配置類,緩存、過濾器等功能在這里進(jìn)行配置,在編譯的時(shí)候便會(huì)先執(zhí)行該配置類,后續(xù)登錄認(rèn)證或者授權(quán)的時(shí)候就會(huì)進(jìn)到MyRealm中獲取數(shù)據(jù) |
| LoginController | 負(fù)責(zé)登錄業(yè)務(wù)流程的控制 |
| MobileController | 負(fù)責(zé)獲取mobile |
| SalaryController | 負(fù)責(zé)獲取salary |
| UserService | 負(fù)責(zé)獲取TestData中數(shù)據(jù),返回給LoginController |
| MyPassWordEncoder | 這是我們封裝的對密碼加鹽加密的工具類 |
| TestData | 一些測試數(shù)據(jù),主要是為了模擬UserService獲取數(shù)據(jù)庫中的數(shù)據(jù),我們這次實(shí)戰(zhàn)并沒有連接數(shù)據(jù)庫,為了測試使用才有了這個(gè)工具類,實(shí)際開發(fā)中這些數(shù)據(jù)需要存放在數(shù)據(jù)庫中 |
4.各個(gè)類詳細(xì)講解
User
import java.util.List; public class User {private String userName;private String userPass;private List<String> userRoles; //用戶的角色private List<String> userPerms; //用戶所具有的權(quán)限// 省略構(gòu)造方法和get/set方法... }MyRealm
import com.m33.shiroAuth.bean.User; import com.m33.shiroAuth.service.UserService; import com.m33.shiroAuth.utils.MyPassWordEncoder; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration;import javax.annotation.Resource;@Configuration(value = "myRealm") public class MyRealm extends AuthorizingRealm {private final Logger logger = LoggerFactory.getLogger(MyRealm.class);@Resourceprivate UserService userService;//授權(quán)@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {logger.info("---------->entered method doGetAuthorizationInfo;");//拿到當(dāng)前用戶Subject subject = SecurityUtils.getSubject();User currentUser = (User)subject.getPrincipal();//寫入當(dāng)前用戶的角色和權(quán)限信息SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.addRoles(currentUser.getUserRoles());info.addStringPermissions(currentUser.getUserPerms());return info;}// 認(rèn)證@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {logger.info("--------------->entered method doGetAuthenticationInfo;");//獲得當(dāng)前用戶的tokenUsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;//從token中獲得當(dāng)前用戶的用戶名String username = userToken.getUsername();//模擬通過用戶名獲取數(shù)據(jù)庫中用戶密碼User user = userService.getUserByUserName(username);if(null == user){//后續(xù)會(huì)拋出UnknownAccountExceptionreturn null;}else{//MyPassWordEncoder.getEncodedPassword()是通過我們的工具類對用戶密碼進(jìn)行加鹽加密//正常是不需要這一步的,注冊的時(shí)候就需要對密碼進(jìn)行加鹽加密。因?yàn)槲覀儧]有模擬注冊流程,// 所以現(xiàn)在密碼是明文,我們才需要先把明文密碼加鹽加密,后續(xù)才能與輸入的密碼進(jìn)行比對user.setUserPass(MyPassWordEncoder.getEncodedPassword(user.getUserPass()));//各參數(shù)含義分別為當(dāng)前用戶、用戶密碼(加密后的)、鹽值(里面的“salt”可隨意設(shè)置)、realm名SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getUserPass(),ByteSource.Util.bytes("salt"), "myRealm");//密碼驗(yàn)證邏輯由shiro完成,我們只需返回SimpleAuthenticationInfo對象即可。密碼不匹配會(huì)拋出密碼不匹配異常。//密碼正確,會(huì)把user對象傳入SecurityUtils.getSubject()。這些都是shiro幫我們做的。return authenticationInfo;}} }ShiroConfig
import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map;@Configuration public class ShiroConfig {// 1、注冊Realm對象,在MyRealm中有@Configuration(value = "myRealm"),已經(jīng)注冊好Realm對象了//所以以下代碼就不需要了,但是我們需要知道配置shiro一般都需要以下三個(gè)步驟,我們可以當(dāng)成模板來記憶,根據(jù)實(shí)際需求進(jìn)行修改即可//@Bean//public Realm myRealm(){// return new MyRealm();// }// 2、DefaultWebSecurityManager @Beanpublic DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") AuthorizingRealm myRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 通過HashedCredentialsMatcher 指定算法和加密次數(shù)HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("MD5");matcher.setHashIterations(2);myRealm.setCredentialsMatcher(matcher);//設(shè)置緩存,使用緩存可以避免需要授權(quán)信息時(shí)頻繁的調(diào)用數(shù)據(jù)庫查詢的問題。//原理很簡單,只要在SecurityManager里注入CacheManager即可。//我們也可以自定義CacheManager的實(shí)現(xiàn),可以是ehcache、redis等等。//實(shí)際開發(fā)中自定義RedisCache是很常見的,這里由于本人水平有限,就不演示如何用redis實(shí)現(xiàn)緩存了securityManager.setCacheManager(new MemoryConstrainedCacheManager());//最后要把我們的myRealm交給securityManager進(jìn)行管理securityManager.setRealm(myRealm);return securityManager;}//3、ShiroFilterFactoryBean@Beanpublic ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();bean.setSecurityManager(securityManager);/*配合Shiro的內(nèi)置過濾器,參見DefaultFilteranon : 無需認(rèn)證就可以訪問authc : 必須認(rèn)證才可以訪問 對應(yīng)@RequiresAuthentication注解user : 用戶登錄且記住我 才可以訪問 對應(yīng)@RequiresUser注解perms : 擁有某個(gè)資源才可以訪問 對應(yīng)@RequiresPermissions注解roles : 擁有某個(gè)角色才可以訪問 對應(yīng)@RequiresRoles注解*/Map<String, String> filterChainDefinition = new HashMap<>();//配置antMatcher,跟SpringSecurity一樣,可以配**,*,?// **:匹配路徑中的零個(gè)或多個(gè)路徑,如/admin/**將匹配/admin/a或/admin/a/b// *:匹配零個(gè)或多個(gè)字符串,如/admin將匹配/admin、/admin123,但不匹配/admin/1// ?:匹配一個(gè)字符,如/admin?將匹配/admin1,但不匹配/admin或/admin/filterChainDefinition.put("/mobile/**","perms[mobile]");filterChainDefinition.put("/salary/**","perms[salary]");filterChainDefinition.put("/main.html","authc");//配置登出過濾器filterChainDefinition.put("/logout","logout");//將配好的內(nèi)容添加到攔截器中bean.setFilterChainDefinitionMap(filterChainDefinition);//設(shè)置登錄頁bean.setLoginUrl("/index.html");//設(shè)置成功后跳轉(zhuǎn)的頁面bean.setSuccessUrl("/main.html");//設(shè)置沒有資源權(quán)限時(shí)跳轉(zhuǎn)到的頁面bean.setUnauthorizedUrl("/common/noauth");return bean;} }補(bǔ)充:Shiro中默認(rèn)的過濾器
| anon | AnonymousFilter | 指定url可以匿名訪問 | /admins/**=anon |
| authc | FormAuthenticationFilter | 指定url需要form表單登錄,默認(rèn)會(huì)從請求中獲取username、password,rememberMe等參數(shù)并嘗試登錄,如果登錄不了就會(huì)跳轉(zhuǎn)到loginUrl配置的路徑。我們也可以用這個(gè)過濾器做默認(rèn)的登錄邏輯,但是一般都是我們自己在控制器寫登錄邏輯的,自己寫的話出錯(cuò)返回的信息都可以定制嘛。 | /user/**=authc |
| authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登錄 | /user/**=authcBasic |
| logout | LogoutFilter | 登出過濾器,配置指定url就可以實(shí)現(xiàn)退出功能,非常方便 | filterChainDefinition.put(“/logout”,“l(fā)ogout”); |
| noSessionCreation | NoSessionCreationFilter | 禁止創(chuàng)建會(huì)話 | |
| perms | PermissionsAuthorizationFilter | 需要指定權(quán)限才能訪問 | /admin/**=perms[user:add:*],多個(gè)perms需要在[]中加“”,中間用,隔開 |
| port | PortFilter | 需要指定端口才能訪問 | /admins/**=port[8080] |
| rest | HttpMethodPermissionFilter | 將http請求方法轉(zhuǎn)化成相應(yīng)的動(dòng)詞來構(gòu)造一個(gè)權(quán)限字符串,這個(gè)感覺意義不大,有興趣自己看源碼的注釋 | /admin/user/**=perms[user.method],其中method為post,get,delete等 |
| roles | RolesAuthorizationFilter | 需要指定角色才能訪問 | /admins/**=roles[“admin,user”] |
| ssl | SslFilter | 需要https請求才能訪問 | |
| user | UserFilter | 需要已登錄或“記住我”的用戶才能訪問 |
LoginController、MobileController、SalaryController
import com.m33.shiroAuth.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map;@RequestMapping("/common") @RestController public class LoginController {private final Logger logger = LoggerFactory.getLogger(LoginController.class);@PostMapping("/login")public Object login(String username,String password){Map<String,String> errorCode = new HashMap<>();UsernamePasswordToken token = new UsernamePasswordToken(username,password);Subject user = SecurityUtils.getSubject();//如果用戶已認(rèn)證,直接打印already loginif(user.isAuthenticated()){return "already login";}else{try{//用戶未認(rèn)證,下面進(jìn)行認(rèn)證user.login(token);user.getSession().setAttribute("currentUser",user.getPrincipal());return "login succeed";} catch (UnknownAccountException uae) {//捕獲認(rèn)證過程中出現(xiàn)的各種異常logger.info("There is no user with username of " + token.getPrincipal());errorCode.put("errorMsg","不存在的用戶名");} catch (IncorrectCredentialsException ice) {logger.info("Password for account " + token.getPrincipal() + " was incorrect!");errorCode.put("errorMsg","密碼不正確");} catch (LockedAccountException lae) {logger.info("The account for username " + token.getPrincipal() + " is locked. " +"Please contact your administrator to unlock it.");errorCode.put("errorMsg","賬號被鎖定");} catch(AuthenticationException authe){logger.info("Authentication error ",authe);errorCode.put("errorMsg",authe.getMessage());}return errorCode;}}//獲取當(dāng)前用戶,用于前臺(tái)頁面展示用@RequestMapping("/getCurrentUser")public Object getCurrentUser(){Subject subject = SecurityUtils.getSubject();return subject.getSession().getAttribute("currentUser");}//未授權(quán)時(shí)執(zhí)行的方法@RequestMapping("/noauth")public String noAuth(){return "未經(jīng)授權(quán),無法訪問。";} } -------------------------------------------------------------- import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/mobile") public class MobileController {/*** 代表一個(gè)查手機(jī)號的后臺(tái)接口。* @return*/@GetMapping("/query")public String query(){return "mobile";} } ------------------------------------------------------------- import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/salary") public class SalaryController {/*** 代表一個(gè)查薪水的后臺(tái)接口。* @return*/@RequiresPermissions("salary")@GetMapping("/query")public String query(){return "salary";} }UserService
import com.m33.shiroAuth.bean.User; import com.m33.shiroAuth.utils.TestData; import org.apache.commons.beanutils.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors;@Service public class UserService {@Resourceprivate TestData testData;//這里的內(nèi)容看看就好,主要是為了模擬獲取數(shù)據(jù)庫中的用戶數(shù)據(jù),實(shí)際開發(fā)中肯定是直接從數(shù)據(jù)庫中獲得public User getUserByUserName(String username){List<User> queryUsers = testData.getAllUser().stream().filter(user -> username.equals(user.getUserName())).collect(Collectors.toList());if(null != queryUsers && queryUsers.size()>0){try {return (User)BeanUtils.cloneBean(queryUsers.get(0));} catch (Exception e) {e.printStackTrace();return null;}}else{return null;}} }TestData
import com.m33.shiroAuth.bean.User; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.List;@Component public class TestData {private List<User> allUser;/*** 模擬數(shù)據(jù)庫獲取到的數(shù)據(jù)。* admin用戶 擁有admin角色,擁有mobile和salary兩個(gè)資源。* mobile用戶,擁有mobile角色,擁有mobile資源。* worker用戶,擁有worker角色,沒有資源。* @return*/public List<User> getAllUser(){if(null == allUser){allUser = new ArrayList<User>();allUser.add(new User("admin","admin",Arrays.asList("admin"),Arrays.asList("mobile","salary")));allUser.add(new User("manager","manager",Arrays.asList("manager"),Arrays.asList("salary")));allUser.add(new User("worker","worker",Arrays.asList("worker"),Arrays.asList("worker")));}return allUser;} }MyPassWordEncoder
import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.util.ByteSource; /*** 負(fù)責(zé)密碼加鹽加密*/ public class MyPassWordEncoder {public static String getEncodedPassword(String password){//各參數(shù)含義分別為加密方式、密碼、鹽值(可隨意設(shè)置)、迭代次數(shù)(指反復(fù)加鹽加密的次數(shù))SimpleHash simpleHash = new SimpleHash("MD5",password, ByteSource.Util.bytes("salt"),2);return simpleHash.toString();} }以上便是全部類的代碼講解,前端代碼這里就不去研究了,下面補(bǔ)充一下認(rèn)證流程和授權(quán)流程
認(rèn)證流程如下:
- 1、首先調(diào)用Subject.login(token)進(jìn)行登錄,其會(huì)自動(dòng)委托給SecurityManager
- 2、SecurityManager負(fù)責(zé)真正的身份驗(yàn)證邏輯;它會(huì)委托給Authenticator進(jìn)行身份驗(yàn)證;
- 3、Authenticator才是真正的身份驗(yàn)證者,ShiroAPI中核心的身份認(rèn)證入口點(diǎn),此處可以自定義插入自己的實(shí)現(xiàn);
- 4、Authenticator可能會(huì)委托給相應(yīng)的AuthenticationStrategy進(jìn)行多Realm身份驗(yàn)證,默認(rèn)ModularRealmAuthenticator會(huì)調(diào)用AuthenticationStrategy進(jìn)行多Realm身份驗(yàn)證;
- 5、Authenticator會(huì)把相應(yīng)的token傳入Realm,從Realm獲取身份驗(yàn)證信息,如果沒有返回/拋出異常表示身份驗(yàn)證失敗了。此處可以配置多個(gè)Realm,將按照相應(yīng)的順序及策略進(jìn)行訪問。
授權(quán)流程如下:
- 1、首先調(diào)用Subject.isPermitted*/hasRole*接口,其會(huì)委托給SecurityManager,而SecurityManager接著會(huì)委托給Authorizer;
- 2、Authorizer是真正的授權(quán)者,如果調(diào)用如isPermitted(“user:view”),其首先會(huì)通過PermissionResolver把字符串轉(zhuǎn)換成相應(yīng)的Permission實(shí)例;
- 3、在進(jìn)行授權(quán)之前,其會(huì)調(diào)用相應(yīng)的Realm獲取Subject相應(yīng)的角色/權(quán)限用于匹配傳入的角色/權(quán)限;
- 4、Authorizer會(huì)判斷Realm的角色/權(quán)限是否和傳入的匹配,如果有多個(gè)Realm,會(huì)委托給ModularRealmAuthorizer進(jìn)行循環(huán)判斷,如果匹配如isPermitted*/hasRole*會(huì)返回true,否則返回false表示授權(quán)失敗。
5.補(bǔ)充多Realm認(rèn)證
當(dāng)我們需要通過Shiro實(shí)現(xiàn)“用戶名/手機(jī)號/郵箱”任意一個(gè)都可以登錄的時(shí)候,我們就需要用到多Realm認(rèn)證;我們要實(shí)現(xiàn)的效果就是通過用戶名+密碼或者手機(jī)號碼+密碼都可以實(shí)現(xiàn)登錄
修改User類,加一個(gè)mobile參數(shù),為了可以通過手機(jī)號登錄
import java.util.List;public class User {private String userName;private String userPass;private String mobile;//補(bǔ)充mobile屬性private List<String> userRoles;private List<String> userPerms;...... }修改TestData,添加手機(jī)號
public List<User> getAllUser(){if(null == allUser){allUser = new ArrayList<User>();allUser.add(new User("admin","admin","15777777777",Arrays.asList("admin"),Arrays.asList("mobile","salary")));allUser.add(new User("manager","manager","15888888888",Arrays.asList("manager"),Arrays.asList("salary")));allUser.add(new User("worker","worker","15999999999",Arrays.asList("worker"),Arrays.asList("worker")));}return allUser;}修改UserService類,添加getUserByMobile方法
public User getUserByMobile(String mobile){List<User> queryUsers = testData.getAllUser().stream().filter(user -> mobile.equals(user.getMobile())).collect(Collectors.toList());if(null != queryUsers && queryUsers.size()>0){return queryUsers.get(0);}else{return null;}}在config下添加MobileRealm,這就是上面那個(gè)代碼結(jié)構(gòu)圖劃掉的藍(lán)色部分
import com.m33.shiroAuth.bean.User; import com.m33.shiroAuth.service.UserService; import com.m33.shiroAuth.utils.MyPassWordEncoder; import org.apache.shiro.authc.*; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; //與MyRealm差不多,但是只需要認(rèn)證流程即可,因?yàn)槲覀冎皇菫榱擞檬謾C(jī)號登錄認(rèn)證 @Configuration(value = "mobileRealm") public class MobileRealm extends AuthenticatingRealm {private final Logger logger = LoggerFactory.getLogger(MobileRealm.class);@Resourceprivate UserService userService;//認(rèn)證@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {logger.info("---->entered method doGetAuthenticationInfo;");UsernamePasswordToken usertoken = (UsernamePasswordToken)authenticationToken;String username = usertoken.getUsername();//只需要管用戶名查詢邏輯。模擬獲取數(shù)據(jù)庫中用戶手機(jī)號User user = userService.getUserByMobile(username);if(null == user){return null; //后續(xù)會(huì)拋出UnknownAccountException}else{user.setUserPass(MyPassWordEncoder.getEncodedPassword(user.getUserPass()));SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getUserPass(),ByteSource.Util.bytes("salt"), "mobileRealm");return authenticationInfo;}} }最后,修改ShiroConfig的getDefaultWebSecurityManager方法
@Beanpublic DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") AuthorizingRealm myRealm, @Qualifier("mobileRealm") Realm mobileRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("MD5");matcher.setHashIterations(2);myRealm.setCredentialsMatcher(matcher);securityManager.setCacheManager(new MemoryConstrainedCacheManager());//創(chuàng)建多Realm認(rèn)證器,設(shè)置認(rèn)證策略,下面有對認(rèn)證策略進(jìn)行一個(gè)簡單的介紹ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());securityManager.setAuthenticator(authenticator);//securityManager.setRealm(myRealm);securityManager.setRealms(Arrays.asList(myRealm,mobileRealm));return securityManager;}補(bǔ)充:認(rèn)證策略說明,Shiro提供了3個(gè)具體的 AuthenticationStrategy 實(shí)現(xiàn)
[1] AtLeastOneSuccessfulStrategy(默認(rèn)實(shí)現(xiàn)):如果一個(gè)(或更多)驗(yàn)證成功,則整體的嘗試被認(rèn)為是成功的。如果沒有一個(gè)驗(yàn)證成功,則整體失敗。說白了就是,至少有一個(gè)Realm的驗(yàn)證是成功的算才認(rèn)證通過,否則認(rèn)證失敗。
[2] FirstSuccessfulStrategy:第一個(gè)Realm成功驗(yàn)證返回的信息將被使用,其他的Realm將被忽略。如果沒有一個(gè)Realm驗(yàn)證成功,則整體失敗。
[3] AllSuccessfulStrategy:所有配置的Realm都必須驗(yàn)證成功才算認(rèn)證通過,否則認(rèn)證失敗。
補(bǔ)充:Realm的繼承關(guān)系
一般繼承AuthorizingRealm(授權(quán))即可;從圖中我們可以看到它繼承了AuthenticatingRealm(即身份驗(yàn)證),而且也間接繼承了CachingRealm(帶有緩存實(shí)現(xiàn))。
總結(jié)
以上是生活随笔為你收集整理的Shiro从入门到实战(整合进SpringBoot)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是L2十档行情API接口
- 下一篇: 通达信l2行情接口怎么用?