一直以來我都想寫一寫Spring Security系列的文章,但是整個(gè)Spring Security體系強(qiáng)大卻又繁雜。陸陸續(xù)續(xù)從最開始的guides接觸它义屏,項(xiàng)目中看了一些源碼,到最近這個(gè)月為了寫一寫這個(gè)系列的文章,閱讀了好幾遍文檔犁河,最終打算嘗試一下,寫一個(gè)較為完整的系列文章魄梯。
較為簡單或者體量較小的技術(shù)桨螺,完全可以參考著demo直接上手,但系統(tǒng)的學(xué)習(xí)一門技術(shù)則不然酿秸。以我的認(rèn)知灭翔,一般的文檔大致有兩種風(fēng)格:Architecture First和Code First。前者致力于讓讀者先了解整體的架構(gòu)辣苏,方便讓我們有一個(gè)宏觀的把控肝箱,而后者以特定的demo配合講解,可以讓讀者在解決問題的過程中順便掌握一門技術(shù)稀蟋。關(guān)注過我博客或者公眾號的朋友會發(fā)現(xiàn)煌张,我之前介紹技術(shù)的文章,大多數(shù)是Code First退客,提出一個(gè)需求骏融,介紹一個(gè)思路链嘀,解決一個(gè)問題,分析一下源碼档玻,大多如此怀泊。而學(xué)習(xí)一個(gè)體系的技術(shù),我推薦Architecture First误趴,正如本文標(biāo)題所言霹琼,這篇文章是我Spring Security系列的第一篇,主要是根據(jù)Spring Security文檔以及源碼凉当,選擇性地整理而成的一個(gè)架構(gòu)概覽枣申,配合自己的一些講解方便大家理解。寫作本系列文章時(shí)纤怒,參考版本為Spring Security 4.2.3.RELEASE糯而。
接下來介紹核心組件,這一節(jié)主要介紹一些在Spring Security中常見且核心的Java類泊窘,它們之間的依賴熄驼,構(gòu)建起了整個(gè)框架。想要理解整個(gè)架構(gòu)烘豹,最起碼得對這些類眼熟瓜贾。
1.1 SecurityContextHolder
SecurityContextHolder用于存儲安全上下文(security context)的信息。當(dāng)前操作的用戶是誰携悯,該用戶是否已經(jīng)被認(rèn)證祭芦,他擁有哪些角色權(quán)限...這些都被保存在SecurityContextHolder中。?SecurityContextHolder默認(rèn)使用?ThreadLocal?策略來存儲認(rèn)證信息憔鬼」昃ⅲ看到?ThreadLocal?也就意味著,這是一種與線程綁定的策略轴或。Spring Security在用戶登錄時(shí)自動綁定認(rèn)證信息到當(dāng)前線程昌跌,在用戶退出時(shí),自動清除當(dāng)前線程的認(rèn)證信息照雁。但這一切的前提蚕愤,是你在web場景下使用Spring Security,而如果是Swing界面饺蚊,Spring也提供了支持,?SecurityContextHolder的策略則需要被替換污呼,鑒于我的初衷是基于web來介紹Spring Security裕坊,所以這里以及后續(xù),非web的相關(guān)的內(nèi)容都一筆帶過悟狱。
獲取當(dāng)前用戶的信息
因?yàn)樯矸菪畔⑹桥c線程綁定的苹享,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶信息得问。一個(gè)典型的獲取當(dāng)前登錄用戶的姓名的例子如下所示:
getAuthentication()返回了認(rèn)證信息膏萧,再次getPrincipal()返回了身份信息,UserDetails便是Spring對身份信息封裝的一個(gè)接口。Authentication和UserDetails的介紹在下面的小節(jié)具體講解椒楣,本節(jié)重要的內(nèi)容是介紹SecurityContextHolder這個(gè)容器。
1.2 Authentication
先看看這個(gè)接口的源碼長什么樣:
<1> Authentication是spring security包中的接口凤壁,直接繼承自Principal類吩屹,而Principal是位于java.security包中的∨《叮可以見得煤搜,Authentication在spring security中是最高級別的身份/認(rèn)證的抽象。
<2> 由這個(gè)頂級接口唧席,我們可以得到用戶擁有的權(quán)限信息列表擦盾,密碼嘲驾,用戶細(xì)節(jié)信息,用戶身份信息迹卢,認(rèn)證信息辽故。
上面1.1第一個(gè)代碼例子中,authentication.getPrincipal()返回了一個(gè)Object腐碱,我們將Principal強(qiáng)轉(zhuǎn)成了Spring Security中最常用的UserDetails誊垢,這在Spring Security中非常常見,接口返回Object症见,使用instanceof判斷類型喂走,強(qiáng)轉(zhuǎn)成對應(yīng)的具體實(shí)現(xiàn)類。接口詳細(xì)解讀如下:
getAuthorities()谋作,權(quán)限信息列表芋肠,默認(rèn)是GrantedAuthority接口的一些實(shí)現(xiàn)類,通常是代表權(quán)限信息的一系列字符串遵蚜。
getCredentials()帖池,密碼信息,用戶輸入的密碼字符串谬晕,在認(rèn)證過后通常會被移除碘裕,用于保障安全。
getDetails()攒钳,細(xì)節(jié)信息帮孔,web應(yīng)用中的實(shí)現(xiàn)接口通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值不撑。
getPrincipal()文兢,敲黑板!;烂省姆坚!最重要的身份信息,大部分情況下返回的是UserDetails接口的實(shí)現(xiàn)類实愚,也是框架中的常用接口之一兼呵。UserDetails接口將會在下面的小節(jié)重點(diǎn)介紹。
Spring Security是如何完成身份認(rèn)證的腊敲?
1.? 用戶名和密碼被過濾器獲取到击喂,封裝成Authentication,通常情況下是UsernamePasswordAuthenticationToken這個(gè)實(shí)現(xiàn)類。
2.? AuthenticationManager身份管理器負(fù)責(zé)驗(yàn)證這個(gè)Authentication
3.? 認(rèn)證成功后碰辅,AuthenticationManager身份管理器返回一個(gè)被填充滿了信息的(包括上面提到的權(quán)限信息懂昂,身份信息,細(xì)節(jié)信息没宾,但密碼通常會被移除)Authentication實(shí)例凌彬。
4.? SecurityContextHolder安全上下文容器將第3步填充了信息的Authentication沸柔,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設(shè)置到其中铲敛。
這是一個(gè)抽象的認(rèn)證流程褐澎,而整個(gè)過程中,如果不糾結(jié)于細(xì)節(jié)原探,其實(shí)只剩下一個(gè)?AuthenticationManager?是我們沒有接觸過的了乱凿,這個(gè)身份管理器我們在后面的小節(jié)介紹。將上述的流程轉(zhuǎn)換成代碼咽弦,便是如下的流程:
注意:上述這段代碼只是為了讓大家了解Spring Security的工作流程而寫的,不是什么源碼胁出。在實(shí)際使用中型型,整個(gè)流程會變得更加的復(fù)雜,但是基本思想全蝶,和上述代碼如出一轍闹蒜。
1.3 AuthenticationManager
初次接觸Spring Security的朋友相信會被AuthenticationManager,ProviderManager抑淫,AuthenticationProvider...這么多相似的Spring認(rèn)證類搞得暈頭轉(zhuǎn)向绷落,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計(jì)者的用意。
AuthenticationManager(接口)是認(rèn)證相關(guān)的核心接口始苇,也是發(fā)起認(rèn)證的出發(fā)點(diǎn)砌烁,因?yàn)樵趯?shí)際需求中,我們可能會允許用戶使用用戶名+密碼登錄催式,同時(shí)允許用戶使用郵箱+密碼函喉,手機(jī)號碼+密碼登錄,甚至荣月,可能允許用戶使用指紋登錄(還有這樣的操作管呵?沒想到吧),所以說AuthenticationManager一般不直接認(rèn)證哺窄,AuthenticationManager接口的常用實(shí)現(xiàn)類ProviderManager內(nèi)部會維護(hù)一個(gè)List<AuthenticationProvider>列表捐下,存放多種認(rèn)證方式,實(shí)際上這是委托者模式的應(yīng)用(Delegate)萌业。也就是說坷襟,核心的認(rèn)證入口始終只有一個(gè):AuthenticationManager,不同的認(rèn)證方式:用戶名+密碼(UsernamePasswordAuthenticationToken)咽白,郵箱+密碼啤握,手機(jī)號碼+密碼登錄則對應(yīng)了三個(gè)AuthenticationProvider。這樣一來四不四就好理解多了晶框?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm排抬。在默認(rèn)策略下懂从,只需要通過一個(gè)AuthenticationProvider的認(rèn)證,即可被認(rèn)為是登錄成功蹲蒲。
只保留了關(guān)鍵認(rèn)證部分的ProviderManager源碼:
ProviderManager中的List番甩,會依照次序去認(rèn)證,認(rèn)證成功則立即返回届搁,若認(rèn)證失敗則返回null缘薛,下一個(gè)AuthenticationProvider會繼續(xù)嘗試認(rèn)證,如果所有認(rèn)證器都無法認(rèn)證成功卡睦,則ProviderManager會拋出一個(gè)ProviderNotFoundException異常宴胧。
到這里,如果不糾結(jié)于AuthenticationProvider的實(shí)現(xiàn)細(xì)節(jié)以及安全相關(guān)的過濾器表锻,認(rèn)證相關(guān)的核心類其實(shí)都已經(jīng)介紹完畢了:身份信息的存放容器SecurityContextHolder恕齐,身份信息的抽象Authentication,身份認(rèn)證器AuthenticationManager及其認(rèn)證流程瞬逊。姑且在這里做一個(gè)分隔線显歧。下面來介紹下AuthenticationProvider接口的具體實(shí)現(xiàn)。
1.4 DaoAuthenticationProvider
AuthenticationProvider最最最常用的一個(gè)實(shí)現(xiàn)便是DaoAuthenticationProvider确镊。顧名思義士骤,Dao正是數(shù)據(jù)訪問層的縮寫,也暗示了這個(gè)身份認(rèn)證器的實(shí)現(xiàn)思路蕾域。由于本文是一個(gè)Overview拷肌,姑且只給出其UML類圖:
按照我們最直觀的思路,怎么去認(rèn)證一個(gè)用戶呢束铭?用戶前臺提交了用戶名和密碼廓块,而數(shù)據(jù)庫中保存了用戶名和密碼,認(rèn)證便是負(fù)責(zé)比對同一個(gè)用戶名契沫,提交的密碼和保存的密碼是否相同便是了带猴。在Spring Security中。提交的用戶名和密碼懈万,被封裝成了UsernamePasswordAuthenticationToken拴清,而根據(jù)用戶名加載用戶的任務(wù)則是交給了UserDetailsService,在DaoAuthenticationProvider中会通,對應(yīng)的方法便是retrieveUser口予,雖然有兩個(gè)參數(shù),但是retrieveUser只有第一個(gè)參數(shù)起主要作用涕侈,返回一個(gè)UserDetails沪停。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這便是交給additionalAuthenticationChecks方法完成的,如果這個(gè)void方法沒有拋異常木张,則認(rèn)為比對成功众辨。比對密碼的過程,用到了PasswordEncoder和SaltSource舷礼,密碼加密和鹽的概念相信不用我贅述了鹃彻,它們?yōu)楸U习踩O(shè)計(jì),都是比較基礎(chǔ)的概念妻献。
如果你已經(jīng)被這些概念搞得暈頭轉(zhuǎn)向了蛛株,不妨這么理解DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性育拨,如果正確谨履,返回一個(gè)數(shù)據(jù)庫中的用戶信息(假設(shè)用戶信息被保存在數(shù)據(jù)庫中)。
1.5 UserDetails與UserDetailsService
上面不斷提到了UserDetails這個(gè)接口至朗,它代表了最詳細(xì)的用戶信息屉符,這個(gè)接口涵蓋了一些必要的用戶信息字段,具體的實(shí)現(xiàn)類對它進(jìn)行了擴(kuò)展锹引。
它和Authentication接口很類似,比如它們都擁有username唆香,authorities嫌变,區(qū)分他們也是本文的重點(diǎn)內(nèi)容之一。Authentication的getCredentials()與UserDetails中的getPassword()需要被區(qū)分對待躬它,前者是用戶提交的密碼憑證腾啥,后者是用戶正確的密碼,認(rèn)證器其實(shí)就是對這兩者的比對冯吓。Authentication中的getAuthorities()實(shí)際是由UserDetails的getAuthorities()傳遞而形成的倘待。還記得Authentication接口中的getUserDetails()方法嗎?其中的UserDetails用戶詳細(xì)信息便是經(jīng)過了AuthenticationProvider之后被填充的组贺。
UserDetailsService和AuthenticationProvider兩者的職責(zé)常常被人們搞混凸舵,關(guān)于他們的問題在文檔的FAQ和issues中屢見不鮮。記住一點(diǎn)即可失尖,敲黑板0⊙佟!掀潮!UserDetailsService只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫)加載用戶信息菇夸,僅此而已,記住這一點(diǎn)仪吧,可以避免走很多彎路庄新。UserDetailsService常見的實(shí)現(xiàn)類有JdbcDaoImpl,InMemoryUserDetailsManager,前者從數(shù)據(jù)庫加載用戶择诈,后者從內(nèi)存中加載用戶械蹋,也可以自己實(shí)現(xiàn)UserDetailsService,通常這更加靈活吭从。
1.6 架構(gòu)概覽圖
為了更加形象的理解上述我介紹的這些核心類朝蜘,附上一張按照我的理解,所畫出Spring Security的一張非典型的UML圖:
如果對Spring Security的這些概念感到理解不能涩金,不用擔(dān)心谱醇,因?yàn)檫@是Architecture First導(dǎo)致的必然結(jié)果,先過個(gè)眼熟步做。后續(xù)的文章會秉持Code First的理念副渴,陸續(xù)詳細(xì)地講解這些實(shí)現(xiàn)類的使用場景,源碼分析全度,以及最基本的:如何配置Spring Security煮剧,在后面的文章中可以不時(shí)翻看這篇文章,找到具體的類在整個(gè)架構(gòu)中所處的位置将鸵,這也是本篇文章的定位勉盅。另外,一些Spring Security的過濾器還未囊括在架構(gòu)概覽中顶掉,如將表單信息包裝成UsernamePasswordAuthenticationToken的過濾器膘壶,考慮到這些雖然也是架構(gòu)的一部分减宣,但是真正重寫他們的可能性較小揽思,所以打算放到后面的章節(jié)講解岭皂。
原文鏈接:https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247484344&idx=1&sn=ceabcd9f4dbcf596fde8f7c1c1c88375&chksm=9bd0ae20aca72736d7867e14eb149e852eaf4502b8d49404f27285e997a5aec7dc65562d558f&scene=21#wechat_redirect