@laigary.com~/interview/system-design/encoding-hashing-enc….md$
$ cat ./system-design/encoding-hashing-encryption.md
[System Design]·2026-08-10·25 min read

Encoding Hashing Encryption

這三個詞在後端和系統設計面試裡出現的頻率很高,而且很常被混用。最常見的是把「密碼加密存起來」講成加密,但那件事該做的其實是雜湊。

會踩到它們的題目也滿固定的:短網址要用什麼編碼、使用者密碼怎麼存、webhook 或 API token 怎麼簽章、主鍵要用什麼 ID。這幾題的難處通常不在演算法本身,而在有沒有辦法一句話講清楚這三個東西是在解不同的問題。

可逆?需要金鑰?目的典型場景
Encoding(Base64/Base62/hex)可以,任何人都能還原換一種表示法,方便傳輸或儲存短網址、附件、把二進位塞進 JSON
Hashing(SHA-256/bcrypt)不行,單向而且有損指紋、比對、完整性密碼儲存、檔案校驗、分片
Encryption(AES/RSA)可以,但要金鑰保密傳輸中的資料、靜態資料

要先講清楚的是 encoding 不是安全機制。Base64 誰都解得開,它解決的是「這段位元組能不能安全地放進一個只吃文字的通道」,跟保密完全沒有關係。看到有人說某個東西「有經過 Base64 編碼所以很安全」,那就是搞錯了。

Encoding

Base64

最常見的一個。Email 附件、把圖片塞進 HTML、把二進位放進 JSON,用的都是它。字元集是 A–Z、a–z、0–9 再加上 +/,總共 64 個,另外有一個補位用的 =

規則是每 3 個 byte 換成 4 個字元。因為 3 個 byte 是 24 bit,剛好可以切成 4 段 6 bit,而 6 bit 剛好能表示 64 個值。拿 Man 來看,先把三個字元攤成 24 個 bit:

Man
ASCII7797110
二進位010011010110000101101110

然後把這 24 個 bit 重新切成 4 段。這裡的切點跟原本的 byte 邊界是錯開的,那大概是整個 Base64 唯一需要動腦的地方:

6 bit 區塊010011010110000101101110
十進位1922546
查表TWFu

所以 Man 會變成 TWFu

因為 +/ 在網址裡有特殊意義,要放進 URL 或 cookie 的時候得用 Base64URL 這個變體,把 + 換成 -/ 換成 _,通常也把 = 省掉。JWT 用的就是這個版本。

Base62

字元集是 0–9、A–Z、a–z。說穿了就是 Base64 拿掉 +/,所以天生就是 URL-safe,也不用補位。短網址和對外的 ID 幾乎都選它。

換算方式就是一直除以 62 取餘數,最後把餘數反過來讀。以十進位的 125 為例:

步驟算式餘數
1125 ÷ 6221
22 ÷ 6202

餘數反過來是 2、1,查表得到 21。解回去驗算 (2×62)+1=125

其他幾種

Base58 是 Base62 再拿掉 0OIl 這幾個看起來會混淆的字元,目的是讓人手抄的時候不會抄錯,比特幣的錢包地址就是這樣來的。

Base36 只有 0–9 和 A–Z,好處是大小寫不敏感,適合那種要念出來、或是要用鍵盤敲進去的短代碼。

Base32 更保守,只用 A–Z 和 2–7,一樣大小寫不敏感而且容錯性好,所以 QR code、DNS、還有 2FA 的 TOTP 金鑰都用它。

Base85 是這裡面最省空間的,代價是幾乎沒辦法讀,只有 PostScript 和少數壓縮工具在用。

到底省多少

編碼一定會讓資料變大,差別只在變大多少。每個 byte 需要幾個字元就是 8/log2(base)

編碼每 byte 需要的字元數膨脹
Base16(hex)2.000+100%
Base321.600+60%
Base361.547+55%
Base581.366+37%
Base621.344+34%
Base641.333+33%
Base851.248+25%

這裡有個常見的誤傳要修正。很多文章會說 Base85 比 Base64 小 25%,但那個 25% 是 Base85 相對於原始資料的膨脹率,不是它跟 Base64 的差距。實際算一下 1.25/1.333 大約是 0.938,也就是只小了 6% 左右。要多付出可讀性和實作複雜度才換到這 6%,大概就是它一直沒有普及的原因。

另外一件從這張表看得出來的事情是 Base62 和 Base64 幾乎一樣長,1.344 對 1.333。所以為了 URL-safe 從 Base64 換成 Base62,長度上等於不用付代價,短網址選它就是這個理由。

什麼時候用哪一個

場景選擇理由
短網址Base62 或 Base58短、可讀、URL-safe、不用補位
Email 附件(MIME)Base64把二進位安全地放進純文字協定,標準支援
API 或 JSON 裡的二進位Base64URL放進網址或 header 不會被特殊字元弄壞
對外的訂單或使用者 IDBase62 或 Base36短、不連號、好念好抄
區塊鏈地址Base58排除視覺上容易混淆的字元
QR code、2FA 金鑰Base32大小寫不敏感、容錯性好
追求傳輸效率的管線Base85最省,但只有在機器對機器的時候才划算

Hashing

雜湊在面試裡其實是兩個不太相干的題目,連用的函式都不一樣。一種是拿它當指紋,看檔案有沒有被改過、兩份內容是不是一樣、或者決定資料要分到哪一個分片,這一類要快。另一種是拿它存密碼,這一類要慢。把第一類的函式拿去做第二類,是最經典的錯誤。

常見的雜湊函式

函式輸出長度現況
MD5128 bits已破,碰撞可以在幾秒內造出來,只能當非安全用途的 checksum
SHA-1160 bits已破,2017 年 SHAttered 造出了真實的碰撞,已經淘汰
SHA-2(SHA-256/512)256/512 bits目前的主流,沒有已知的實用攻擊
SHA-3可變結構跟 SHA-2 完全不同,當作 SHA-2 萬一被破的備案
BLAKE3可變非常快,適合大檔案校驗,但不是 NIST 標準

講「已破」的時候要說清楚破的是什麼。MD5 和 SHA-1 被破的是碰撞攻擊,也就是可以造出兩份雜湊值相同的不同內容,並不是可以從雜湊值反推回原文。不過對簽章和憑證那種場景來說,能造出碰撞就已經足夠致命了。

密碼不能用一般的雜湊

用 SHA-256 存密碼看起來很合理,反正它不可逆。問題不是它不安全,是它太快,而快本來就是它被設計出來的目的。這個優點放到密碼上就變成災難:現代 GPU 一秒可以算幾十億次,拿一份常見密碼的字典跑一輪,幾分鐘就能把大部分的人還原出來。

所以密碼要用刻意設計得慢的那幾個。bcrypt 算老牌,有 cost 參數可以調慢;scrypt 除了慢還刻意吃記憶體,讓 GPU 和 ASIC 難以平行化;Argon2id 是 2015 年 Password Hashing Competition 的冠軍,新專案大概直接選它就好。

它們都會處理 salt,每個使用者一組隨機值,所以兩個人用同樣的密碼也會存出不同的結果,彩虹表就沒用了。cost 則是留給未來的,硬體變快就把它調高,不用換演算法。慢在這裡是特性不是缺點。

HMAC

一般的雜湊誰都算得出來,所以它只能證明內容沒被改過,不能證明是誰送來的。HMAC 的作法是把一把只有雙方知道的金鑰混進雜湊裡,這樣就同時證明了兩件事。GitHub 和 Stripe 送 webhook 過來時的簽章、JWT 的 HS256、各種 API 請求簽名,用的都是它。

它是雜湊不是加密,收到的人做的事情是拿同樣的金鑰重算一次然後比對,而不是把它解開來看。

順帶一提

系統設計裡常出現的 consistent hashing 跟這一節的其他東西沒什麼關係,它只是拿雜湊來做分片,取的是分布均勻這個性質,跟安全無關。放在一起講很容易讓人搞混。

Encryption

