一看就会!一篇全搞定!权限处理专家--Shiro保姆式教学,超详细!
輕量級權限處理框架--Shiro
- 前言
- Shiro三大對象
- Subject
- SecurityManager
- Realm
- Authentication和Authorization
- Authentication
- Authorization
- Authentication和Authorization小結
- 權限控制的三個基本要素
- 用戶
- 角色
- 權限
- 權限表達式是如何工作的呢?
- Shiro單機示例
- 解讀ini配置文件
- 單機程序代碼
- 運行結果
- 程序執行過程
- 程序執行過程詳解
- 我們沒有配置Realm,憑什么知道賬號密碼能登錄?
- **login過程**
- 權限驗證過程
- 登錄流程總結
- 鑒權流程總結
- 總結
前言
Shiro是一個輕量級的權限處理框架,提供了身份驗證,授權,加密,會話管理的功能,Shiro可以適合任何程序,從大型的Web應用到小型的命令行程序都可以使用,從整合數據庫驗證信息到硬編碼用戶信息到.ini文件一并支持。
Shiro三大對象
Shiro框架里有三個核心元素,分別是Subject(主體),SecurityManager(安全管理者),Realm(域),這三個核心元素構成了Shiro的核心功能,下面我們將逐個講解。
Subject
在Shiro中,和程序產生交互行為的對象叫做Subject,比如說一個用戶要登錄一個網頁,那么這個用戶就是一個Subject,但是我們知道,和程序交互的并不一定是個“人”,頁面可以通過爬蟲訪問,也可以模擬瀏覽器行為進行訪問,所以在Shiro框架中,并不把產生交互行為的所有對象都稱作“人”,而是稱作一個主體–Subject,一切和用戶有關的行為都是通過Subject來控制的。
SecurityManager
SecurityManager就是安全管理員,就好像一個攔路的強盜,可以在這個管理員對象里對請求進行攔截,設置允許的請求路徑,設置相應的攔截路徑,比如你要走上一座山,有好幾條路可以走,其中有幾條路是有攔路強盜的,但是如果你遇到強盜說對了暗號,那么強盜也可以放你過去(權限驗證通過),但是如果你沒有說對暗號,那么強盜就會把你攔下來(權限驗證失敗),或者帶你去山寨里(重定向)
Realm
Realm是一個域,這個域的作用就是驗證與授權,就好像你要拿著令牌進城門,那個守城的保安隊長就要看令牌上面畫的畫像、寫的名字是不是你,和你對不對得上,再如果你要進紫禁城去面見圣上,還得檢查你有沒有資格進紫禁城,這個域的作用其實就好像是一個令牌池,負責記錄哪塊令牌是誰,誰可以進城,負責校驗令牌,查看令牌權限,也就是我們后面會提到的Authentication(驗證)與Authorization(授權)。
這個Realm在Shiro框架里類似一個非常安全的操作的數據庫,從數據庫里拿數據,檢查,驗證,授權。
Authentication和Authorization
Authentication
Authentication(驗證),即檢測用戶是不是該用戶,查查看你是不是假冒別人的身份,偷偷拿了別人的令牌,這一步驗證的作用就是證明自己確實是自己而不是別人
Authorization
Authorization(授權),即檢測用戶是不是有權限,查查你是不是夠資格干這個事情,比如老師說班長可以放學早點走,你也向早點走,老師就會問你:你是班長嗎?如果按照Shiro框架的做法,老師還會問一句:你是XXX嗎?你是班長嗎?
你夠資格嗎?也許不夠。
-----知名哲學家 凱隱和拉亞斯特
Authentication和Authorization小結
驗證和授權其實每天都發生在我們身邊,比如以前坐動車得提供動車票和身份證,身份證證明你是本人,動車票證明你有資格坐動車,身份證驗證,動車票授權,不能把驗證和授權混為一談。
在Shiro框架的Realm里會有兩個方法,一個是doGetAuthentication,一個是doGetAuthorization,前者進行驗證身份操作,后者進行授權操作,都是我們實現域對象需要重寫的方法。
權限控制的三個基本要素
用戶
用戶就是確定一個用戶是誰,比如說確定小文是小文
角色
角色代表一種權限的集合,比如說一個圖書館管理員,他可以增加書籍,刪除書籍,甚至可以優先借走書籍,這都是圖書館管理員的權限,一個管理員代表著一堆權利的集合體,我們在程序中可以能會遇到非常多操作權限的地方,如果一個人既要操作圖書,又要操作用戶,每次都判斷對應的權限實在是太過麻煩,我們不妨抽象出一個角色,每次都判斷這個操作的用戶是不是這個角色,如果是就放行,如果不是就拒絕,這樣后期添加權限,刪除權限都會比較輕松,只需要修改角色對應的權限即可。
權限
這是一個最細粒度的權限管理范圍,比如說有這樣一個權限管理表達式 “user:query:01”,表達的是可以對User類的01實例進行query操作,再比如說,"user:*"這個表達式代表著可以對User類的所有實例進行任意操作,很神奇吧!上面看到的兩個表達式就是Shiro框架中使用的權限表達式,三個粒度的權限范圍用兩個 “:” 分開。
權限表達式是如何工作的呢?
權限表達式并不是直接對數據庫進行控制,也就是說他并不能阻止你去操作數據庫,說到底他只是一個字符串,他并不知道哪個對象是屬于User類,哪個對象是User類的01實例,哪個操作是query,他只知道自己代表著一種權力就是 這個用戶可以查詢User類的01實例信息,它僅僅代表著一種權力,想要讓權力生效,必須在操作之前加上權力的判斷,也就是鑒權,舉一個簡單的例子:一個用戶想要操作數據庫修改管理員信息,在他發起請求之后,被SecurityManager給攔截了,SecurityManager會對他進行身份驗證,看看你到底是不是一個用戶,發現你是,那沒問題,繼續進行權限判斷,從數據庫或者ini文件中讀取一個代表你這個角色的權限表達式,然后跟你要進行操作的權限表達式進行匹配,如果匹配上了,恭喜你,你可以修改管理員信息了,如果匹配不上,那么你的請求就會被攔截。
綜上所述,權限表達式并不能直接去控制你能不能操作對象或者數據庫,但是可以通過權限表達式的匹配來判斷你有沒有這個權力,決定你的請求可不可以生效,再說的通俗一點,權限表達式僅僅是一個代表權限表達式!
Shiro單機示例
Shiro可以運行在任何的程序中,為了先帶領大家稍微領略一下Shiro的魅力,我選擇了單機運行程序,硬編碼配置文件的方法使用Shiro
解讀ini配置文件
Shiro的ini配置文件比較簡單,寫起來很輕松,對于Shiro來說ini文件分為幾個空間,main空間,user空間,roles空間,三個不同的空間有什么用呢?
main空間一般用作全局的配置文件,比如說shiro緩存設置等等
users空間就是我們一般俗稱的賬號密碼,看到上圖里的
climbingxiaowen即為賬號,nihao即為密碼,中間用等號相連接,后面跟著的admin代表著該用戶有哪些角色
roles空間就是一個角色對應著的權限,圖中表示admin角色,可以有兩種權限,user:*代表著可以對user類進行任意操作,agent:create代表可以對管理員進行創建操作。
單機程序代碼
public class SingleShiroTest {public static void main(String[] args) {//通過Shiro提供的SecurityManager工廠類讀取配置文件創建實例Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();//設置一個securityManagerSecurityUtils.setSecurityManager(securityManager);//獲取當前需要操作程序的一個對象Subject subject = SecurityUtils.getSubject();String username = "climbingxiaowen";String password = "nihao";//生成令牌,即UsernamePasswordTokenUsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);//對用戶登錄subject.login(usernamePasswordToken);//對用戶進行權限校驗subject.checkPermission("user:delete");subject.checkPermission("agent:delete");} }運行結果
程序順利運行到了最后一行,檢驗是否有agent:delete權限,我們看到ini文件里是沒有這個權限的,符合預期
程序執行過程
程序執行過程詳解
我們沒有配置Realm,憑什么知道賬號密碼能登錄?
Realm的配置隱含在工廠類中,在工廠類中調用getInstance()的時候,會自動讀取.ini配置文件,創建一個Realm,并把Realm放到SecurityManager中去,具體代碼如下:
private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) {getReflectionBuilder().setObjects(createDefaults(ini, mainSection));Map<String, ?> objects = buildInstances(mainSection);SecurityManager securityManager = getSecurityManagerBean();boolean autoApplyRealms = isAutoApplyRealms(securityManager);if (autoApplyRealms) {//realms and realm factory might have been created - pull them out first so we can//initialize the securityManager:Collection<Realm> realms = getRealms(objects);//set them on the SecurityManagerif (!CollectionUtils.isEmpty(realms)) {applyRealmsToSecurityManager(realms, securityManager);}}return securityManager;}把傳進來的username和password存儲到新生成的token中,非常簡單
login過程
重要!重要!重要!重要!重要!
將token傳入login函數,調用順序:
Subject subject = securityManager.login(this, token);將token傳給DefaultSecurityManager.login
AuthenticationInfo info = uthenticate(token);DefaultSecurityManager將token傳給自己的AuthenticatingSecurityManager.authenticate()
return this.authenticator.authenticate(token);AuthenticatingSecurityManager調用自己存儲的authenticator進行驗證
可以在這副圖中看到很熟悉的兩個身影,驗證器和授權器下面都有一個realms,而這個realms就存儲著我們讀取配置文件生成的iniRealm
發現端倪了嗎?其實iniRealm存儲用戶和角色和權限的方式,使用HashMap做一個映射,讓用戶映射到角色,讓角色映射到權限。
authenticator是如何驗證身份的呢?
先進入info = doAuthenticate(token);,判斷有幾個realm需要驗證,
如果只有一個realm需要驗證就進入doSingleRealmAuthentication。
進入之后會判斷,該realm是不是支持驗證該token,每個realm有對應的支持token類型,token的類型有兩種:
1、 UsernamePasswordToken
2、 BearerToken
兩種Token的區別在于:
前者保存的是一對用戶名和密碼,后者保存的是一段token字符串,前者一般用于登錄驗證,后者一般是在發起Http請求帶來的,比如后面整合前后端分離權限驗證會用到的JWT token,不過我們在使用的時候也可以自己去實現Token類,但是需要記得在Realm里重寫support方法來支持自己的Realm對Token進行驗證!
判斷完支持類型之后會調用AuthenticationInfo info = realm.getAuthenticationInfo(token);,進入該函數會首先從Realm緩存中讀取是不是已經有緩存過這個token的信息,如果沒有就進入關鍵的info = doGetAuthenticationInfo(token);因為iniRealm繼承的是SimpleAccountRealm,所以調用的是SimpleAccountRealm的驗證函數,我們在實現自己的Realm的時候需要重寫這個doGetAuthenticationInfo方法,來支持自己的token判斷,比如會在該函數從,調用Dao層方法,獲取數據庫存儲的用戶名和密碼。
在SimpleAcountRealm中,驗證權限的過程很簡單,我們前面提到過Realm里保存了一張HashMap映射用來存儲用戶-角色,角色-權限的對應關系,在這里驗證權限的方法就是從HashMap中根據用戶名查找,如果能拿到對應的SimpleAccont對象就返回該對象。
返回的是一個AuthenticationInfo類型,也就是驗證信息對象,拿到驗證信息對象之后要把對象和token進行比對,上面拿到Info的過程就好像是一個人來到一家理發店說自己是這里的300號會員,店主看了下系統,發現確實有300號會員,然后店主說報下你的手機號,這個時候就是要進入檢測credentials的過程。
進入assertCredentialsMatch(token, info);進行憑證檢測,從token和info中獲取Credentials,轉換成Byte[]數組進入return MessageDigest.isEqual(tokenBytes, accountBytes);,進行比對,比對的過程很有意思,先上源碼:
我們發現這是個時間恒定的比較,跟我們一般比較不同,如果讓我來寫可能會寫成下面這個:
for(int i=0;i<digesta.length();i++){if(digesta[i]!=digestb[i]){return false;} } return true;按照有源碼方式寫的比對好處在于不怕時間檢測攻擊,比如說一個人一直用不同的密碼來校驗,發現有的密碼校驗時間很短,一下子就失敗了,有的密碼校驗時間比較長,說明校驗到比較后面的位置。按照源碼方式的比對,不管怎么校驗都是需要校驗完畢最后返回結果,避免了時間檢測攻擊。
如果比對成功,就返回一個authenticationInfo,期間沒有拋出異常說明都成功,登錄也就成功了!
權限驗證過程
重要!重要!重要!重要!重要!
權限驗證的過程大體上也和登錄差不多,關鍵的對象是一個AuthorizationInfo,先調用this.authorizer.checkPermission(principals, permission);,進入驗證權限函數,檢查是否有Realm支持對該權限的驗證,在單機程序中,系統默認配置的AuthorizingRealm顯然是支持的,這里是層層嵌套調用,不去細說,說點關鍵部分:
1、把Permission表達式解析成Permission對象
一般我們使用的都是WildcardPermission對象,這個對象里保存了一個List<Set<String>> parts;屬性用來存儲權限表達式的三個部分,是通過new WildcardPermission的時候調用setParts()方法來生成,將字符串分割添加到parts中去,源碼如下:
創建好Permission之后,會進入我們非常熟悉的方法info = doGetAuthorizationInfo(principals);,這一步還是通過存儲在Hashmap中的kv對來獲取username對應的SimpleAccount信息,該對象存儲了account的驗證信息和權限信息。
拿到AuthorizationInfo信息之后,開始權限比對:
比對過程就是把傳入的權限表達式也分解為parts,然后從AuthorizationInfo中拿到parts,一個個進行比對,如果有*通配符則跳過該part的比對。
至此權限驗證結束。
登錄流程總結
鑒權流程總結
總結
Shiro框架使用起來非常簡單,源碼閱讀難度也不大,是個簡單易用的框架,整體核心部分就是弄清楚三大對象Subject,SecurityManager,Realm,以及驗證和鑒權的關鍵過程,這對于以后我們自定義權限管理信息有很大幫助,可以在SecurityManager中添加攔截API的路徑,在Realm里和數據庫打通實現密碼校驗、權限檢查等操作,下次會出一篇SpringBoot+Shiro+JWT Token+Mybatis的集成前后端分離Demo教學。
總結
以上是生活随笔為你收集整理的一看就会!一篇全搞定!权限处理专家--Shiro保姆式教学,超详细!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 20-Netty TCP 粘包和拆包及解
- 下一篇: SpringBoot项目配置SSL证书微