Nano ID 是一段 21 個字元的隨機字串,取自一組固定的 64 字元字母表,內含大小寫英文字母、數字、底線與連字號,預設帶有 126 位元的熵。所謂「產生」一個 Nano ID,指的是在本機使用密碼學等級的隨機性,產生一段符合這種格式的字串,這也是為什麼「產生 nanoid」這個需求,會是建立 URL 友善識別碼時很常見的一項工作。21 個字元這個預設長度,是官方 Nano ID 專案設定的;每個字元貢獻六位元,因為這組 64 字元的字母表,正好可以完整放進六個二進位數字中,因此預設的空間大小遠超過 10 的 37 次方。要產生一批 ID,你只需要選擇一個介於 1 到 128 之間的長度,以及一個介於 1 到 100 之間的數量,瀏覽器就會透過 window.crypto.getRandomValues 取得位元組,並使用拒絕抽樣法,把這些位元組對應到字母表上,不會偏袒任何一個符號。結果會是一份以換行分隔的字串清單,你可以把它貼進程式碼、資料庫種子資料,或設定檔中。整個操作都留在目前的分頁裡完成,因此不論是識別碼、數量、長度、剪貼簿內容、隨機位元組,或使用事件,都不會離開這個頁面。

Nano ID 字串的組成結構
Nano ID 規範維護在官方儲存庫中,固定使用一組 64 個可列印 ASCII 字元的字母表,預設長度為 21。之所以選擇這個組合,是因為每一個索引值都剛好放得進六個位元,因此字母表能乾淨地對應到隨機位元組的範圍,而這個預設長度則帶有 126 位元的熵。這組字母表刻意避開斜線、加號、等號、空白字元,以及其他在 URL 路徑或查詢字串中經常需要做百分號編碼的字元。Nano ID 並不是 UUID,也不宣稱相容於 RFC 4122;它是自成一格的精簡格式,有自己的一套碰撞機率計算方式。它不會內嵌時間戳記、計數器、地區代碼,或記錄類型,因此你無法依建立時間排序,也無法從字元本身推斷出任何商業意義。
這種設計帶來兩個實際影響。第一,Nano ID 很適合用在那些 UUID 顯得笨重的場合:短網址、對外公開的分享權杖,以及本身就已經帶有時間戳記字首的日誌行。第二,因為沒有內嵌排序資訊,兩個相隔僅一毫秒產生的 ID,並無法透過字典順序比較,來判斷哪一個先產生。如果你的應用程式需要離線的時間先後排序,或需要可解析的時間戳記,像 ULID 或 UUID v7 這類具有時間感知能力的格式,會是更合適的選擇,因為 Nano ID 從一開始就不是為了承載這類資訊而設計的。
如何產生一批 Nano ID
在你的瀏覽器中開啟Nano ID 產生器,並依照這個工具內部強制執行的同一套三步驟流程進行。
- 選擇一個介於 1 到 128 之間的 ID 長度,以及一個介於 1 到 100 之間的數量。預設長度 21 符合官方 Nano ID API 的規範,除非你有經過實測的理由需要偏離,否則這是最安全的起點。
- 產生這批 ID。這個工具會透過 window.crypto.getRandomValues 取得位元組,套用拒絕抽樣法比對計算出來的位元遮罩,再從官方的 64 字元字母表中組出每一個字串。
- 複製這份以換行分隔的 ID 清單,貼到需要用到它們的地方,接著在你自己持久保存資料的儲存系統中再次強制執行唯一性檢查,而不要只信任這個產生器本身。
如果這一批 ID 中出現重複(在統計上極不可能,但並非完全不可能),這個工具會回報錯誤,而不是悄悄回傳那個發生碰撞的字串。這項防護機制只針對同一批次生效;它並不會與其他分頁、裝置、行程、部署,或先前作業階段中產生的 ID 互相協調,這也是為什麼第三步要提醒你回到資料庫層去做把關,而不是單靠這個頁面。
Web Crypto 與拒絕抽樣法如何塑造輸出結果
Nano ID 的字元分布,取決於提供給它的隨機來源品質。Nano ID 產生器使用的是W3C Web Cryptography API 原生方法window.crypto.getRandomValues,這是瀏覽器官方認可、用來取得密碼學等級強隨機位元組的來源。它不會呼叫 Math.random,也不會從時間戳記、計數器、瀏覽器指紋,或某個確定性種子推導出數值。這個區別,在識別碼必須難以預測的場合中很重要,但這並不代表一個識別碼因此就能被當成驗證、授權、加密,或密鑰管理系統來使用;存取控制必須獨立於這個 ID 看起來是否隨機。
取得位元組只是問題的一半。另一半,是要把這些位元組無偏地對應到 64 字元字母表上。天真的作法,是把一個隨機位元組對字母表長度取模,但只要位元組的範圍無法被字母表大小整除,這種做法就會產生不均勻的分布。官方的 Nano ID 演算法,用拒絕抽樣法避開了這個問題:它會計算出一個位元遮罩,剛好涵蓋字母表範圍內的所有數值,持續抽取位元組,直到每一個索引值都落在這個範圍內為止,其餘的則捨棄不用。由於預設的字母表正好有 64 個符號,其索引值剛好放得進六個位元,這個遮罩正好對齊整數個位元組,因此在實務上很少會發生拒絕重抽的情形。測試會鎖定具代表性的字母表索引,以及每一種尺寸與數量的邊界值,確保這個實作不會出現偏差。
選擇長度以及它的代價
長度決定了這個識別碼隨機空間的大小。在 64 字元的字母表下,每個字元代表六個位元,因此可用空間會隨長度增加而呈指數成長。把預設長度縮短,等於是用熵去換取更短的字串,而這個代價是指數級的。下表整理了長度、總位元數,以及最終空間大略規模之間的關係。
| 長度 | 熵(位元) | 大約空間大小 |
|---|---|---|
| 10 | 60 | 約 1.15 × 1018 |
| 16 | 96 | 約 7.92 × 1028 |
| 21(預設值) | 126 | 約 8.51 × 1037 |
| 32 | 192 | 約 6.28 × 1057 |
| 64 | 384 | 約 3.94 × 10115 |
這些數字都假設符號的選取是均勻分布的,而拒絕抽樣法正是讓這項假設成立的關鍵。適合你專案的長度,取決於預期的發行量、可接受的碰撞風險、重試機制的行為、資料儲存的存續時間,以及暴露面遭到惡意利用的可能性有多高。一個會永久存在、對外公開的分享連結,所需要的熵,會比一個帶有唯一性約束與重試機制的內部資料列識別碼更多。這個頁面並不保證你選擇的任何長度都不會發生碰撞,因為沒有任何有限長度的隨機識別碼能做到這一點。
把產生出來的 ID 整合進真正的應用程式中
把這批 ID 貼進你的程式碼庫,做法就跟處理其他任何不透明的識別碼一樣,接著再加上這個產生器本身無法提供的安全防護網。最簡單可靠的模式,大致如下。
- 除非有經過實測的需求證明有理由使用不同長度,否則請保留 21 個字元的預設長度;更短並不代表更安全,比預設值更短的 ID,會隨著整個集合的成長而提高碰撞機率。
- 把這個值儲存在一個能保留大小寫,以及兩種標點符號的欄位中。不區分大小寫的定序規則,會悄悄讓兩個僅大小寫不同的相異 ID 產生碰撞;而一個會移除連字號或底線的欄位,則會破壞這組字母表。
- 在資料庫層加上唯一性約束。把這項約束當成判定唯一性的最終權威依據,而不是依賴這個產生器在頁面內部所做的任何檢查。
- 只針對失敗的寫入動作進行原子性重試,而不要採用「先檢查、再寫入」的方式。「先檢查再寫入」的模式會與並行的寫入動作互相競速,而這正是重複值悄悄混進正式環境的常見原因。
- 測試每一條會處理這個 ID 的路由、日誌處理程序、數據分析管線、CSV 匯出流程,以及資料庫定序規則,因為每一處都有可能截斷字串、轉成小寫,或移除某個標點符號。
以上建議,跟讓 UUID 在正式環境中保持健康所需要的做法完全一樣。Nano ID 雖然比較短,但圍繞著它的操作紀律,跟任何不透明識別碼並無二致,而且較短的字串長度,反而會讓草率的日誌記錄或傳輸處理問題更容易被看出來,因為一旦出錯,字母表中就會留下明顯的缺口。
Nano ID 產生器在設計上刻意維持純用戶端運作,這讓它很適合用在受到嚴格資料落地規範限制的程式碼中。瀏覽器擴充功能、遭入侵的腳本,以及遭入侵的裝置,仍然有可能觀察到頁面上的資料,因此請不要在這裡產生密碼、API 金鑰、復原代碼、簽章材料,或其他高價值的機密資訊。這類工作,請改用目標平台專屬的憑證產生器與機密管理系統來處理,把這個工具保留給它原本被設計用來產生的那種 ID 就好。