金鑰速度代表主要用途
對稱加密加解密同一把AES-256真正拿來加大量資料
非對稱加密公鑰私鑰一對慢很多RSA、ECC(X25519、Ed25519)交換金鑰、數位簽章

對稱加密的模式建議直接用 AES-GCM,因為它是 authenticated encryption,機密性和完整性一起顧到。用 CBC 的話得自己再接一個 MAC,那一步很容易做錯。

非對稱有兩個方向,不要搞混。公鑰加密、私鑰解密是為了保密,只有拿著私鑰的人看得到內容;私鑰簽名、公鑰驗證則是為了證明來源,誰都可以驗,但只有私鑰持有者簽得出來。

實務上都是混合的

非對稱太慢,所以真實系統不會拿它來加大量資料。TLS 的做法是先用非對稱協商出一把臨時的對稱金鑰,之後整條連線的資料都用這把對稱金鑰加密。

靜態資料也是同一套,叫做信封加密。資料本身用一把隨機產生的資料金鑰加密,這把資料金鑰再用 KMS 裡的主金鑰加密之後存在旁邊。這樣要輪替主金鑰的時候,只要重新加密那些很短的資料金鑰就好,不用把所有資料重新加密一次。

傳輸中和靜態要分開回答

面試問加密通常會分成兩個面向。傳輸中就是 TLS,基本上是預設答案;靜態則是磁碟或儲存層的加密,或者敏感欄位在應用層就先加密起來。要分開講是因為它們防的威脅不一樣,前者防的是路上被竊聽,後者防的是儲存媒介或備份外流。

真正麻煩的是金鑰

演算法都是現成的,實務上會出事的幾乎都在金鑰上:放在哪裡(KMS 或 HSM,不是原始碼裡)、多久輪替一次、外洩之後怎麼撤銷、誰有權限拿。被問到加密卻只答得出演算法名字,會顯得沒有真的做過。

UUID 為什麼常跟前面混在一起講

很多時候我看到會有人把上面的內容跟 UUID 放在一起講,但是它們並不是同一件事。UUID 回答的是怎麼產生一個唯一的值,encoding 回答的是這個值要怎麼寫成字串。

會混在一起是因為同一個 UUID 可以用不同的編碼表示,長度差滿多的:

表示法長度
hex 加上連字號,也就是平常看到的樣子36 字元
hex32 字元
Base3226 字元
Base62 或 Base64URL22 字元

短網址那種題目會同時碰到這兩件事,既要唯一又要短,所以兩個主題就被綁在一起講了。分開來看的話它們是兩層,先決定要產生什麼值,再決定怎麼表示它。

常見的版本

版本組成特性
v1時間戳加上網卡 MAC可以排序,但會洩漏機器資訊和產生時間
v4122 bits 隨機最常見,不洩漏資訊,但完全無序
v7毫秒時間戳前綴加上隨機2024 年 RFC 9562 納入標準,可以排序又不洩漏機器資訊

v4 當主鍵會痛在哪裡

這大概是實務上最值得記住的一點。v4 是隨機的,所以每次插入都會落在 B-tree 索引的隨機位置,造成頁面分裂、索引膨脹、快取命中率變差,資料量一大寫入效能就會明顯掉下來。

自增整數沒有這個問題,它永遠插在尾端,但它會洩漏你有多少筆資料,而且很容易被猜。

v7 就是為了同時解決這兩件事。前綴是毫秒時間戳,所以整體是遞增的、對索引友善;後面接隨機,所以猜不到也不會洩漏機器資訊。新專案要用 UUID 當主鍵的話,現在應該直接選 v7。

相近的東西

ULID 的想法跟 v7 一樣,時間戳加隨機,一樣是 128 bit,但它規定要用 Crockford Base32 編成 26 個字元,所以字串本身就可以直接拿來排序。

Snowflake 是 Twitter 的做法,只有 64 bit,切成時間戳、機器編號和序號三段。長度只有 UUID 的一半而且嚴格遞增,代價是需要協調機器編號,也就是得有一套發號的基礎設施。