歡迎光臨
每天分享高質量文章

該如何設計你的 PasswordEncoder?

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

來源:https://www.cnkirito.moe/spring-security-6

緣起

前端時間將一個集成了 spring-security-oauth2 的舊專案改造了一番,將 springboot 升級成了 springboot 2.0,眾所周知 springboot 2.0 依賴的是 spring5,並且許多相關的依賴都發生了較大的改動,與本文相關的改動羅列如下,有興趣的同學可以看看:Spring Security 5.0 New Features ,增強了 oauth2 整合的功能以及和一個比較有意思的改動—重構了密碼編碼器的實現(Password Encoding,由於大多數 PasswordEncoder 相關的演演算法是 hash 演演算法,所以本文將 PasswordEncoder 翻譯成‘密碼編碼器’和並非‘密碼加密器’)官方稱之為

Modernized Password Encoding — 現代化的密碼編碼方式
另外,springboot2.0 的自動配置也做了一些調整,其中也有幾點和 spring-security 相關,戳這裡看所有細節 springboot2.0 遷移指南

一開始,我僅僅修改了依賴,將

<parent>
    <groupId>org.springframework.bootgroupId>


    <artifactId>spring-boot-starter-parentartifactId>
    <version>1.5.4.RELEASEversion>
parent>

升級成了

<parent>
    <groupId>org.springframework.bootgroupId>


    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.0.1.RELEASEversion>
parent>

不出意料出現了相容性的問題,我在嘗試登陸時,出現瞭如下的報錯

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

原因也很明顯,正如 spring security 的更新檔案中描述的那樣,spring security 5 對 PasswordEncoder 做了相關的重構,原先預設配置的 PlainTextPasswordEncoder(明文密碼)被移除了。這引起了我的興趣,spring security 在新版本中對於 passwordEncoder 進行了哪些改造,這些改造背後又是出於什麼樣的目的呢?賣個關子,先從遠古時期的案例來一步步演化出所謂的“現代化密碼編碼方式”。

密碼儲存演進史

自從網際網路有了使用者的那一刻起,儲存使用者密碼這件事便成為了一個健全的系統不得不面對的一件事。遠古時期,明文儲存密碼可能還不被認為是一個很大的系統缺陷(事實上這是一件很恐怖的事)。提及明文儲存密碼,我立刻聯想到的是 CSDN 社群在 2011 年末發生的 600 萬使用者密碼洩露的事件,誰也不會想到這個和程式員密切相關的網站會犯如此低階的錯誤。明文儲存密碼使得惡意使用者可以透過 sql 註入等攻擊方式來獲取使用者名稱和密碼,雖然安全框架和良好的編碼規範可以規避很多類似的攻擊,但依舊避免不了系統管理員,DBA 有途徑獲取使用者密碼這一事實。事實上,不用明文儲儲存存密碼,程式員們早在 n 多年前就已經達成了共識。

不能明文儲存,一些 hash 演演算法便被廣泛用做密碼的編碼器,對密碼進行單向 hash 處理後儲存資料庫,當使用者登入時,計算使用者輸入的密碼的 hash 值,將兩者進行比對。單向 hash 演演算法,顧名思義,它無法(或者用不能輕易更為合適)被反向解析還原出原密碼。這杜絕了管理員直接獲取密碼的途徑,可僅僅依賴於普通的 hash 演演算法(如 md5,sha256)是不合適的,他主要有 3 個特點:

  1. 同一密碼生成的 hash 值一定相同

  2. 不同密碼的生成的 hash 值可能相同(md5 的碰撞問題相比 sha256 還要嚴重)

  3. 計算速度快。

以上三點結合在一起,破解此類演演算法成了不是那麼困難的一件事,尤其是第三點,會在下文中再次提到,多快才算非常快?按照相關資料的說法:

modern hardware perform billions of hash calculations a second.

考慮到大多數使用者使用的密碼多為數字+字母+特殊符號的組合,攻擊者將常用的密碼進行列舉,甚至透過排列組合來暴力破解,這被稱為 rainbow table。演演算法愛好者能夠立刻看懂到上述的方案,這被親切地稱之為—打表,一種暴力美學,這張表是可以被覆用的。

雖然僅僅依賴於傳統 hash 演演算法的思路被否決了,但這種 hash 後比對的思路,幾乎被後續所有的最佳化方案繼承。

