Wybór identyfikatora: UUID, krótkie kody i te, które zdradzają informacje
Do wszystkiego, co wewnętrzne, używaj UUID v4, v7 wtedy, gdy identyfikator jest kluczem głównym dużej tabeli, a krótkiego kodu z ograniczonego alfabetu wtedy, gdy człowiek musi go odczytać na głos. Kolejne liczby całkowite to opcja, której należy unikać we wszystkim, co publiczne.
Wersje UUID, które mają znaczenie
UUID ma 128 bitów. Cztery niosą wersję, a dwa wariant, więc żadna wersja nie ma do dyspozycji pełnych 128.
| Wersja | Co zawiera | Kiedy jej używać |
|---|---|---|
| v1 | 60 bitowy znacznik czasu, 14 bitowa sekwencja zegara, 48 bitowy identyfikator węzła (adres MAC) | Tylko dane odziedziczone |
| v3 / v5 | Skrót MD5 albo SHA-1 z przestrzeni nazw i nazwy | Gdy potrzebne są identyfikatory deterministyczne |
| v4 | 122 losowe bity i nic więcej | Domyślnie, gdy niczego nie powinno dać się wywnioskować |
| v7 | 48 bitowy znacznik czasu w milisekundach, potem 74 losowe bity | Klucze, w których liczy się kolejność wstawiania |
v1 jest tą ostrzegawczą. Osadza czas utworzenia, a w większości implementacji także adres MAC maszyny, która go wytworzyła, i właśnie dzięki temu takie identyfikatory pomogły w 1999 roku namierzyć autora wirusa Melissa. v7, ustandaryzowany w RFC 9562 w 2024 roku, publikuje czas utworzenia celowo: to jednocześnie jego zaleta i jego koszt.
Kwestia kolizji
UUID v4 ma 122 losowe bity, więc takich wartości jest 2^122, mniej więcej 5,3 x 10^36. Znaczenie ma granica urodzinowa, a nie rozmiar przestrzeni: szansa, że dowolne dwie z n wartości okażą się takie same, wynosi mniej więcej n do kwadratu przez 2^123. Bilion wygenerowanych UUID niesie z sobą mniej więcej jedną na dziesięć bilionów szansę na duplikat, a szansa rzutu monetą wymaga około 2,7 x 10^18 wartości, czyli jakichś 85 lat przy miliardzie na sekundę.
"Nie jest to praktyczny problem" wciąż nie znaczy "niemożliwe", a luka nigdy nie leży w arytmetyce. Duplikaty biorą się z generatora: ze stałego ziarna, z maszyny wirtualnej sklonowanej razem z pulą entropii, z obrazu kontenera, który wozi z sobą zapisany stan generatora pseudolosowego. Unikalność należy do źródła losowości, a nie do formatu, więc zostaw na kolumnie ograniczenie unikalności.
Kolejność, lokalność indeksu i wyciek informacji
Identyfikatory losowe rozpraszają się. W indeksie B-drzewa każde wstawienie ląduje na losowej stronie liścia, więc zbiorem roboczym jest cały indeks, a nie jego prawa krawędź, spada trafność pamięci podręcznej, a strony się rozszczepiają. Klucz rosnący w czasie dokleja się w jednym miejscu i utrzymuje w cieple wciąż te same kilka stron. Właśnie po to istnieją v7 i ULID; ULID to 48 bitów znacznika czasu w milisekundach plus 80 bitów losowych, zapisane jako 26 sortowalnych znaków w base32 Crockforda.
Kosztem jest przewidywalność: identyfikator uporządkowany w czasie ogłasza, kiedy powstał jego rekord, więc garść takich identyfikatorów odsłania tempo rejestracji i godziny martwe. Jeśli to ma znaczenie, użyj v4 i zapłać cenę po stronie indeksu.
Kolejne liczby całkowite idą jeszcze dalej. /invoices/1041 mówi, że wystawiłeś mniej więcej tysiąc faktur, a dwa identyfikatory oddalone o tydzień zdradzają tempo wzrostu. Gorzej, atakujący może poprosić o 1040. Jeżeli serwer zwróci ten rekord dlatego, że istnieje, a wywołujący jest zalogowany, bez sprawdzenia, do kogo rekord należy, mamy do czynienia z naruszeniem kontroli dostępu, skatalogowanym jako niezabezpieczone bezpośrednie odwołanie do obiektu. Nieodgadywalny identyfikator nie jest kontrolą uprawnień, ale skutecznie powstrzymuje spacer po całej tabeli.
Slugi też są identyfikatorami i przy poprawnej budowie nie zdradzają niczego o skali: znaki diakrytyczne złożone, wielkość liter obniżona, interpunkcja sprowadzona do łączników, dokładnie tak, jak robi to generator slugów. Po publikacji utrzymuj slug niezmieniony; zmiana psuje odnośniki przychodzące.
Krótkie identyfikatory, alfabety i długość
Każdy znak losowego kodu jest wart log2 z rozmiaru alfabetu: 5,95 bita dla base62, 5 dla base32, 4 dla systemu szesnastkowego. Długość i alfabet ustalają prawdopodobieństwo kolizji przy zadanej liczbie identyfikatorów.
| Długość (base62) | Liczba wartości | Bity | Szansa kolizji w miliardzie identyfikatorów |
|---|---|---|---|
| 8 | 2,2 x 10^14 | 43,7 | Pewna |
| 10 | 8,4 x 10^17 | 59,5 | Około 45% |
| 12 | 3,2 x 10^21 | 71,5 | Około 1 na 6500 |
| 16 | 4,8 x 10^28 | 95,3 | Około 1 na 100 miliardów |
To przy założeniu, że każdy znak jest niezależnie losowy; przedrostek taki jak inv_ dodaje zero bitów.
Do wszystkiego, co czyta człowiek, base62 jest złym alfabetem. 0 i O, 1 oraz l i I to gotowe błędy przy przepisywaniu, a kod rozróżniający wielkość liter nie przeżyje rozmowy telefonicznej. Base32 Crockforda usuwa I, L, O i U oraz przyjmuje na wejściu dowolną wielkość liter, co jest warte jednego dodatkowego znaku.
Skąd biorą się losowe bity
Math.random() nie jest źródłem kryptograficznym. V8 implementuje je za pomocą xorshift128+, którego stan wewnętrzny da się odtworzyć z krótkiej serii wyników, po czym każda przyszła wartość jest przewidywalna. Nadaje się do tasowania w wersji demonstracyjnej albo do wypełniacza. Nie nadaje się do identyfikatora sesji, klucza API, odnośnika resetującego ani tokenu.
// Browsers, Node 19+, Deno and Bun
const id = crypto.randomUUID();
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
Gdzie indziej: crypto.randomBytes w Node, secrets.token_urlsafe w Pythonie, crypto/rand w Go, SecureRandom w Javie, random_bytes w PHP. Generator UUID wytwarza kryptograficznie losowe wartości v4, a generator haseł korzysta z tego samego API przeglądarki; generator liczb losowych służy do próbkowania, a nie do sekretów.
Jedna pułapka przy pisaniu własnego rozwiązania: odwzorowanie losowego bajtu na alfabet 62 znakowy za pomocą % 62 przechyla wynik w stronę pierwszych ośmiu znaków; użyj próbkowania z odrzucaniem.
Dane zastępcze i przykładowe
Lorem ipsum to poszatkowany Cyceron i przetrwał dlatego, że jest nieczytelny: tekst, który wygląda jak język, nie będąc językiem, pozwala ocenić układ po kształcie i długości wiersza, a nie po zdaniach, i właśnie to wytwarza generator Lorem Ipsum. Przed zatwierdzeniem podmień go na realistyczną treść, bo zastępcza łacina ukrywa przepełnienie, które wywoła prawdziwy nagłówek.
Zmyślone dane wymagają jednej zasady: nigdy nie wolno ich pomylić z prawdziwymi.
- Numery kart pochodzą z opublikowanego zakresu testowego dostawcy płatności albo wprost nie przechodzą kontroli Luhna. Losowy 16 cyfrowy numer, który przechodzi Luhna, należy do kogoś.
- Identyfikatory krajowe korzystają z zakresów zarezerwowanych. Amerykańskie numery Social Security zaczynające się od 000, 666 albo od 900 do 999 nigdy nie są wydawane.
- Używaj
example.comi domen pokrewnych, zarezerwowanych przez RFC 2606, oraz bloku dokumentacyjnego 192.0.2.0/24.
Dane testowe trafiają na produkcję częściej, niż się spodziewamy: skrypt zasilający wycelowany w niewłaściwą bazę, wypełniacz zostawiony w szablonie wiadomości wysłanej do całej listy. Fałszywe rekordy powinny wyglądać ewidentnie fałszywie, żeby człowiek wyłapał taki rekord, zanim wsparcie zacznie na nim działać.
Kody QR w skrócie
Kod QR to siatka modułów, wersje od 1 do 40, od 21x21 aż po 177x177. Długość ładunku wyznacza wersję, a przy stałym rozmiarze wydruku wyższa wersja oznacza mniejsze moduły, co wymaga lepszego druku i skanowania z bliższej odległości. Skróć adres URL, zanim podasz go generatorowi kodów QR, i pamiętaj, że tryb alfanumeryczny obsługuje wyłącznie wielkie litery, więc HTTPS://EXAMPLE.COM koduje się do mniejszego kodu niż postać zapisana małymi literami.
Korekcja błędów ma cztery poziomy, odzyskujące mniej więcej 7% (L), 15% (M), 25% (Q) i 30% (H) uszkodzonego kodu. Nadmiarowość kosztuje pojemność, więc wyższy poziom wpycha ten sam ładunek w gęstszą wersję. M jest zwykłą wartością domyślną; wybierz Q albo H przy małym druku, powierzchniach zakrzywionych albo logo na środku, które działa właśnie dlatego, że nadmiarowość je pochłania.
O tym, czy kod da się zeskanować, decydują dwa wymagania fizyczne. Strefa ciszy to czysty margines szerokości czterech modułów z każdej strony, po którym dekoder rozpoznaje granicę, więc kod dostawiony wprost do ramki albo zdjęcia często zawodzi. Kontrast musi być naprawdę ciemny na naprawdę jasnym: blady błękit na bieli to częsta przyczyna porażki, a wiele dekoderów odmawia odczytu kodów odwróconych.