松哥最近在研究 Spring Security 源碼,發(fā)現(xiàn)了很多好玩的代碼,抽空寫幾篇文章和小伙伴們分享一下。
很多人吐槽 Spring Security 比 Shiro 重量級(jí),這個(gè)重量級(jí)不是憑空來的,重量有重量的好處,就是它提供了更為強(qiáng)大的防護(hù)功能。
比如松哥最近看到的一段代碼:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection(); try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
} return loadedUser;
} catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication); throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}}
這段代碼位于 DaoAuthenticationProvider 類中,為了方便大家理解,我來簡(jiǎn)單說下這段代碼的上下文環(huán)境。
當(dāng)用戶提交用戶名密碼登錄之后,Spring Security 需要根據(jù)用戶提交的用戶名去數(shù)據(jù)庫中查詢用戶,這塊如果大家不熟悉,可以參考松哥之前的文章:
- Spring Security 如何將用戶數(shù)據(jù)存入數(shù)據(jù)庫?
- Spring Security+Spring Data Jpa 強(qiáng)強(qiáng)聯(lián)手,安全管理只有更簡(jiǎn)單!
查到用戶對(duì)象之后,再去比對(duì)從數(shù)據(jù)庫中查到的用戶密碼和用戶提交的密碼之間的差異。具體的比對(duì)工作,可以參考Spring Boot 中密碼加密的兩種姿勢(shì)!一文。
而上面這段代碼就是 Spring Security 根據(jù)用戶登錄時(shí)傳入的用戶名去數(shù)據(jù)庫中查詢用戶,并將查到的用戶返回。方法中還有一個(gè) authentication 參數(shù),這個(gè)參數(shù)里邊保存了用戶登錄時(shí)傳入的用戶名/密碼信息。
那么這段代碼有什么神奇之處呢?
我們來一行一行分析。
源碼梳理
1
首先方法一進(jìn)來調(diào)用了 prepareTimingAttackProtection 方法,從方法名字上可以看出,這個(gè)是為計(jì)時(shí)攻擊的防御做準(zhǔn)備,那么什么又是計(jì)時(shí)攻擊呢?別急,松哥一會(huì)來解釋。我們先來吧流程走完。prepareTimingAttackProtection 方法的執(zhí)行很簡(jiǎn)單,如下:
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}}
該方法就是將常量 USER_NOT_FOUND_PASSWORD 使用 passwordEncoder 編碼之后(如果不了解 passwordEncoder,可以參考 Spring Boot 中密碼加密的兩種姿勢(shì)!一文),將編碼結(jié)果賦值給 userNotFoundEncodedPassword 變量。
2
接下來調(diào)用 loadUserByUsername 方法,根據(jù)登錄用戶傳入的用戶名去數(shù)據(jù)庫中查詢用戶,如果查到了,就將查到的對(duì)象返回。
3
如果查詢過程中拋出 UsernameNotFoundException 異常,按理說直接拋出異常,接下來的密碼比對(duì)也不用做了,因?yàn)楦鶕?jù)用戶名都沒查到用戶,這次登錄肯定是失敗的,沒有必要進(jìn)行密碼比對(duì)操作!
但是大家注意,在拋出異常之前調(diào)用了 mitigateAgainstTimingAttack 方法。這個(gè)方法從名字上來看,有緩解計(jì)時(shí)攻擊的意思。
我們來看下該方法的執(zhí)行流程:
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}}
可以看到,這里首先獲取到登錄用戶傳入的密碼即 presentedPassword,然后調(diào)用 passwordEncoder.matches 方法進(jìn)行密碼比對(duì)操作,本來該方法的第二個(gè)參數(shù)是數(shù)據(jù)庫查詢出來的用戶密碼,現(xiàn)在數(shù)據(jù)庫中沒有查到用戶,所以第二個(gè)參數(shù)用 userNotFoundEncodedPassword 代替了,userNotFoundEncodedPassword 就是我們一開始調(diào)用 prepareTimingAttackProtection 方法時(shí)賦值的變量。這個(gè)密碼比對(duì),從一開始就注定了肯定會(huì)失敗,那為什么還要比對(duì)呢?
計(jì)時(shí)攻擊
這就引入了我們今天的主題--計(jì)時(shí)攻擊。
計(jì)時(shí)攻擊是旁路攻擊的一種,在密碼學(xué)中,旁道攻擊又稱側(cè)信道攻擊、邊信道攻擊(Side-channel attack)。
這種攻擊方式并非利用加密算法的理論弱點(diǎn),也不是暴力破解,而是從密碼系統(tǒng)的物理實(shí)現(xiàn)中獲取的信息。例如:時(shí)間信息、功率消耗、電磁泄露等額外的信息源,這些信息可被用于對(duì)系統(tǒng)的進(jìn)一步破解。
旁路攻擊有多種不同的分類:
- 緩存攻擊(Cache Side-Channel Attacks),通過獲取對(duì)緩存的訪問權(quán)而獲取緩存內(nèi)的一些敏感信息,例如攻擊者獲取云端主機(jī)物理主機(jī)的訪問權(quán)而獲取存儲(chǔ)器的訪問權(quán)。
- 計(jì)時(shí)攻擊(Timing attack),通過設(shè)備運(yùn)算的用時(shí)來推斷出所使用的運(yùn)算操作,或者通過對(duì)比運(yùn)算的時(shí)間推定數(shù)據(jù)位于哪個(gè)存儲(chǔ)設(shè)備,或者利用通信的時(shí)間差進(jìn)行數(shù)據(jù)竊取。
- 基于功耗監(jiān)控的旁路攻擊,同一設(shè)備不同的硬件電路單元的運(yùn)作功耗也是不一樣的,因此一個(gè)程序運(yùn)行時(shí)的功耗會(huì)隨著程序使用哪一種硬件電路單元而變動(dòng),據(jù)此推斷出數(shù)據(jù)輸出位于哪一個(gè)硬件單元,進(jìn)而竊取數(shù)據(jù)。
- 電磁攻擊(Electromagnetic attack),設(shè)備運(yùn)算時(shí)會(huì)泄漏電磁輻射,經(jīng)過得當(dāng)分析的話可解析出這些泄漏的電磁輻射中包含的信息(比如文本、聲音、圖像等),這種攻擊方式除了用于密碼學(xué)攻擊以外也被用于非密碼學(xué)攻擊等竊聽行為,如TEMPEST 攻擊。
- 聲學(xué)密碼分析(Acoustic cryptanalysis),通過捕捉設(shè)備在運(yùn)算時(shí)泄漏的聲學(xué)信號(hào)捉取信息(與功率分析類似)。
- 差別錯(cuò)誤分析,隱密數(shù)據(jù)在程序運(yùn)行發(fā)生錯(cuò)誤并輸出錯(cuò)誤信息時(shí)被發(fā)現(xiàn)。
- 數(shù)據(jù)殘留(Data remanence),可使理應(yīng)被刪除的敏感數(shù)據(jù)被讀取出來(例如冷啟動(dòng)攻擊)。
- 軟件初始化錯(cuò)誤攻擊,現(xiàn)時(shí)較為少見,行錘攻擊(Row hammer)是該類攻擊方式的一個(gè)實(shí)例,在這種攻擊實(shí)現(xiàn)中,被禁止訪問的存儲(chǔ)器位置旁邊的存儲(chǔ)器空間如果被頻繁訪問將會(huì)有狀態(tài)保留丟失的風(fēng)險(xiǎn)。
- 光學(xué)方式,即隱密數(shù)據(jù)被一些視覺光學(xué)儀器(如高清晰度相機(jī)、高清晰度攝影機(jī)等設(shè)備)捕捉。
所有的攻擊類型都利用了加密/解密系統(tǒng)在進(jìn)行加密/解密操作時(shí)算法邏輯沒有被發(fā)現(xiàn)缺陷,但是通過物理效應(yīng)提供了有用的額外信息(這也是稱為“旁路”的緣由),而這些物理信息往往包含了密鑰、密碼、密文等隱密數(shù)據(jù)。
而上面 Spring Security 中的那段代碼就是為了防止計(jì)時(shí)攻擊。
具體是怎么做的呢?假設(shè) Spring Security 從數(shù)據(jù)庫中沒有查到用戶信息就直接拋出異常了,沒有去執(zhí)行 mitigateAgainstTimingAttack 方法,那么黑客經(jīng)過大量的測(cè)試,再經(jīng)過統(tǒng)計(jì)分析,就會(huì)發(fā)現(xiàn)有一些登錄驗(yàn)證耗時(shí)明顯少于其他登錄,進(jìn)而推斷出登錄驗(yàn)證時(shí)間較短的都是不存在的用戶,而登錄耗時(shí)較長(zhǎng)的是數(shù)據(jù)庫中存在的用戶。
現(xiàn)在 Spring Security 中,通過執(zhí)行 mitigateAgainstTimingAttack 方法,無論用戶存在或者不存在,登錄校驗(yàn)的耗時(shí)不會(huì)有明顯差別,這樣就避免了計(jì)時(shí)攻擊。
可能有小伙伴會(huì)說,passwordEncoder.matches 方法執(zhí)行能耗費(fèi)多少時(shí)間呀?這要看你怎么計(jì)時(shí)了,時(shí)間單位越小,差異就越明顯:毫秒(ms)、微秒(µs)、奈秒(ns)、皮秒(ps)、飛秒(fs)、阿秒(as)、仄秒(zs)。
另外,Spring Security 為了安全,passwordEncoder 中引入了一個(gè)概念叫做自適應(yīng)單向函數(shù),這種函數(shù)故意執(zhí)行的很慢并且消耗大量系統(tǒng)資源,所以非常有必要進(jìn)行計(jì)時(shí)攻擊防御。
關(guān)于自適應(yīng)單向函數(shù),這是另外一個(gè)故事了,松哥抽空再和小伙伴們聊~
好啦,今天就先和小伙伴們聊這么多,小伙伴們決定有收獲的話,記得點(diǎn)個(gè)在看鼓勵(lì)下松哥哦~
不知不覺,Spring Security 系列已經(jīng)連載了 49 篇啦,感興趣的小伙伴也可以看看本系列其他文章哦:
- 挖一個(gè)大坑,Spring Security 開搞!
- 松哥手把手帶你入門 Spring Security,別再問密碼怎么解密了
- 手把手教你定制 Spring Security 中的表單登錄
- Spring Security 做前后端分離,咱就別做頁面跳轉(zhuǎn)了!統(tǒng)統(tǒng) JSON 交互
- Spring Security 中的授權(quán)操作原來這么簡(jiǎn)單
- Spring Security 如何將用戶數(shù)據(jù)存入數(shù)據(jù)庫?
- Spring Security+Spring Data Jpa 強(qiáng)強(qiáng)聯(lián)手,安全管理只有更簡(jiǎn)單!
- Spring Boot + Spring Security 實(shí)現(xiàn)自動(dòng)登錄功能
- Spring Boot 自動(dòng)登錄,安全風(fēng)險(xiǎn)要怎么控制?
- 在微服務(wù)項(xiàng)目中,Spring Security 比 Shiro 強(qiáng)在哪?
- SpringSecurity 自定義認(rèn)證邏輯的兩種方式(高級(jí)玩法)
- Spring Security 中如何快速查看登錄用戶 IP 地址等信息?
- Spring Security 自動(dòng)踢掉前一個(gè)登錄用戶,一個(gè)配置搞定!
- Spring Boot + Vue 前后端分離項(xiàng)目,如何踢掉已登錄用戶?
- Spring Security 自帶防火墻!你都不知道自己的系統(tǒng)有多安全!
- 什么是會(huì)話固定攻擊?Spring Boot 中要如何防御會(huì)話固定攻擊?
- 集群化部署,Spring Security 要如何處理 session 共享?
- 松哥手把手教你在 SpringBoot 中防御 CSRF 攻擊!so easy!
- 要學(xué)就學(xué)透徹!Spring Security 中 CSRF 防御源碼解析
- Spring Boot 中密碼加密的兩種姿勢(shì)!
- Spring Security 要怎么學(xué)?為什么一定要成體系的學(xué)習(xí)?
- Spring Security 兩種資源放行策略,千萬別用錯(cuò)了!
- 松哥手把手教你入門 Spring Boot + CAS 單點(diǎn)登錄
- Spring Boot 實(shí)現(xiàn)單點(diǎn)登錄的第三種方案!
- Spring Boot+CAS 單點(diǎn)登錄,如何對(duì)接數(shù)據(jù)庫?
- Spring Boot+CAS 默認(rèn)登錄頁面太丑了,怎么辦?
- 用 Swagger 測(cè)試接口,怎么在請(qǐng)求頭中攜帶 Token?
- Spring Boot 中三種跨域場(chǎng)景總結(jié)
- Spring Boot 中如何實(shí)現(xiàn) HTTP 認(rèn)證?
- Spring Security 中的四種權(quán)限控制方式
- Spring Security 多種加密方案共存,老破舊系統(tǒng)整合利器!
- 神奇!自己 new 出來的對(duì)象一樣也可以被 Spring 容器管理!
- Spring Security 配置中的 and 到底該怎么理解?
- 一文搞定 Spring Security 異常處理機(jī)制!
- 寫了這么多年代碼,這樣的登錄方式還是頭一回見!
- Spring Security 竟然可以同時(shí)存在多個(gè)過濾器鏈?
- Spring Security 可以同時(shí)對(duì)接多個(gè)用戶表?
- 在 Spring Security 中,我就想從子線程獲取用戶登錄信息,怎么辦?
- 深入理解 FilterChainProxy【源碼篇】
- 深入理解 SecurityConfigurer 【源碼篇】
- 深入理解 HttpSecurity【源碼篇】
- 深入理解 AuthenticationManagerBuilder 【源碼篇】
- 花式玩 Spring Security ,這樣的用戶定義方式你可能沒見過!
- 深入理解 WebSecurityConfigurerAdapter【源碼篇】
- 盤點(diǎn) Spring Security 框架中的八大經(jīng)典設(shè)計(jì)模式
- Spring Security 初始化流程梳理
- 為什么你使用的 Spring Security OAuth 過期了?松哥來和大家捋一捋!
- 一個(gè)詭異的登錄問題