hash 方案迎來的第一個改造是對引入一個“隨機的因子”來摻雜進明文中進行 hash 計算,這樣的隨機因子通常被稱之為鹽 (salt)。salt 一般是使用者相關的,每個使用者持有各自的 salt。此時狗蛋和二丫的密碼即使相同,由於 salt 的影響,儲存在資料庫中的密碼也是不同的,除非…為每個使用者單獨建議一張 rainbow table。很明顯 salted hash 相比普通的單向 hash 方案加大了 hacker 攻擊的難度。但瞭解過 GPU 平行計算能力之強大的童鞋,都能夠意識到,雖然破解 salted hash 比較麻煩,卻並非不可行,勤勞勇敢的安全專家似乎也對這個方案不夠滿意。

為解決上述 salted hash 仍然存在的問題,一些新型的單向 hash 演演算法被研究了出來。其中就包括:Bcrypt,PBKDF2,Scrypt,Argon2。為什麼這些 hash 演演算法能保證密碼儲存的安全性?因為他們足夠慢,恰到好處的慢。這麼說不嚴謹,只是為了給大家留個深刻的映像:慢。這類演演算法有一個特點,存在一個影響因子,可以用來控制計算強度,這直接決定了破解密碼所需要的資源和時間,直觀的體會可以見下圖,在一年內破解如下演演算法所需要的硬體資源花費(折算成美元)

一年內破解如下演演算法所需要的硬體資源花費

一年內破解如下演演算法所需要的硬體資源花費

這使得破解成了一件極其困難的事,並且,其中的計算強度因子是可控的,這樣,即使未來量子計算機的計算能力爆表,也可以透過其控制計算強度以防破解。註意,普通的驗證過程只需要計算一次 hash 計算,使用此類 hash 演演算法並不會影響到使用者體驗。

慢 hash 演演算法真的安全嗎?

Bcrypt,Scrypt,PBKDF2 這些慢 hash 演演算法是目前最為推崇的 password encoding 方式,好奇心驅使我思考了這樣一個問題:慢 hash 演演算法真的安全嗎?

我暫時還沒有精力仔細去研究他們中每一個演演算法的具體實現,只能透過一些文章來拾人牙慧,簡單看看這幾個演演算法的原理和安全性。

PBKDF2 被設計的很簡單,它的基本原理是透過一個偽隨機函式(例如 HMAC 函式),把明文和一個鹽值作為輸入引數,然後按照設定的計算強度因子重覆進行運算,並最終產生金鑰。這樣的重覆 hash 已經被認為足夠安全,但也有人提出了不同意見,此類演演算法對於傳統的 CPU 來說的確是足夠安全,但 GPU 被搬了出來,前文提到過 GPU 的平行計算能力非常強大。

Bcrypt 強大的一點在於,其不僅僅是 CPU 密集型,還是 RAM 密集型!雙重的限制因素,導致 GPU,ASIC(專用積體電路)無法應對 Bcrypt 帶來的破解困境。

然後…看了 Scrypt 的相關資料之後我才意識到這個坑有多深。一個熟悉又陌生的詞出現在了我面前:FPGA(現場可程式設計邏輯閘陣列),這貨就比較厲害了。現成的晶片指令結構如傳統的 CPU,GPU,ASIC 都無法破解 Bcrypt,但是 FPGA 支援燒錄邏輯閘(如AND、OR、XOR、NOT),透過程式設計的方式燒錄指令集的這一特性使得可以定製硬體來破解 Bcrypt。儘管我不認為懂這個技術的人會去想辦法破解真正的系統,但,只要這是一個可能性,就總有方法會被髮明出來與之對抗。Scrypt 比 Bcrypt 額外考慮到的就是大規模的自定義硬體攻擊 ,從而刻意設計需要大量記憶體運算。

理論終歸是理論,實際上 Bcrypt 演演算法被髮明至今 18 年,使用範圍廣,且從未因為安全問題而被修改,其有限性是已經被驗證過的,相比之下 Scrypt 據我看到的文章顯示是 9 年的歷史,沒有 Bcrypt 使用的廣泛。從破解成本和權威性的角度來看,Bcrypt 用作密碼編碼器是不錯的選擇。

spring security 廢棄的介面

回到檔案中,spring security 5 對 PasswordEncoder 做了相關的重構,原先預設配置的 PlainTextPasswordEncoder(明文密碼)被移除了,想要做到明文儲存密碼,只能使用一個過期的類來過渡

@Bean
PasswordEncoder passwordEncoder(){
    return NoOpPasswordEncoder.getInstance();
}

實際上,spring security 提供了 BCryptPasswordEncoder 來進行密碼編碼,並作為了相關配置的預設配置,只不過沒有暴露為全域性的 Bean。使用明文儲存的風險在文章一開始就已經強調過,NoOpPasswordEncoder 只能存在於 demo 中。

