指南:Web Crypto API、AES-GCM與密鑰管理)
在Web開發(fā)這件事上談到JavaScript密碼算法與網絡安全我聽到最多的質疑就是“前端加密有什么意義數據反正會到后端后端驗不就行了”這個想法不能算錯但它忽略了一大類真實存在的場景。比如你的應用需要校驗第三方回調的數據有沒有被中途篡改需要在不暴露明文的情況下把敏感字段傳給數據分析平臺甚至需要直接在瀏覽器端做文件的分塊加密上傳讓服務器只保存密文。這些需求靠純粹的傳輸層加密解決不了必須真正把JavaScript密碼算法用起來。這篇內容不是教科書式的概念羅列而是我在幾個真實項目里蹚過坑之后的總結包含瀏覽器原生Web Crypto API的完整實操、密鑰管理的取舍、以及一系列調試經驗。適合正在做Web安全改造、加密存儲、數據防篡改需求的開發(fā)者也適合那些拿到需求卻不知道從哪兒下手的同學。我會盡量把每一步為什么這么做講清楚你可以直接拿去復用。1. 密碼學基礎為什么前端加密讓我又愛又恨1.1 對稱加密與非對稱加密的核心邏輯先把最基礎的東西說透。對稱加密和非對稱加密名字聽著唬人本質區(qū)別就一句話加密和解密用的密鑰是不是同一個。對稱加密典型代表是AES加密和解密用同一把鑰匙。就好比你家的門鎖用鑰匙鎖上門也用同一把鑰匙打開。它的優(yōu)點是速度快、算力開銷小適合加密大量數據。缺點也很致命鑰匙怎么送到對方手里如果兩個人通過網絡通信總得先把這把密鑰傳過去傳輸過程中一旦被截獲后面所有密文都等于裸奔。非對稱加密典型代表是RSA和橢圓曲線ECC它用一對鑰匙公鑰和私鑰。公鑰可以公開誰都能拿私鑰自己保管絕不外傳。這個設計更像街邊的公共信箱——任何人都能把信投進信箱用公鑰加密但只有握著信箱鑰匙的人能打開用私鑰解密。非對稱加密解決了密鑰分發(fā)問題但性能比對稱加密慢幾個數量級不適合直接加密大段數據。所以在真實項目里兩者從來不是二選一而是搭配使用用非對稱加密協(xié)商出一把臨時對稱密鑰再用這把對稱密鑰加密業(yè)務數據。這也就是TLS握手的核心思路之一。前端場景里如果你不需要跟別人協(xié)商密鑰更多時候直接使用對稱加密就夠了密鑰從用戶口令派生根本不落到服務器上。1.2 瀏覽器端的威脅模型加密到底保護了什么很多開發(fā)者對前端加密的最大誤解是拿它當HTTPS的替代品這是大忌。HTTPS解決的是傳輸鏈路被竊聽和篡改的問題它保證的是“數據從瀏覽器到服務器之間沒人看得懂”而JavaScript密碼算法解決的是“數據在業(yè)務層落庫、傳遞、校驗”時的安全問題。兩者是不同層級的防護不是二選一的關系。舉個我實際見過的例子某個系統(tǒng)的網關會記錄請求日志用于排障但日志里包含用戶身份證號等敏感字段。即使整個鏈路是HTTPS日志一旦落到存儲系統(tǒng)就變成了明文數據庫安全審計直接不過關。后來改成前端先用AES加密敏感字段后端只把密文交給日志系統(tǒng)日志查詢時再解密。這就是應用層加密的真實價值。再比如一個云筆記應用用戶寫的內容同步到服務器。如果服務器本身可以解密數據那運維人員、被拖庫時攻擊者都能看到全部筆記。這時候就需要端到端加密前端加密后上傳服務器只負責存儲密文解密永遠發(fā)生在用戶自己的瀏覽器里。這類需求單純指望HTTPS和服務器端防護是不夠的。我把前端加密的真實作用總結成三件事第一數據完整性校驗確保密文或明文在傳輸、存儲過程中沒有被篡改第二敏感字段的端到端加密讓中間環(huán)節(jié)看不到明文第三防重放和防偽造比如用HMAC驗證回調請求確實來自受信任方。想清楚這三件事你就不會被“前端加密是否安全”這類問題困住。1.3 哈希、消息認證碼與加密三者千萬別混用在講代碼之前必須把哈希、消息認證碼和加密這三個概念掰開。我見過太多項目把SHA256叫“加密”實際上SHA256算出來的摘要根本解不回去。哈希函數是單項通道它把你的一段數據映射成固定長度的指紋數據稍微改一個bit指紋就面目全非。哈希的作用是校驗完整性不是保密。消息認證碼MAC和哈希的最大區(qū)別是它需要一把密鑰。哈希誰都能算MAC只有持有密鑰的人才能算得出來。所以在第三方回調驗簽、API防篡改這些場景里用HMAC而不是普通哈希攻擊者沒有密鑰偽造不了正確的校驗值。加密則是可逆的它能保護機密性讓明文變成看不懂的密文接收方用密鑰還原。哈希、MAC、加密三者的定位分別是完整性、認證性、機密性。真正項目里往往是組合拳先用哈希或MAC保證數據沒被改過再用加密讓數據不被別人看到。腦子里有了這個框架后面看代碼就不會暈。有一個很直觀的類比哈希像驗毒試紙只能告訴你這包藥材有沒有被動過MAC像是在試紙上再加一層私人暗號只有你和對方知道暗號規(guī)則加密則是把藥材整個鎖進保險箱鎖是密碼鎖沒有密碼連看都看不到。三件事互相獨立但經常一起用。2. JavaScript加密實戰(zhàn)瀏覽器原生API的正確打開方式2.1 Web Crypto API為什么原生實現值得信賴先亮明態(tài)度新項目做JavaScript密碼算法優(yōu)先用瀏覽器原生的Web Crypto API而不是各種第三方加密庫。理由很簡單原生API是瀏覽器內核實現的運行時有硬件加速密鑰材料在底層做管理JavaScript環(huán)境本身接觸不到完整的密鑰細節(jié)從源頭上減少被XSS腳本竊取密鑰的風險。第三方純JS庫雖然方便但很多實現存在歷史包袱。Web Crypto API的核心對象是crypto.subtle提供摘要、加解密、簽名、密鑰派生等能力。注意subtle這個名字官方文檔說得很直白這是一套強大的低級API越強大越容易用錯所以把“subtle”寫進了命名里。先來一個最基礎的哈希操作對字符串算SHA256摘要async function sha256(input) { const data new TextEncoder().encode(input); const digestBuffer await crypto.subtle.digest(SHA-256, data); const hashArray Array.from(new Uint8Array(digestBuffer)); return hashArray.map(b b.toString(16).padStart(2, 0)).join(); }這段代碼有兩個新手常犯的坑。第一digest方法不直接接受字符串必須先把字符串編碼成Uint8Array我用的是TextEncoder。第二crypto.subtle.digest返回的是ArrayBuffer不能直接打印需要轉成Uint8Array再逐字節(jié)轉十六進制字符串。很多人天真的console.log(digest)打出來永遠是[object ArrayBuffer]還以為是自己寫錯了。哈希用的算法名是SHA-256這里也可以用SHA-1和SHA-384但SHA-1已經不安全除非做兼容老系統(tǒng)的完整性校驗否則一律不要用。新項目無腦選SHA-256。2.2 AES-GCM對稱加密實戰(zhàn)封裝瀏覽器端做對稱加密我的首選永遠是AES-GCM。原因有三個一是GCM模式同時提供加密和完整性認證既保證了機密性又防止密文被篡改二是Web Crypto API原生支持不需要額外引入庫三是一個模式解決兩件事代碼結構更簡單。AES-GCM有兩個關鍵參數密鑰和IV初始化向量。IV不需要保密但絕對不能重復。同一把密鑰下如果IV重復攻擊者可以通過密文的異或關系還原出明文信息。我用crypto.getRandomValues生成12字節(jié)的隨機IV這是一個比較標準的做法12字節(jié)是GCM模式推薦的長度太長或太短都會帶來額外的兼容性問題。下面是一組可以直接復用的封裝加密函數返回{ iv, ciphertext }async function aesGcmEncrypt(secretKey, plaintext, additionalData) { const iv crypto.getRandomValues(new Uint8Array(12)); const encrypted await crypto.subtle.encrypt( { name: AES-GCM, iv: iv, tagLength: 128, additionalData: additionalData }, secretKey, new TextEncoder().encode(plaintext) ); return { iv: iv, ciphertext: new Uint8Array(encrypted) }; } async function aesGcmDecrypt(secretKey, iv, ciphertext, additionalData) { const decrypted await crypto.subtle.decrypt( { name: AES-GCM, iv: iv, tagLength: 128, additionalData: additionalData }, secretKey, ciphertext ); return new TextDecoder().decode(decrypted); }這中間有個很容易被忽略的細節(jié)additionalDataAAD。它是什么它是一段不加密但要參與認證的數據典型用法是綁定上下文。比如你加密一條消息可以把消息的ID、發(fā)送人、版本號放進AAD這樣如果攻擊者把一條合法密文換個ID插到別處解密時AAD校驗不過立刻失敗。等于給密文加了一層“防移花接木”的保險。crypto.subtle.encrypt返回的密文里已經包含了GCM的認證標簽不需要你再單獨拼接tag解密時也是自動校驗。如果密文被篡改decrypt會直接拋OperationError異常。這一點對排查問題很重要下面第4章還會展開講。2.3 PBKDF2與口令派生密碼存儲的正確姿勢接下來是另一個高頻場景用戶密碼。很多人拿到需求“密碼加密存數據庫”直接對密碼算一個SHA256就存進去這是巨大的錯誤。SHA256速度太快攻擊者拿到數據庫后用GPU暴力跑幾億次哈希毫無壓力弱口令一跑一個準。正確的做法是什么要么用專門的慢哈希算法比如bcrypt、scrypt、Argon2要么用密鑰派生函數PBKDF2把密碼加上隨機鹽經過成千上萬次迭代派生出定長密鑰或哈希值。慢哈希的本質是讓每次猜測的成本變得很高從而提高暴力破解的代價。在瀏覽器端Web Crypto API支持PBKDF2。下面是一個加鹽派生的完整示例async function deriveKeyFromPassword(password, salt, iterations 310000) { const baseKey await crypto.subtle.importKey( raw, new TextEncoder().encode(password), PBKDF2, false, [deriveBits] ); const derivedBits await crypto.subtle.deriveBits( { name: PBKDF2, salt: salt, iterations: iterations, hash: SHA-256 }, baseKey, 256 ); return new Uint8Array(derivedBits); } // 調用示例 const salt crypto.getRandomValues(new Uint8Array(16)); const derivedKey await deriveKeyFromPassword(用戶密碼, salt);鹽的作用是防預計算攻擊和彩虹表。即使兩個用戶設置相同密碼只要鹽不同最終派生結果也不同。鹽必須是隨機生成、長度足夠16字節(jié)以上并且可以明文存儲不需要保密。迭代次數決定了慢哈希的成本安全社區(qū)目前的共識是PBKDF2-HMAC-SHA256至少30萬次迭代硬件環(huán)境允許的話做到60萬次也不過分。在低端移動設備上迭代次數太高的體驗會明顯變慢需要根據自己的用戶設備畫像平衡。這里我必須強調一個邊界瀏覽器端運行PBKDF2主要是為了“客戶端派生密鑰”比如做端到端加密時從用戶口令生成本地密鑰口令永遠不離開設備。而真正的密碼庫校驗依然強烈建議放在服務器端用專門的密碼哈希方案處理。如果只是在瀏覽器里算個PBKDF2然后當密碼哈希存到數據庫那就失去了服務端慢哈希的專業(yè)算法優(yōu)勢安全強度反而打折。3. 真實項目落地一個加密同步方案的完整拆解3.1 場景設定與整體架構理論講再多不落地等于零。我拿一個做過多次的方案來拆解云筆記的加密同步。需求很簡單筆記內容要加密后上傳到服務器服務器只存密文即使數據庫被拖走攻擊者拿到的也是一堆無法解讀的字節(jié)。這里采用的架構是“密鑰層級”思想。直接用用戶口令加密所有筆記并不可行因為任意一條筆記更新時都要重新派生密鑰而且口令一旦修改所有數據都要重加密。更合理的方案是引入兩層密鑰數據加密密鑰DEK為每一條筆記隨機生成真正用于AES-GCM加密筆記內容。密鑰加密密鑰KEK由用戶口令加鹽通過PBKDF2派生用于加密DEK。整體流程是用戶輸入口令瀏覽器本地派生出KEK每新建一條筆記生成隨機DEK加密筆記內容DEK本身再用KEK加密后和筆記密文一起上傳服務器。用戶查看筆記時先用口令派生KEK解包出DEK再用DEK解密筆記。服務器全程只接觸密文和包裹后的密鑰沒有能力還原明文。這個架構帶來一個直接收益用戶修改口令時只需要用新KEK重新包裹所有DEK不需要把幾十萬條筆記重新加密一遍。操作成本從“重寫全庫”降為“重寫密鑰表”量級完全不同。3.2 關鍵實現與代碼走讀密鑰包裝可以借助Web Crypto API的wrapKey和unwrapKey原生支持AES-GCM作為包裝算法。下面是核心代碼邏輯。生成一條新筆記的加密過程// 第一步生成隨機DEKAES-GCM 256位 const dataKey await crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt, decrypt] ); // 第二步用DEK加密筆記內容 const iv crypto.getRandomValues(new Uint8Array(12)); const ciphertext await crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, dataKey, new TextEncoder().encode(plaintext) ); // 第三步用KEK包裹DEK const wrappedKey await crypto.subtle.wrapKey( raw, dataKey, kek, { name: AES-GCM, iv: iv } );讀取筆記時的解包過程// 第一步從服務器拿到包裹后的密鑰 // 第二步用KEK解包DEK const unwrappedKey await crypto.subtle.unwrapKey( raw, wrappedKeyBuffer, kek, { name: AES-GCM, iv: iv }, { name: AES-GCM, length: 256 }, true, [encrypt, decrypt] ); // 第三步用DEK解密筆記 const decrypted await crypto.subtle.decrypt( { name: AES-GCM, iv: iv }, unwrappedKey, ciphertext );注意到一個問題沒有包裹DEK時用了和加密筆記相同的iv。這里要特別小心如果完全復用同一個DEK和同一個IV加密兩份數據是災難性的。但在密鑰包裹場景中包裹DEK用的是KEK加密筆記用的是DEK兩把密鑰不同復用IV并不會造成安全風險。不過為了保險起見我建議還是分別為wrapKey和encrypt生成獨立的IV避免后續(xù)有人重構代碼時混淆密鑰上下文。實現這個方案時DEK是否標記為extractable也是關鍵決策。包裹密鑰之后本來可以把DEK設置為false即不可導出這樣JavaScript環(huán)境根本無法取出DEK的原始字節(jié)只能用它加解密。但在實際存儲場景中一旦KEK丟失或用戶忘記口令不可導出的DEK意味著數據徹底無法恢復??紤]到用戶找回口令的需求我通常會保留extractable: true但伴隨更嚴格的口令策略和提醒機制讓用戶知道忘記口令等于放棄數據。3.3 密鑰管理與輪換的教訓說到密鑰管理我在真實項目里踩過不少坑這里集中說幾個。第一不要為了方便把密鑰存進localStorage。localStorage對同源下的所有腳本開放一個XSS漏洞就能把密鑰偷走。哪怕你把密鑰先做一層編碼再存也只是掩耳盜鈴攻擊者只需要照貓畫虎解碼一次。密鑰的最好歸宿是“不落盤”每次使用前由口令重新派生。第二IndexedDB比localStorage更適合存密鑰主要原因不是安全性而是它支持結構化克隆和更大的數據容量可以存CryptoKey對象并且存取是異步的。但要注意IndexedDB對XSS同樣沒有免疫力它只是工程上更合理不代表密鑰就安全了。第三密鑰輪換。很多項目上線時做了加密卻完全沒有輪換機制。一旦懷疑密鑰泄露或者安全合規(guī)要求定期更換你總不能讓人改一次密鑰就重加密所有歷史數據。所以設計數據模型時就要預留密鑰版本字段。我現在的做法是在每條密文記錄里額外存一個keyVersion字段。輪換時生成新的KEK批量解包-重新包裹DEK更新版本號。解密時根據記錄的版本號選擇對應的KEK歷史數據平滑過渡。整個過程用戶無感知數據量也不需要全量重加密這是密鑰層級架構另一個很大的優(yōu)勢。第四永遠不要在前端日志里打印密鑰或口令。調試時開發(fā)者也難免手滑把console.log(wrappedKey)留在線上代碼里。我在代碼規(guī)范里直接加了要求涉及密鑰對象的日志一律使用console.log([key] omitted)避免思維方式上就養(yǎng)出隨手打的習慣。4. 常見問題與排查技巧實錄4.1 安全上下文與兼容性為什么我的crypto.subtle是undefinedWeb Crypto API有一個硬性條件必須在安全上下文中可用。所謂安全上下文簡單說就是HTTPS頁面或localhost環(huán)境。如果你直接在某個HTTP內網地址打開頁面window.crypto.subtle大概率是undefined所有加密代碼都會報錯。排查這個問題最快的方式是看一眼瀏覽器控制臺錯誤。如果是“Cannot read properties of undefined (reading digest)”先確認是不是在HTTP環(huán)境。開發(fā)階段建議用localhost進行聯(lián)調因為瀏覽器把localhost視為安全上下文不需要配HTTPS證書。生產環(huán)境則必須上HTTPS這本身也是Web安全的基本要求。Node.js環(huán)境下情況略有不同。Node.js 20開始全局的crypto對象默認包含subtle可以直接使用更早版本需要從require(crypto).webcrypto取。前后端共用同一套Web Crypto API代碼是可行的但要注意兩端默認支持細節(jié)存在差異比如某些算法名、哈希算法清單可能不一致。兼容性方面還有一個必須提的坑老版本瀏覽器IE時代遺產比如某些企業(yè)瀏覽器內核根本不支持crypto.subtle。如果產品明確需要支持這類環(huán)境要么引入降級方案要么干脆在功能設計上做分流不要試圖用同一個代碼路徑硬兼容否則你會被一串又一串的環(huán)境判斷折磨到崩潰。4.2 二進制數據的編碼轉換ArrayBuffer、Uint8Array與Base64做密碼學開發(fā)二進制數據的編碼轉換幾乎是每天都要處理的事。crypto.subtle的輸入輸出全是ArrayBuffer和Uint8Array但網絡傳輸、數據庫存儲、URL傳參都要求字符串或Base64。很多新手在這里翻車我見到最多的報錯是“byte length mismatch”或“invalid key length”仔細一查全是編碼沒轉對。下面這組工具函數我?guī)缀趺總€項目都會粘一份function bytesToHex(bytes) { return Array.from(new Uint8Array(bytes)) .map(b b.toString(16).padStart(2, 0)) .join(); } function base64ToBytes(base64) { const binaryString atob(base64); const bytes new Uint8Array(binaryString.length); for (let i 0; i binaryString.length; i) { bytes[i] binaryString.charCodeAt(i); } return bytes; } function bytesToBase64(bytes) { let binary ; const uint8 new Uint8Array(bytes); for (let i 0; i uint8.length; i) { binary String.fromCharCode(uint8[i]); } return btoa(binary); }這里有一個容易懵的點Uint8Array和ArrayBuffer是兩種不同的東西。Uint8Array本質是一個“視圖”它里面有一個buffer屬性指向底層的ArrayBuffer。你如果傳給crypto.subtle一個從ArrayBuffer上切出來的Uint8Array通常沒問題因為Web Crypto API接受BufferSource也就是它倆都行。但當你需要精確控制長度時務必用byteLength而不是length因為Uint8Array的長度可能只是底層ArrayBuffer的一部分。另一個和服務器通信時的典型問題后端返回Base64字符串前端直接JSON.parse解出字符串后忘了轉回字節(jié)數組就傳給crypto.subtle.importKey結果密鑰導入報錯。處理JSON傳輸時密鑰和密文一律用Base64字符串承載拿到前端先轉字節(jié)再參與運算。4.3 AES-GCM解密失敗逐層排查清單AES-GCM解密失敗拋OperationError這是最典型的加密報錯。我把自己總結的排查思路列成清單按下面的順序查大多數問題五分鐘內能定位。第一檢查IV是否和解密時一致。很多人只保存了密文忘記保存IV。AES-GCM解密必須使用加密時的同一個IV否則GCM認證必然失敗。所以存儲時一定要把iv和ciphertext放在一起保存甚至可以把IV直接拼在密文前面然后統(tǒng)一轉為Base64?,F象最可能原因處理建議解密報OperationError密文被篡改、IV不對、AAD不一致逐項比對密文、IV、AAD密鑰導入失敗密鑰字節(jié)長度不對或編碼錯誤確認Base64轉字節(jié)無殘留、長度符合算法要求頁面打開即報錯crypto.subtle不可用檢查HTTPS、localhost、瀏覽器版本服務器與前端互通失敗編碼規(guī)則不一致比如前端hex、后端base64統(tǒng)一編碼格式建立轉換工具函數數據偶爾解密失敗并行加密時IV生成沖突或隨機源退化確認用crypto.getRandomValues避免自實現隨機數第二確認IV字節(jié)數是否為12字節(jié)。GCM標準推薦12字節(jié)太長在某些實現里要額外做IV哈希處理容易出現兼容性問題。我見過有人用了16字節(jié)IVNode.js 和瀏覽器某一邊能解、另一邊不行查到最后是IV長度處理規(guī)則不一致。第三檢查additionalData。如果你在加密時傳了AAD解密時也必須傳一模一樣的AAD順序和字節(jié)都不能錯。這里最坑的是AAD在傳輸過程中如果被序列化成了不一樣的編碼比如字符串編碼從UTF-8變成UTF-16解密也會失敗。第四確認導出導入密鑰時沒有改變字節(jié)順序。密鑰的raw導出直接對應字節(jié)序列不應該有任何處理。有些代碼為了顯示好看把密鑰轉成hex字符串再次解碼中間一旦出現大小寫處理誤差密鑰就廢了。4.4 大數據量加密的性能取舍前面講的所有示例都是對小數據量直接加密但真正做文件同步、圖片加密這類需求時直接把整個文件塞進crypto.subtle.encrypt會讓內存瞬間爆炸。瀏覽器里加密一個幾百MB的文件你幾乎能聽到風扇狂轉的聲音。標準做法是分塊加密把文件切成固定大小的分塊比如1MB一塊依次加密后傳給服務器。但GCM模式有分塊安全邊界分塊太小會擴大密文膨脹率分塊太大則內存占用高。我實測下來1MB到4MB是一個比較舒服的區(qū)間密文膨脹率約為密文長度的0.8%GCM各分塊都要加一個認證標簽通常16字節(jié)在可接受范圍內。分塊加密時每個分塊必須使用獨立的IV并且不能以任何可預測的方式遞增。最穩(wěn)妥的是每塊都用crypto.getRandomValues生成隨機IV同時把分塊序號、IV、密文長度作為存儲元數據保存。注意分塊序號的意義它防止攻擊者刪除或調換分塊順序結合每個分塊的GCM認證標簽可以有效保證數據的完整性和順序。const CHUNK_SIZE 1024 * 1024; // 1MB async function encryptFile(file, key) { const chunks []; for (let offset 0; offset file.size; offset CHUNK_SIZE) { const slice file.slice(offset, offset CHUNK_SIZE); const buf await slice.arrayBuffer(); const iv crypto.getRandomValues(new Uint8Array(12)); const enc await crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, key, buf ); chunks.push({ index: chunks.length, iv: bytesToBase64(iv), data: bytesToBase64(new Uint8Array(enc)) }); } return chunks; }在這個例子里我先對每個分塊獨立加密再把分塊元數據傳給服務器。如果不做Web Worker主線程中循環(huán)加密1MB分塊會讓頁面出現明顯的卡頓。更合理的方式是把加密任務扔進Web Worker把主線程騰出來給用戶交互。我在項目中通常會創(chuàng)建一個專門的加密Worker接收ArrayBuffer返回加密結果主線程只維護任務狀態(tài)和進度條。整體體驗能從“點擊保存后頁面凍結三秒”變成“后臺默默加密、界面絲滑不減”。最后分享一個調用習慣多次調用crypto.subtle的異步方法時不要在循環(huán)里無腦await串行執(zhí)行逐個排隊會拖慢總時長。在任務粒度小、相互獨立的前提下可以考慮用Promise.all并發(fā)控制但要注意并發(fā)量一次性發(fā)幾十個加密任務同樣能把CPU踩滿。我的經驗是維護一個并發(fā)池同時運行4到6個加密任務既充分利用多核資源又不至于讓頁面失去響應。加密性能優(yōu)化本質上是個資源調度的活在瀏覽器這種單線程表象下需要多試幾種策略找到適合自己場景的平衡點。踩過幾次坑之后我現在做前端加密方案已經形成了一套固定動作先確認威脅模型想清楚要防誰再選算法能走Web Crypto API就不引第三方庫然后設計密鑰層級預計未來的輪換需求最后把編碼轉換工具函數寫好放在公共模塊里。這個流程讓我從“寫出一段能跑的加密代碼”進化到“做出一個幾年后依然經得起推敲的加密設計”中間省下來的排查時間非??捎^。如果你正好在做一個新的加密功能我建議先從第2章的AES-GCM封裝入手把它內置到項目工具庫里再逐步疊加PBKDF2派生、密鑰包裹和分塊加密。不要一開始就上大而全的加密框架瀏覽器密碼學最危險的地方恰恰是那些看起來很省事的封裝它們隱藏了太多細節(jié)而安全這件事最怕的就是細節(jié)失守。