简述
在系统架构中,API Key 是客户端与服务端交互的凭证。合理的 API Key 设计需要在检索性能与凭证安全性之间取得平衡。本文拆解常见的双段式 API Key 结构,给出核心数据库表设计,并提供基于 Java 的实现方案。
一、 双段式 API Key 结构拆解
以形如 b848f22790274b4caaa7addfe6d92c0c.2ZYUKSEOQVBHQfmh 的常见格式为例,这类 Key 通常由点号(.)划分为两部分:
- 前段(API ID):用于快速检索
- 通常为固定长度的随机串或 UUID。
- 用途:作为数据库或缓存(如 Redis)的唯一索引。服务端收到请求时,直接通过该 ID 进行精准匹配,避免全表扫描,保证高并发场景下的检索性能。
- 后段(Secret):用于安全校验,高强度的密码学随机串
- 生成与展示 :Secret 仅在创建时生成一次明文并展示给用户,由用户自行保存。服务端绝不落盘存储明文。
- 存储方式:服务端仅在数据库中保存其单向哈希值(如 SHA-256)。
- 校验流程 :每次客户端调用 API 时携带该明文 Secret;服务端接收后对其进行相同的单向哈希计算,再与数据库中的
secret_hash进行比对。这种设计即使在数据库泄露时也能保证凭证不会直接暴露。
二、 核心数据库表结构设计参考
为了支撑 API Key 的检索、安全校验、权限控制以及生命周期管理,数据库通常包含以下核心字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
api_id |
VARCHAR | 完整 Key 的前段,作为唯一索引,用于快速检索 |
name |
VARCHAR | API Key 名称/备注,用于用户辨识和管理 |
secret_hash |
VARCHAR | 后段 Secret 经过单向哈希(SHA-256)后的值,绝不存明文 |
payload |
JSON / TEXT | 自定义扩展的业务上下文(如 user_id、tenant_id、权限范围等) |
status |
TINYINT / VARCHAR | 状态标识(例如:1-正常启用、0-已吊销) |
expires_at |
TIMESTAMP | 过期时间,为空则永久有效,用于控制临时凭证 |
last_used_at |
TIMESTAMP | 最后一次成功调用的时间,用于审计或过期清理 |
usage_count |
BIGINT | 累计使用次数,用于统计使用量与监控频次 |
created_at |
TIMESTAMP | 创建时间 |
... |
... | ... |
根据业务系统实际需要,可以有独立的主键字段,或者直接使用 app_id 作为主键。
三、 Java 实现方案
以下是 API Secret 的生成实现。设计上直接接收最终期望的字符长度,底层使用密码学安全随机数(SecureRandom)配合 Base64URL 编码生成。
1. 代码实现 (ApiKeyUtils.java)
java
import java.security.SecureRandom;
import java.util.Base64;
public class ApiKeyUtils {
// 使用密码学安全随机数生成器
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/**
* 生成 API Secret
* <p> 使用保守计算的字节长度生成后截取,在保证最终长度的目标下,平衡了 nextBytes 和 循环逐个字符生成性能问题 </p>
*
* @param finalLength 期望的最终字符串长度
* @return 经过 Base64URL 编码并截断的随机串
*/
public static String generateSecret(int finalLength) {
if (finalLength <= 0) {
throw new IllegalArgumentException("Length must be greater than 0");
}
// 根据 Base64 编码规则(4个字符对应3个字节),计算所需的原始字节数
int byteLength = (int) Math.ceil(finalLength * 3.0 / 4.0);
byte[] bytes = new byte[byteLength];
SECURE_RANDOM.nextBytes(bytes);
// 使用无填充的 Base64URL 编码(包含大小写字母、数字、- 和 _)
String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
// 截断至目标长度
return encoded.substring(0, Math.min(finalLength, encoded.length()));
}
public static void main(String[] args) {
// 示例:生成 16 位与 32 位的 Secret
System.out.println("16位 API Secret: " + generateSecret(16));
System.out.println("32位 API Secret: " + generateSecret(32));
}
}
2. 设计说明
- 安全性 :强制使用
SecureRandom,避免使用普通随机数算法带来的可预测性风险。 - 接口直观性:输入参数直接对应最终字符长度,屏蔽底层的字节换算和编码细节。
- 字符集特性:Base64URL 编码保证了字符在 URL、HTTP Header 及数据库中的安全传输。
(END)