@Bean
PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

別忘了對你資料庫中的密碼進行同樣的編碼,否則無法對應。

更深層的思考

實際上,spring security 5 的另一個設計是促使我寫成本文的初衷。

不知道有沒有讀者產生跟我相同的困擾:

  1. 如果我要設計一個 QPS 很高的登入系統,使用 spring security 推薦的 BCrypt 會不會存在效能問題?

  2. spring security 怎麼這麼坑,原來的密碼編碼器都給改了,我需要怎麼遷移舊密碼編碼的應用程式?

  3. 萬一以後出了更高效的加密演演算法,這種笨重的硬編碼方式配置密碼編碼器是不是不夠靈活?

在 spring security 5 提供了這樣一個思路,應該將密碼編碼之後的 hash 值和加密方式一起儲存,並提供了一個 DelegatingPasswordEncoder 來作為眾多密碼密碼編碼方式的集合。

@Bean
PasswordEncoder passwordEncoder(){
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

負責生產 DelegatingPasswordEncoder 的工廠方法:

public class PasswordEncoderFactories {

   public static PasswordEncoder createDelegatingPasswordEncoder() {
      String encodingId = "bcrypt";
      Map encoders = new HashMap<>();
      encoders.put(encodingId, new BCryptPasswordEncoder());
      encoders.put("ldap"new LdapShaPasswordEncoder());
      encoders.put("MD4"new Md4PasswordEncoder());
      encoders.put("MD5"new MessageDigestPasswordEncoder("MD5"));
      encoders.put("noop", NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2"new Pbkdf2PasswordEncoder());
      encoders.put("scrypt"new SCryptPasswordEncoder());
      encoders.put("SHA-1"new MessageDigestPasswordEncoder("SHA-1"));
      encoders.put("SHA-256"new MessageDigestPasswordEncoder("SHA-256"));
      encoders.put("sha256"new StandardPasswordEncoder());

      return new DelegatingPasswordEncoder(encodingId, encoders);
   }

   private PasswordEncoderFactories() {}
}

如此註入 PasswordEncoder 之後,我們在資料庫中需要這麼儲存資料:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

還記得文章開始的報錯嗎?

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

這個 id 就是因為我們沒有為資料庫中的密碼新增 {bcrypt} 此類的字首導致的。

你會不會擔心密碼洩露後,{bcrypt},{pbkdf2},{scrypt},{sha256} 此類字首會直接暴露密碼的編碼方式?其實這個考慮是多餘的,因為密碼儲存的依賴演演算法並不是一個秘密。大多數能搞到你密碼的 hacker 都可以輕鬆的知道你用的是什麼演演算法,例如,bcrypt 演演算法通常以

稍微思考下,前面的三個疑問就可以迎刃而解,這就是檔案中所謂的:能夠自適應伺服器效能的現代化密碼編碼方案

參考

Password Hashing: PBKDF2, Scrypt, Bcrypt

core-services-password-encoding

show me the code

spring security oauth2 的 github 程式碼示例,體會下 spring security 4 -> spring security 5 的相關變化。

https://github.com/lexburner/oauth2-demo



如果你對 Dubbo / Netty 等等原始碼與原理感興趣,歡迎加入我的知識星球一起交流。長按下方二維碼噢

目前在知識星球更新了《Dubbo 原始碼解析》目錄如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 伺服器

16. P2P 伺服器

17. HTTP 伺服器

18. 序列化 Serialization

19. 叢集容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing

… 一共 69+ 篇

目前在知識星球更新了《Netty 原始碼解析》目錄如下:

01. 除錯環境搭建
02. NIO 基礎
03. Netty 簡介
04. 啟動 Bootstrap

05. 事件輪詢 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 位元組緩衝區 ByteBuf

09. 通道處理器 ChannelHandler

10. 編解碼 Codec

11. 工具類 Util

… 一共 61+ 篇

目前在知識星球更新了《資料庫物體設計》目錄如下:


01. 商品模組
02. 交易模組
03. 營銷模組
04. 公用模組

… 一共 17+ 篇


目前在知識星球更新了《Spring 原始碼解析》目錄如下:


01. 除錯環境搭建
02. IoC Resource 定位
03. IoC BeanDefinition 載入

04. IoC BeanDefinition 註冊

05. IoC Bean 獲取

06. IoC Bean 生命週期

… 一共 35+ 篇

贊(0)

分享創造快樂