「存之前先 Base64 一下就好了嘛。」審過夠多的組態檔、內部後台、瀏覽器 bundle、Webhook 處理器與驗證程式碼之後,你早晚會看到這句話。因為輸出看起來雜亂無章,聽起來就像很有道理:
c2VjcmV0IHBhc3N3b3Jk
但 Base64 不是加密。它不是密碼保護。也不是在 URL 裡安全藏 API 金鑰、JWT claims、使用者 ID、Basic 驗證憑據或私密資料的辦法。同一個值一次呼叫就會解出來:
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
實務規則很直白:如果原值不用金鑰就能還原,那 Base64 就沒有保護它,只是換了個表示方式而已。
快速安全自檢
看到程式碼裡的 Base64,先問一句它在做什麼:
| Base64 被用來做什麼 | 合理用途? | 需要檢查什麼 |
|---|---|---|
| 把位元組塞進 JSON | 是 | 大小上限保持合理 |
內嵌一個小的 data: URI |
是 | 別無腦內嵌大檔案 |
| 把密文、nonce、鹽或簽章位元組以文字形式儲存 | 是 | 加密步驟必須在 Base64 之前 |
| 攜帶一個不透明的分頁游標 | 通常可 | 當作介面細節,不是存取控制 |
| 建構 Basic 驗證標頭 | 只能搭 HTTPS | 保護請求的是 TLS,不是 Base64 |
| 在前端程式碼裡藏 API 金鑰 | 否 | 每個瀏覽器都會拿到值和解碼器 |
| 儲存密碼 | 否 | 用慢速密碼雜湊 |
| 藏 JWT claims | 否 | 已簽名的 JWT 載荷若沒做 JWE 加密就是可讀的 |
| 在 URL 裡藏使用者 ID 或角色 | 否 | 伺服器端授權仍必須執行 |
如果你腦中冒出的話是「這樣就沒人能讀了」,那你講的不是 Base64,是加密、雜湊、簽章、存取控制或機密管理。
Base64 實際上在做什麼
Base64 是一種二進位到文字的編碼方案。RFC 4648 定義了常見的 Base64 字母表:
A-Z a-z 0-9 + / =
字元 = 是填補。Base64 把輸入位元組按 24 位一組分組:3 個輸入位元組變成 4 個可列印字元。所以 Base64 的輸出比壓縮前的原始位元組多約 33%。
btoa("hello");
// "aGVsbG8="
atob("aGVsbG8=");
// "hello"
同樣的往返在終端機也一樣:
printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk
printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password
沒有密碼提示、私鑰、解密步驟、鹽、nonce、IV、tag,也沒有工作因子。誰拿到編碼字串誰就能還原。
Base64 與 Base64url
標準 Base64 不一定 URL 安全,因為 +、/、= 放在 URL、檔名、Cookie、Token 段裡都不好處理。Base64url 換了字母表:
| 特性 | 標準 Base64 | Base64url |
|---|---|---|
| 第 62、63 個字元 | + 和 / |
- 和 _ |
| 填補 | 通常是 = |
常常省略 |
| 常見場景 | MIME、PEM、Basic 驗證、二進位欄位 | JWT、URL Token、檔名 |
| 安全差異 | 無 | 無 |
除錯 JWT 時這個很重要。JWT 用的是 Base64url,不是帶填補的標準 Base64:
header.payload.signature
如果你把一段 JWT 貼進標準 Base64 解碼器失敗了,那段字串未必是秘密的、也未必被加密了,只是可能需要按 Base64url 處理:
function base64urlToBase64(part) {
let out = part.replace(/-/g, "+").replace(/_/g, "/");
while (out.length % 4 !== 0) out += "=";
return out;
}
這個轉換只換字母表,不會多出機密性。
UTF-8:瀏覽器的坑
瀏覽器的 btoa() 與 atob() 處理的是二進位字串。純 ASCII 的例子沒問題:
btoa("hello");
// "aGVsbG8="
但 Unicode 文字得先過一步轉位元組。用 hello 測過後再去編碼客戶姓名、中文、Emoji 或阿拉伯文,團隊常常會被打個措手不及:
btoa("你好");
// InvalidCharacterError in browsers
改用 UTF-8 位元組:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"
本站的 Base64 Encode & Decode 工具會在瀏覽器裡完成 UTF-8 的往返,非 ASCII 文字會照原文字回來,不會變成亂碼。
開發者為什麼會把 Base64 當加密
Base64 常出現在真安全機制附近:
- TLS 憑證以 PEM 文字形式儲存。
- JWT 使用 Base64url 段。
- HTTP Basic 驗證用 Base64 編碼
username:password。 - SSH 公鑰裡帶有 Base64 編碼的金鑰素材。
- 加密後的密文常常在儲存或傳輸前做 Base64 編碼。
最後這一點造成了最多混淆。被加密的資料可能是 Base64 編碼的,但 Base64 不是那份加密。安全來自 AES-GCM、XChaCha20-Poly1305、RSA-OAEP、JWE 或 TLS 這類密碼學運算,Base64 只是讓結果位元組更容易放進 JSON、電子郵件、標頭或資料庫欄位。
在程式碼與產品文案裡用精準的措辭:
encodedBackup // 只是 Base64,OK
encryptedBackup // 真的做過加密才 OK
signedState // 有 HMAC 或簽章保護完整性時 OK
passwordHash // 由密碼雜湊演算法產生時 OK
這裡的命名不是裝飾。誤導的名字會讓後面的審閱者以為存在一道安全邊界——實際上並沒有。
編碼 vs 加密 vs 雜湊 vs 簽章
這些運算解決的是不同問題:
| 運算 | 主要作用 | 是否需要祕密 | 是否可逆 | 例子 |
|---|---|---|---|---|
| 編碼 | 改變表示 | 否 | 是 | Base64、Base64url、URL 編碼 |
| 加密 | 隱藏明文 | 是 | 有金鑰則可逆 | AES-GCM、XChaCha20-Poly1305、JWE |
| 雜湊 | 單向指紋 | 否 | 否 | 校驗和用的 SHA-256 |
| 密碼雜湊 | 慢速密碼驗證 | 鹽加成本參數 | 否 | Argon2id、bcrypt、scrypt |
| 簽章 / MAC | 證明完整性與來源 | 是,或金鑰對 | 否 | HMAC、JWS、Ed25519 簽章 |
Base64 回答的是「這串位元組能走文字通道嗎?」,不回答「誰能讀?」「誰改過?」「這個使用者可以存取那筆記錄嗎?」。
真實安全錯誤 1:把 API 金鑰藏在前端程式碼
這種模式在 React 應用、瀏覽器擴充功能、WordPress 主題與內部管理工具都會見到:
// 金鑰依舊會被送到每個瀏覽器。
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");
fetch("https://api.example.com/report", {
headers: { Authorization: `Bearer ${apiKey}` },
});
Base64 沒有幫助,因為瀏覽器同時拿到編碼後的值和解碼器。誰都能打開 DevTools、看 JavaScript bundle、跑一次一樣的 atob(),把金鑰抄走。
修復要從架構上來:
- 真的機密留在伺服器端。
- 瀏覽器上的金鑰用嚴格的 scope 與網域限制。
- 敏感的上游呼叫透過你的後端代理。
- 已經發到使用者那邊的機密全部輪換。
- 假設機密掃描器能解碼類 Base64 的字串。
編碼一把金鑰可能讓程式碼審查更麻煩,但金鑰不會更安全。
真實安全錯誤 2:把密碼存成 Base64
這特別糟,因為它可以在資料庫裡靜靜地待上好幾年:
// 可逆儲存,別這樣做。
const storedPassword = btoa(password);
// 之後:
const password = atob(storedPassword);
資料庫一外洩,每個密碼馬上都會還原。沒有破解成本,因為根本沒東西好破解。
密碼儲存必須是單向的:
// 用密碼雜湊函式庫時的流程樣貌。
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);
正常的登入系統從來不需要還原使用者的密碼,只要能拿候選密碼比對已儲存的雜湊就夠了。
真實安全錯誤 3:把 Basic 驗證誤認為加密
那個標頭看起來夠「編碼」,能唬住人:
Authorization: Basic dXNlcjpwYXNzd29yZA==
解碼出來是:
atob("dXNlcjpwYXNzd29yZA==");
// "user:password"
HTTP Basic 驗證只有在請求被 HTTPS 保護時才可以接受。TLS 負責傳輸中的加密。Base64 只是把 username:password 打包成適合放在標頭裡的 ASCII 字串。
在純 HTTP 上,網路觀察者能直接看到憑據。日誌、反向代理、APM 追蹤與技術支援匯出中的 Basic 驗證標頭都應該當成明文密碼處理。
真實安全錯誤 4:把 JWT 載荷當作私密
一個普通的簽名 JWT 有三段 Base64url:
header.payload.signature
前兩段解碼成 JSON,第三段是簽章。
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
解碼結果:
{
"sub": "1234",
"role": "admin"
}
這並不代表 token 是假的。簽名 JWT 可以同時做到防竄改與可讀。簽章保護的是完整性:告訴伺服器 token 是否由可信任方核發,以及標頭或載荷有沒有被改動過。它不隱藏載荷。
不要在普通簽名 JWT 的載荷裡放密碼、API 金鑰、Session 機密、完整信用卡號、醫療資訊或私人個人資料。如果 claim 需要機密性,用 JWE,或乾脆不要把這些資料放進 token。
還有一點:解碼不是驗證。開發者工具能顯示 claims;伺服器在信任它們之前,仍然必須驗證簽章、issuer、audience、過期時間、key ID、演算法策略以及應用自己的授權規則。
真實安全錯誤 5:看起來不透明的 URL 參數
舊系統常這樣傳狀態:
/profile?data=eyJ1c2VySWQiOjQyfQ==
解出來是:
{
"userId": 42
}
跟著兩個問題就來了。誰都能讀,誰都能改了以後再重新編碼。如果 /profile?data=... 是決定要載入哪筆使用者資料的唯一依據,那 bug 就是缺少授權。
更安全的做法:
- 把解碼出來的 URL 資料當成不可信任輸入。
- 在伺服器再檢查一次權限。
- 狀態敏感時優先使用短的、伺服器端隨機參照。
- 客戶端如果得攜帶防竄改的狀態,用 HMAC 或簽章。
- 私密狀態別放在 URL 裡,URL 會被複製到日誌、瀏覽器歷史、分析與技術支援截圖。
真實安全錯誤 6:只是編碼卻叫「加密匯出」
程式碼審查的另一種味道是這種函式:
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
函式名說加密,程式碼說編碼。
若這個匯出只想做一段方便攜帶的文字 blob,就改個名字:
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
如果真的要告訴使用者匯出是加密的,就用真正的認證加密,把金鑰管理明確寫清楚:經過審核的密碼函式庫、隨機的 nonce/IV、認證 tag、安全的金鑰生成,還有金鑰存放計畫。之後再用 Base64 包一層密文當然可以。
Base64 真正擅長的事
Base64 是有用的,錯的是給它派安全任務。
好的用途:
- JSON、XML、HTML 或電子郵件裡的二進位資料。
- MIME 電子郵件附件。
- 圖片或字型的小
data:URI。 - 密文、簽章、nonce、鹽與公鑰等密碼學輸出的格式化。
- JWT 與 JOSE 的緊湊序列化各段。
- 可讀性屬於產品層級關注、不是安全邊界的不透明分頁游標。
- 除錯 API 欄位、JWT 載荷與設定 blob。
安全的措辭是「Base64 編碼」,不是「加密」。就這一個詞,能省掉在正式環境裡多得驚人的誤解。
該用什麼代替
如果你伸手去找 Base64 是想得到安全,那就挑一個切合場景的原語:
| 目標 | 該用什麼 |
|---|---|
| 對使用者或攻擊者隱藏資料 | 認證加密,例如 AES-GCM 或 XChaCha20-Poly1305 |
| 保護傳輸中的資料 | HTTPS/TLS |
| 儲存密碼 | Argon2id、bcrypt 或 scrypt,搭配每筆密碼獨立鹽與調整後的成本設定 |
| 儲存 API 金鑰 | 機密管理服務、伺服器端環境、scope、稽核紀錄與輪換 |
| 證明某個值沒被改過 | HMAC 或數位簽章 |
| 阻止使用者編輯 URL 狀態 | 伺服器端授權,加上簽章狀態或伺服器端 Session 參照 |
| 讓 JWT claim 保密 | JWE,或乾脆不要把私密 claim 放進 token |
| 把二進位塞進 JSON、電子郵件或標頭 | Base64 就是對的選擇 |
難的部分很少是演算法名稱,而是威脅建模、金鑰管理、輪換、存取控制,以及一開始就要決定誰可以看見明文。
實用的程式碼審查清單
先找出 Base64 的使用點:
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
對每個命中的地方做分類:
- 傳輸:為 JSON、MIME、data URI、PEM 或儲存做位元組到文字的轉換。通常沒問題。
- 加密後的格式化:密文、nonce、簽章、鹽或公鑰。密碼學正確的話通常沒問題。
- 不透明介面:游標、邀請碼或客戶端攜帶的狀態。檢查授權與防竄改。
- 藏機密:API 金鑰、密碼、token、憑據、私密 claim 或使用者資料。這不 OK。
- 命名誤導:變數或 UI 文案宣稱加密、安全、受保護、機密、遮罩或隱藏。要嘛改名字,要嘛改實作。
接著問自己:
- 編碼後的值會不會到達瀏覽器、行動 App、使用者、日誌收集器、代理或技術支援工具?
- 旁邊是不是就放著解碼器?
- 解碼後有沒有伺服器在檢查權限?
- 如果客戶端可以修改,這個值是否被簽章過?
- 如果是 JWT,做出信任決策前是否驗證過簽章?
- 如果是密碼,為什麼它是可逆的?
這樣可以讓審查踩在地上。data URI 裡的 PNG 是無聊題材;JavaScript bundle 裡的正式環境 API 金鑰不是。
事件回應:發現 Base64「機密」怎麼辦
如果你在原始碼、日誌、工單、分析、URL 或資料庫匯出裡發現 Base64 編碼的機密:
- 本地解碼,確認到底是什麼。
- 把解碼後的值視為已經外洩。
- 輪換任何可能已經被看到的憑據、token 與金鑰。
- 盡量從原始碼、日誌、截圖、工單中移除編碼版本。
- 把設計換成伺服器端機密、帶 scope 的公鑰、簽章狀態、真正的加密或密碼雜湊。
- 加一條審查規則,讓同樣的模式別再回來。
不必花時間爭論那串 Base64「難不難被察覺」。自動化掃描器、瀏覽器使用者、攻擊者都能解開。
你真的需要加密 token 的時候
標準的簽名 JWT 用的是 JWS:可讀的載荷、防竄改的簽章。若你真的需要 claim 的機密性,用 JWE(JSON Web Encryption)。
一個緊湊 JWE 有五段 Base64url:
protected-header.encrypted-key.iv.ciphertext.authentication-tag
這些段仍是 Base64url 文字。機密性來自 JWE 的加密演算法和收件人的金鑰,而不是 Base64url 本身。
在許多 Web 應用裡,更簡單的設計比較好:把敏感狀態留在伺服器,只給瀏覽器一個短的隨機 Session 識別碼。不是每個可讀的 JWT 都要變成 JWE。
常見問題
Base64 是加密嗎?
不是。Base64 是沒有金鑰的可逆編碼。任何拿到那串字串的人,都能用瀏覽器函式、終端機命令、線上工具或幾行程式碼解開。
Base64 至少算混淆吧?
只在最弱的意義上算。它也許能擋住隨手一瞥,但擋不住開發者、攻擊者、掃描器、瀏覽器擴充功能、日誌閱讀者與技術支援工具。
Base64 對密碼或 API 金鑰安全嗎?
不安全。密碼需要 Argon2id、bcrypt、scrypt 這類慢速密碼雜湊。API 金鑰應該留在伺服器端,或放在具備 scope、可稽核性與輪換能力的機密管理服務。
大家都能讀 JWT 載荷嗎?
普通的簽名 JWT 是的。標頭與載荷是以 Base64url 編碼的 JSON。簽章在驗證之後能證明完整性,但並不隱藏 claim。
既然不安全,Basic 驗證為什麼要用 Base64?
Basic 驗證用 Base64 是為了讓 username:password 塞進 HTTP 標頭。傳輸層加密由 HTTPS/TLS 負責。沒有 HTTPS,Basic 驗證的憑據在網路上基本就是明文。
Base64url 比 Base64 更安全嗎?
不。Base64url 把 + 與 / 換成 - 與 _,也常常省去填補,讓值可以在 URL 與 JWT 裡使用。它仍然是可逆編碼。
那要加密的話該用什麼代替 Base64?
用經過審查的認證加密設計,例如 AES-GCM、XChaCha20-Poly1305 或 JWE,加上妥當的金鑰生成、金鑰儲存、輪換與存取控制。不要自造密碼學格式。
那 Base64 到底是幹嘛的?
Base64 把位元組表達成文字:JSON 的二進位欄位、電子郵件附件、data URI、PEM 檔、簽章、密文的格式化,以及 URL 安全的 token 段。它是包裝層,不是安全邊界。
在瀏覽器裡編碼與解碼
現在就想看看某個 Base64 字串?fixjson.org 上的 Base64 Encode & Decode 可以在瀏覽器裡本地編碼或解碼文字,包含 UTF-8 內容。用它來看 JWT 載荷、API 回應欄位或 data: URI,但別把解碼當作驗證或解密。
相關工具與指南
- Base64 Encode & Decode — 在瀏覽器裡本地編碼與解碼 Base64。
- 解碼 Base64 字串與 JWT 載荷 — Base64 與 Base64url 的實用解碼。
- 如何解碼 JWT — 讀 claim,並理解為何解碼不是驗證。
- 敏感 JSON 與本地工具 — 判斷什麼內容可以放心貼進瀏覽器工具。
- JSON Stringify — 檢視並跳脫巢狀 JSON 字串。
- RFC 4648:Base64 標準 — Base64 與 Base64url 的正式背景。
- RFC 7519:JSON Web Token — JWT 結構與安全注意事項。
參考
- RFC 4648 — Base64、Base64url、填補、字母表與安全注意事項。
- RFC 7617 — HTTP Basic 驗證與其 Base64 憑據格式。
- RFC 7519 — JWT 結構、信任決策與隱私考量。
- RFC 7516 — JSON Web Encryption 與緊湊 JWE 序列化。
- MDN btoa 與 MDN atob — 瀏覽器 Base64 原語與 UTF-8 注意點。
- OWASP Cryptographic Storage Cheat Sheet — 加密、認證模式與金鑰管理。
- OWASP Password Storage Cheat Sheet — 密碼雜湊指南。
最後審閱:2026 年 7 月。