前言 開心一刻 小明的朋友骨折了,小明去他家裡看他。他老婆很細心的為他換藥,敷藥,然後出去買菜。小明滿臉羡慕地說:你特麽真幸福啊,你老婆對你那麼好!朋友哭得稀里嘩啦的說:兄弟你別說了,我幸福個錘子,就是她把我打骨折的。 揣摩下此刻男人的內心 路漫漫其修遠兮,吾將上下而求索! github:https ...
前言
開心一刻
小明的朋友骨折了,小明去他家裡看他。他老婆很細心的為他換藥,敷藥,然後出去買菜。小明滿臉羡慕地說:你特麽真幸福啊,你老婆對你那麼好!朋友哭得稀里嘩啦的說:兄弟你別說了,我幸福個錘子,就是她把我打骨折的。
揣摩下此刻男人的內心
路漫漫其修遠兮,吾將上下而求索!
github:https://github.com/youzhibing
碼雲(gitee):https://gitee.com/youzhibing
前情回顧
上篇中主要講了兩點認證與授權,認證主要FormAuthenticationFilter和AnonymousFilter兩個filter來控制,shiro對所有請求都會先生成ProxiedFilterChain,請求會經過ProxiedFilterChain,先執行shiro的filter鏈,再執行剩下的servlet Filter鏈,最後來到我們的Controller。
認證過程是通過filter控制實現的,我們所有的請求由shiro中3個Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分攤了,LogoutFilter負責/logout,AnonymousFilter負責/login和靜態資源,FormAuthenticationFilter則負責剩下的(/**),三個filter只會有一個生效(註意filter的配置順序);當FormAuthenticationFilter生效的時候會進行登錄認證,認證過程:先從緩存獲取authenticationInfo,沒有則通過realm從資料庫獲取並放入緩存,然後將頁面輸入的用戶信息(UsernamePasswordToken)與authenticationInfo進行匹配驗證,認證通過會將subject中的authenticated設置成true,表示當前subject已經被認證過了。關於認證緩存,個人不建議開啟,因為當修改用戶信息後,不好處理緩存中的authenticationInfo,另外認證頻率本來就不高,緩存的意義不大。
一般情況下授權是通過註解方式實現的,註解配合aop會在我們的業務方法前織入前置許可權檢查處理,檢查過程與認證過程類似:從緩存中獲取authorizationInfo,沒有則通過realm從資料庫獲取,然後放入緩存,然後將authorizationInfo與@RequiresPermissions("xxx")中的xxx來進行匹配,完成許可權檢查,檢查通過則進入我們的目標方法,不通過則拋出異常。關於許可權緩存,個人建議開啟,因為許可權的驗證還是挺頻繁的,如果不開啟緩存,那麼會給資料庫造成一定的壓力。
遺留問題解答
上篇遺留問題:session過期後,我們再請求,shiro是如何處理並跳轉到登錄頁的?回答這個問題之前,我們先看看另外一個問題:
上篇博文中講到了登錄認證成功後會將subject的authenticated設置成true,表示當前subject已經被認證過了,但是只是當前subject; 我們可以將subject看成是request,每次請求來的時候都會將request/response對封裝成subject,AbstractShiroFilter的方法doFilterInternal中有這樣一個調用 final Subject subject = createSubject(request, response); 我們來看看createSubject的方法描述: Creates a WebSubject instance to associate with the incoming request/response pair which will be used throughout the request/response execution. 創建一個關聯request/response對的WebSubject實例,用於後續request/response的執行
也就是目前我們還只是看到了當前請求有認證狀態,當前會話還沒有看到認證狀態;撇開shiro,如果是我們自己實現,我們會怎麼實現,肯定會在subject的authenticated設置成true的時候也將認證狀態也設置在session中,至於是存儲在自定義session的某個標誌欄位(類似subject的authenticated)中,還是存儲在session的attributes中(setAttribute(Object key, Object value)進行設置),看我們的需求和個人喜好。
歸納下這個問題:shiro是如何保存當前會話認證狀態的,是上述中的某種實現方式,還是shiro有另外的實現方式
shiro是如何保存會話認證狀態的
每次請求都會生成新的subject,如果我們把認證狀態只放到subject中,那麼每次請求都需要進行認證,這顯然是不合理的,我們需要將認證狀態保存到會話(session)中,那麼整個會話期間只需要認證一次即可。那麼shiro是怎麼實現的了,我們來看看源碼,從我們Controller的doLogin方法開始
如果我們繼續跟進s.setAttribute(attributeKey, value),會發現認證狀態最終存放到了SimpleSession的attributes屬性中,
private transient Map<Object, Object> attributes;
也就是說認證狀態會存在session的attributes中,正是我們上面說的方式之一,shiro沒有用它特有的方式,最終在session中的存在形式如下圖
看過上篇博客的朋友應該會有印象:FormAuthenticationFilter的isAccessAllowed方法(從AuthenticatingFilter繼承)中第一個認證的是subject的authenticated
為什麼獲取subject的authenticated,而不是直接獲取session的認證狀態,我還沒弄清楚為什麼,難道是為了組件的分工明確? 既然shiro這麼做了肯定有它的道理,我們先不糾結這個(知道的朋友可以評論區提示下)。我們知道登錄成功後,subject的authenticated會被賦值成true,但是登錄成功後的其他請求,比如:http://localhost:8080/own/index,subject的authenticated是什麼時候在哪被賦值成true的呢?我們已經知道session中有認證狀態,那麼肯定是某個時候在某個地方將session中的認證狀態賦值給了subject,具體是怎麼樣我們來跟一下源碼,還記得shiro的入口filter:SpringShiroFilter,我們從SpringShiroFilter的doFilterInternal(從AbstractShiroFilter繼承)方法開始
可以看到,在創建subject的時候,會將session中的認證狀態賦值給subject的authenticated。
小結下:登錄時,登錄成功會將認證狀態(成功)存儲到session的attributes中,之後的每一次請求,在創建subject的時候,都會將session中的認證狀態賦值給subject的authenticated,那麼FormAuthenticationFilter在認證的時候會直接返回true,繼續走servlet filter鏈,最終來到我們的Controller。
至此,該問題就明瞭了,會話認證狀態還是保存在session中,只是中間處理的時候會將session中的認證狀態賦值給subject,由subject傳遞給FormAuthenticationFilter認證狀態。
session過期後,我們再請求,shiro是如何處理並跳轉到登錄頁的
如果我們明白了上個問題,那麼這個問題就很好理解了。如果session過期,那麼通過sessionDAO獲取的session為null,subject的authenticated就會被賦值成false,那麼在FormAuthenticationFilter中認證不通過,則會重定向到/login,讓用戶重新進行登錄認證。事實是這樣嗎,我們來跟下源碼(假設此時請求是:http://localhost:8080/own/index)
可以看到,請求進過SpringShiroFilter時,subject中authenticated被設置成false,然後生成ProxiedFilterChain
請求會來到FormAuthenticationFilter,認證不通過,重定向到/login,並返回false,表示filter鏈不用繼續往下走了(具體可查看上篇博文)。
強調下:很多對session的操作,都會同步到緩存(或持久層),包括session刷新(touch())、設置session屬性(setAttribute()等等,具體可以看AbstractNativeSessionManager,很多session操作中都會調用onChange方法
protected void onChange(Session session) { sessionDAO.update(session); }
shiro源碼系列
shiro源碼篇 - shiro的session創建,你值得擁有
SessionManager負責session的操作,包括創建、維護、刪除、失效、驗證等;
AbstractNativeSessionManager的start是創建session的入口;
SimpleSession是shiro完完全全的自己實現,是shiro對session的一種拓展。但SimpleSession不對外暴露,我們一般操作的是SimpleSession的代理:DelegatingSession,或者是DelegatingSession的代理:StoppingAwareProxiedSession;對session的操作,會通過一層層代理,來到DelegatingSession,DelegatingSession將session的操作轉交給sessionMananger,sessionManager通過一些校驗後,最後轉交給SimpleSession處理。
shiro源碼篇 - shiro的session的查詢、刷新、過期與刪除,你值得擁有
一般操作的session是session的代理,代理將session操作委托給sessionManager,sesionManager校驗之後再轉交給SimpleSession;
session過期定時任務預設60分鐘執行一次,所session已過期或不合法,則拋出對應的異常,上層通過捕獲異常從sessionDAO中刪除session;
不只定時任務做session的校驗,session的基本操作都在sessionManager中有做session的校驗,例如touch、setAttribute等,具體可以查看AbstractNativeSessionManager,對session的操作都是通過AbstractNativeSessionManager處理後轉交給SimpleSession。
shiro源碼篇 - shiro的session共用,你值得擁有
session共用實現的原理其實都是一樣的,都是filter + HttpServletRequestWrapper,只是實現細節會有所區別;
shiro的session共用其實是比較簡單的,重寫CacheManager,將其操作指向我們的redis,然後定製CachingSessionDAO實現session緩存操作和session持久化;
如果session需要持久化,推薦自定義sessionDAO繼承EnterpriseCacheSessionDAO,如果只是緩存,則推薦自定義sessionDAO繼承CachingSessionDAO。
shiro源碼篇 - shiro的filter,你值得擁有
SpringShiroFilter註冊到spring容器,會被包裝成FilterRegistrationBean,通過FilterRegistrationBean註冊到servlet容器;SpringShiroFilter相當於是整個shiro的入口;
SpringShiroFilter會創建ProxiedFilterChain,代理servlet FilterChain,讓請求先走shiro的filter鏈,讓後再走servlet FilterChain。
shiro源碼篇 - shiro認證與授權,你值得擁有
認證通過Filter實現,anon表示匿名訪問,不需要認證,一般就是針對游客可以訪問的資源,而authc則表示需要登錄認證;
我們所有的請求一般由shiro中3個Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分攤了,LogoutFilter負責/logout,AnonymousFilter負責/login和靜態資源,FormAuthenticationFilter則負責剩下的(/**);
認證由FormAuthenticationFilter實現,未登錄的請求會由它重定向到/login;認證過程是將界面輸入的信息(UsernamePasswordToken)與緩存(或資料庫)中的authenticationInfo進行匹對驗證;認證信息不建議緩存;
授權由註解方式,配合aop實現目標方法前的增強織入;認證過程是將緩存(或資料庫)中的authorizationInfo與@RequiresPermissions("xxx")中的xxx進行匹配校驗;認證信息建議緩存起來。
參考
《跟我學shiro》