鬆哥最近在研究 Spring Security 源碼,發現了不少好玩的代碼,抽空寫幾篇文章和小夥伴們分享一下。java
不少人吐槽 Spring Security 比 Shiro 重量級,這個重量級不是憑空來的,重量有重量的好處,就是它提供了更爲強大的防禦功能。算法
好比鬆哥最近看到的一段代碼:數據庫
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 類中,爲了方便你們理解,我來簡單說下這段代碼的上下文環境。後端
當用戶提交用戶名密碼登陸以後,Spring Security 須要根據用戶提交的用戶名去數據庫中查詢用戶,這塊若是你們不熟悉,能夠參考鬆哥以前的文章:設計模式
查到用戶對象以後,再去比對從數據庫中查到的用戶密碼和用戶提交的密碼之間的差別。具體的比對工做,能夠參考Spring Boot 中密碼加密的兩種姿式!一文。跨域
而上面這段代碼就是 Spring Security 根據用戶登陸時傳入的用戶名去數據庫中查詢用戶,並將查到的用戶返回。方法中還有一個 authentication 參數,這個參數裏邊保存了用戶登陸時傳入的用戶名/密碼信息。緩存
那麼這段代碼有什麼神奇之處呢?安全
咱們來一行一行分析。session
首先方法一進來調用了 prepareTimingAttackProtection 方法,從方法名字上能夠看出,這個是爲計時攻擊的防護作準備,那麼什麼又是計時攻擊呢?別急,鬆哥一會來解釋。咱們先來吧流程走完。prepareTimingAttackProtection 方法的執行很簡單,以下:框架
private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); } }
該方法就是將常量 USER_NOT_FOUND_PASSWORD 使用 passwordEncoder 編碼以後(若是不瞭解 passwordEncoder,能夠參考 Spring Boot 中密碼加密的兩種姿式!一文),將編碼結果賦值給 userNotFoundEncodedPassword 變量。
接下來調用 loadUserByUsername 方法,根據登陸用戶傳入的用戶名去數據庫中查詢用戶,若是查到了,就將查到的對象返回。
若是查詢過程當中拋出 UsernameNotFoundException 異常,按理說直接拋出異常,接下來的密碼比對也不用作了,由於根據用戶名都沒查到用戶,此次登陸確定是失敗的,沒有必要進行密碼比對操做!
可是你們注意,在拋出異常以前調用了 mitigateAgainstTimingAttack 方法。這個方法從名字上來看,有緩解計時攻擊的意思。
咱們來看下該方法的執行流程:
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); } }
能夠看到,這裏首先獲取到登陸用戶傳入的密碼即 presentedPassword,而後調用 passwordEncoder.matches 方法進行密碼比對操做,原本該方法的第二個參數是數據庫查詢出來的用戶密碼,如今數據庫中沒有查到用戶,因此第二個參數用 userNotFoundEncodedPassword 代替了,userNotFoundEncodedPassword 就是咱們一開始調用 prepareTimingAttackProtection 方法時賦值的變量。這個密碼比對,從一開始就註定了確定會失敗,那爲何還要比對呢?
這就引入了咱們今天的主題--計時攻擊。
計時攻擊是旁路攻擊的一種,在密碼學中,旁道攻擊又稱側信道攻擊、邊信道攻擊(Side-channel attack)。
這種攻擊方式並不是利用加密算法的理論弱點,也不是暴力破解,而是從密碼系統的物理實現中獲取的信息。例如:時間信息、功率消耗、電磁泄露等額外的信息源,這些信息可被用於對系統的進一步破解。
旁路攻擊有多種不一樣的分類:
全部的攻擊類型都利用了加密/解密系統在進行加密/解密操做時算法邏輯沒有被發現缺陷,可是經過物理效應提供了有用的額外信息(這也是稱爲「旁路」的原因),而這些物理信息每每包含了密鑰、密碼、密文等隱密數據。
而上面 Spring Security 中的那段代碼就是爲了防止計時攻擊。
具體是怎麼作的呢?假設 Spring Security 從數據庫中沒有查到用戶信息就直接拋出異常了,沒有去執行 mitigateAgainstTimingAttack 方法,那麼黑客通過大量的測試,再通過統計分析,就會發現有一些登陸驗證耗時明顯少於其餘登陸,進而推斷出登陸驗證時間較短的都是不存在的用戶,而登陸耗時較長的是數據庫中存在的用戶。
如今 Spring Security 中,經過執行 mitigateAgainstTimingAttack 方法,不管用戶存在或者不存在,登陸校驗的耗時不會有明顯差異,這樣就避免了計時攻擊。
可能有小夥伴會說,passwordEncoder.matches 方法執行能耗費多少時間呀?這要看你怎麼計時了,時間單位越小,差別就越明顯:毫秒(ms)、微秒(µs)、奈秒(ns)、皮秒(ps)、飛秒(fs)、阿秒(as)、仄秒(zs)。
另外,Spring Security 爲了安全,passwordEncoder 中引入了一個概念叫作自適應單向函數,這種函數故意執行的很慢而且消耗大量系統資源,因此很是有必要進行計時攻擊防護。
關於自適應單向函數,這是另一個故事了,鬆哥抽空再和小夥伴們聊~
好啦,今天就先和小夥伴們聊這麼多,小夥伴們決定有收穫的話,記得點個在看鼓勵下鬆哥哦~
不知不覺,Spring Security 系列已經連載了 49 篇啦,感興趣的小夥伴也能夠看看本系列其餘文章哦: