UUID 的用户体验
唯一标识符(UUID)在所有应用程序中都起着至关重要的作用,从用户认证到资源管理。虽然使用标准的 UUID 可以满足所有的安全需求,但我们可以为用户做很多改进。
基础知识:确保全局唯一性
唯一标识符对于区分系统中的各个实体至关重要。它们提供了一种可靠的方法来确保每个项目、用户或数据具有唯一的身份。通过保持唯一性,应用程序可以有效地管理和组织信息,从而提高操作效率并促进数据完整性。
我们不需要像 Google 或 AWS 那样有特殊需求。任何具有 128 位的安全生成的 UUID 对我们来说已经足够了。有许多库可以生成 UUID,或者你可以使用你所选编程语言的标准库。在这篇博客中,我将使用 Java 示例,但基本思想适用于任何语言。
import java.util.UUID;
UUID id = UUID.randomUUID();
// 5727a4a4-9bba-41ae-b7fe-e69cf60bb0ab
到这里为止是一个选项,但让我们抓住机会,通过一些小而有效的迭代改进来提升用户体验:
- 使它们易于复制
- 添加前缀
- 更高效的编码
- 改变长度
复制 UUID 很烦人
尝试通过双击复制这个 UUID:
c6b10dd3-1dcf-416c-8ed8-ae561807fcaf
如果你幸运的话,你得到了整个 UUID,但对于大多数人来说,他们只得到了一部分。增强唯一标识符的可用性的一种方法是使其易于复制。可以通过移除 UUID 中的连字符来实现,这样用户只需双击标识符即可复制。通过消除手动选择和复制粘贴的需要,这一小改动可以大大改善使用标识符时的用户体验。
移除连字符在所有语言中都很简单,以下是 Java 的实现:
String id = UUID.randomUUID().toString().replace("-", "");
// fe4723eab07f408384a2c0f051696083
现在试试复制它,体验是不是好很多?
添加前缀
避免在开发环境中意外使用生产环境的 API 密钥。可以通过添加有意义的前缀来帮助用户区分不同的环境或系统中的资源。例如,Stripe 使用 sk_live_ 作为生产环境密钥的前缀,或 cus_ 作为客户标识符。通过加入这样的前缀,可以确保清晰度并减少混淆的可能,特别是在多个环境共存的复杂系统中。
String id = "hello_" + UUID.randomUUID().toString().replace("-", "");
// hello_1559debea64142f3b2d29f8b0f126041
命名前缀就像命名变量一样,需要描述性但尽量简短。
使用 base58 编码
除了使用十六进制表示标识符,还可以考虑更高效的编码方式,如 base58 编码。Base58 编码使用更大的字符集,并避免了易混淆的字符,如大写字母 I 和小写字母 l,从而在不影响可读性的情况下生成更短的标识符字符串。
例如,一个 8 字符长的 base58 字符串,可以存储大约 30,000 倍于 8 字符十六进制字符串的状态。而 16 字符的 base58 字符串可以存储 889,054,070 倍于 UUID 的组合。
可以使用第三方库来实现这一点,以下是 Java 的实现示例:
import com.aventrix.jnanoid.jnanoid.NanoIdUtils;
import java.util.Random;
Random random = new Random();
String alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
String id = "prefix_" + NanoIdUtils.randomNanoId(random, alphabet.toCharArray(), 22);
// prefix_KSPKGySWPqJWWWa37RqGaX
我们生成了一个 22 字符长的 ID,这可以编码约 100 倍于 UUID 的状态,同时短了 10 个字符。
字符集 | 长度 | 总状态数 |
---|---|---|
UUID | 16 | 2^122 = 5.3e+36 |
Base58 | 58 | 58^22 = 6.2e+38 |
状态越多,碰撞的概率越低,因为生成相同 ID 所需的时间更长(在算法真正随机的情况下)。
改变熵值
并不是所有标识符都需要高碰撞抵抗性。在某些情况下,根据应用程序的具体要求,较短的标识符可能就足够了。通过减少标识符的熵,可以生成较短的 ID,同时仍然保持可接受的唯一性水平。
减少 ID 长度可能很不错,但需要确保系统能防止 ID 碰撞。在我们的 MySQL 数据库中,我们主要使用 ID 作为主键,数据库可以保护我们免受碰撞的影响。如果 ID 已存在,我们只需生成一个新 ID 并重试。如果碰撞率显著上升,可以简单地增加所有未来 ID 的长度。
长度 | 示例 | 总状态数 |
---|---|---|
nanoid(8) | re6ZkUUV | 1.3e+14 |
nanoid(12) | pfpPYdZGbZvw | 1.4e+21 |
nanoid(16) | sFDUZScHfZTfkLwk | 1.6e+28 |
nanoid(24) | u7vzXJL9cGqUeabGPAZ5XUJ6 | 2.1e+42 |
nanoid(32) | qkvPDeH6JyAsRhaZ3X4ZLDPSLFP7MnJz | 2.7e+56 |
结论
通过实施这些改进,可以增强应用程序中唯一标识符的可用性和效率。这将为用户和开发人员提供更好的体验,因为他们在系统内与各种实体交互和管理这些实体。无论是轻松复制标识符、区分不同环境,还是实现更短且更易读的标识符字符串,这些策略都可以帮助构建一个更用户友好和健壮的识别系统。
Unkey 的 ID 和密钥
我们使用一个简单的函数,该函数接收一个类型化的前缀,然后为我们生成 ID。这样我们可以确保同一类型的 ID 总是使用相同的前缀。这在系统中有多种类型的 ID 时特别有用。
import com.aventrix.jnanoid.jnanoid.NanoIdUtils;
import java.util.Random;
public class IdGenerator {
private static final Random random = new Random();
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final int ID_LENGTH = 16;
private static final Map<String, String> prefixes = Map.of(
"key", "key",
"api", "api",
"policy", "pol",
"request", "req",
"workspace", "ws",
"keyAuth", "key_auth",
"vercelBinding", "vb",
"test", "test"
);
public static String newId(String prefix) {
if (!prefixes.containsKey(prefix)) {
throw new IllegalArgumentException("Invalid prefix: " + prefix);
}
return prefixes.get(prefix) + "_" + NanoIdUtils.randomNanoId(random, ALPHABET.toCharArray(), ID_LENGTH);
}
}
在代码库中使用它时,我们可以确保始终为正确类型的 ID 使用正确的前缀。
String id = IdGenerator.newId("workspace");
// ws_dYuyGV3qMKvebjML
String id = IdGenerator.newId("keyy");
// 抛出异常,因为 `keyy` 不是有效的前缀名称
API 密钥实际上也是一种标识符。它只是一种特殊的标识符,用于验证请求。我们对 API 密钥使用与标识符相同的策略。可以添加一个前缀,让用户知道他们正在使用哪种类型的密钥,并且可以在合理范围内指定密钥的长度。API 密钥的碰撞比 ID 更严重,因此我们强制执行安全限制。
通常,会在 API 密钥前添加一个标识公司的前缀。例如,Resend 使用 re_ 和 OpenStatus 使用 os_ 前缀。这使用户能够快速识别密钥并了解其用途。
String key = IdGenerator.newId("blog");
// blog_cLsvCvmY35kCfchi
参考链接
- UUID 标准
- NanoID 文档
- Crypto API
- Stripe API Keys