API 密钥是用户与我们产品首次互动的重要组成部分,我们希望给用户留下良好的印象。我们希望我们的密钥看起来不错,感觉也不错,但业内似乎没有“好”的标准。
我们是一家忙碌的初创公司,但我们也是一家以开发者为先的平台公司。因此,我们认为花一些时间、精力和心思来找到一个我们(希望我们的开发者)都会满意的解决方案是有意义的。
我们列出了 API 密钥的要求:
- 安全的
- 全球独一无二
- 可排序
- Postgres 中的性能
- 看起来不错
不幸的是,大多数 API 密钥都很丑陋。它们通常只是格式不一致的随机字符串,很难阅读、排序和识别。
由于没有一个选项足够美观(对称),所以我们创造了自己的方法:
- 使用 UUIDv7 作为基本 ID 来利用时间戳
- 使用 Crockford Base32 对 ID 进行编码以提高可读性
- 添加巧妙放置的破折号以增添美感
结果:
key, _ := uuidkey.Encode("d1756360-5da0-40df-9926-a76abff5601d") fmt.Println(key) // Output: 38QARV0-1ET0G6Z-2CJD9VA-2ZZAR0X
|
特点:
- 31 个字符(不含破折号为 28 个) vs UUID 为 36 个
- 高度可读的段落,包含 4 组 7 个大写字母和数字,具有“块状”美感和可读性
- 解码为 UUID 存储时可按时间顺序排序
- 面向用户的密钥中存在模糊的时间戳(但精明的用户仍然可以对其进行解码)。我们发现密钥中的时间戳元数据是一个优势,您可以随时使用 UUIDv4 来代替!你做到了!
我们制作了自己的密钥包 uuidkey :
- 您可以使用它将 UUID编码和格式化为人类可读的密钥。
- 如果您使用UUIDv7,您还可以解码密钥以将它们存储为可排序、可索引的 ID 在您的数据库中。
为什么选择 UUIDv7
除了时间戳优势之外,UUIDv7 还将在 v18 3中获得原生 Postgres 支持。虽然目前可以使用扩展在服务器端生成 UUIDv7,但原生 Postgres 支持的性能肯定会更高4,并且可以很好地传递给uuidkey.Encode()。
对于我们的实现,我们目前在应用程序层生成密钥并将其存储为 UUID,以进行排序和索引。一旦 Postgres v18 发布,我们将切换到 Postgres 生成,以从我们的应用程序层卸载该数据流并获得略微更好的性能。
为什么选择 Crockford Base32
我们选择Crockford Base32编码是因为它:
- 仅使用大写字母和数字,提高可读性
- 将密钥长度缩短约 1/5
- 制图高效且可预测
- 是所有酷孩子都在用的东西
为什么选择 Dashes
由此产生的虚线键是“块状”且对称的。如果您将单个字符变灰,它们看起来几乎像条形码。我们认为它可以轻松快速地读取键的部分内容以进行识别。
我们可能在潜意识中受到了老式产品 CD 密钥的启发。
破折号确实消除了简单的双击复制,但我们认为这对可读性来说是一个很好的权衡。我们不希望用户到处复制和粘贴它们,事实上我们希望小心处理它们。理想情况下,用户只复制每个密钥一次 - 当他们从我们的仪表板生成密钥时 - 因此我们在用户界面中添加了一个复制按钮来解决这种情况。
我们在github.com/agentstation/uuidkey上开源了这些设计选择。如果您认同我们的美学、推理、对称性,并想要拥有自己漂亮的 API 密钥,欢迎您试用我们的开源项目。
该软件包的核心是通过 Base32-Crockford 编解码器uuidkey将 UUID 编码为可读Key格式,然后将其解码回 UUID。
// Encode will encode a given UUID string into a Key with basic length validation. func Encode(uuid string) (Key, error) { if len(uuid) != UUIDLength { // basic length validation to ensure we can encode return "", fmt.Errorf("invalid UUID length: expected %d characters, got %d", UUIDLength, len(uuid)) }
// select the 5 parts of the UUID string s1 := uuid[0:8] // [d1756360]-5da0-40df-9926-a76abff5601d s2 := uuid[9:13] // d1756360-[5da0]-40df-9926-a76abff5601d s3 := uuid[14:18] // d1756360-5da0-[40df]-9926-a76abff5601d s4 := uuid[19:23] // d1756360-5da0-40df-[9926]-a76abff5601d s5 := uuid[24:36] // d1756360-5da0-40df-9926-[a76abff5601d]
// decode each string part into uint64 n1, _ := strconv.ParseUint(s1, 16, 32) n2, _ := strconv.ParseUint(s2+s3, 16, 32) // combine s2 and s3 n3, _ := strconv.ParseUint(s4+s5[:4], 16, 32) // combine s4 and the first 4 chars of s5 n4, _ := strconv.ParseUint(s5[4:], 16, 32) // the last 8 chars of s5
// encode each uint64 into base32 crockford encoding format e1 := encode(n1) e2 := encode(n2) e3 := encode(n3) e4 := encode(n4)
// build and return key return Key(e1 + "-" + e2 + "-" + e3 + "-" + e4), nil }
|
解锁:
// Decode will decode a given Key into a UUID string with basic length validation. func (k Key) Decode() (string, error) { if len(k) != KeyLength { // basic length validation to ensure we can decode return "", fmt.Errorf("invalid Key length: expected %d characters, got %d", KeyLength, len(k)) }
// select the 4 parts of the key string key := string(k) // convert the type from a Key to string s1 := key[0:7] // [38QARV0]-1ET0G6Z-2CJD9VA-2ZZAR0X s2 := key[8:15] // 38QARV0-[1ET0G6Z]-2CJD9VA-2ZZAR0X s3 := key[16:23] // 38QARV0-1ET0G6Z-[2CJD9VA]-2ZZAR0X s4 := key[24:31] // 38QARV0-1ET0G6Z-2CJD9VA-[2ZZAR0X]
// decode each string part into original UUID part string n1 := decode(s1) n2 := decode(s2) n3 := decode(s3) n4 := decode(s4)
// select the 4 parts of the decoded parts n2a := n2[0:4] n2b := n2[4:8] n3a := n3[0:4] n3b := n3[4:8]
// build and return UUID string return (n1 + "-" + n2a + "-" + n2b + "-" + n3a + "-" + n3b + n4), nil }
|
该软件包旨在与遵循官方 UUID 规范(RFC 4122)的任何 UUID 配合使用,但我们专门测试并保持与两个最流行的 UUID Go 生成器的兼容性:
安装非常简单:
go get github.com/agentstation/uuidkey
|
基本用法:
key, _ := uuidkey.Encode("d1756360-5da0-40df-9926-a76abff5601d") fmt.Println(key) // 38QARV0-1ET0G6Z-2CJD9VA-2ZZAR0X
|
我们尽力将开销降至最低:
BenchmarkValidate-12 33527211 35.72 ns/op BenchmarkParse-12 32329798 36.96 ns/op BenchmarkEncode-12 3151844 377.0 ns/op BenchmarkDecode-12 5587066 216.7 ns/op
|