一、概述
API Key(应用程序接口密钥)是一种轻量级的身份认证机制,用于识别和验证调用方身份。它适用于机器对机器(M2M)通信场景,具有实现简单、性能高、易于管理等优点。
⚠️ 注意:API Key 不是用于用户登录(应使用 OAuth 2.0 / JWT),而是用于标识客户端应用或服务。
🔍 API Key 本质上只是一个字符串凭证,其验证逻辑完全由业务系统自定义,因此:
- 没有统一的颁发流程
- 没有标准的令牌结构
- 没有跨系统的互操作需求(通常只用于自家 API)
二、核心设计原则
| 原则 | 说明 |
|---|---|
| 无状态 | 每次请求独立验证,不依赖会话 |
| 最小权限 | 每个 Key 应限制可访问的资源和操作 |
| 安全存储 | 服务端绝不存储原始 Key,仅存哈希值 |
| 可撤销 & 可轮换 | 支持立即禁用或生成新 Key |
| 默认过期 | 强烈建议设置有效期(如 90 天) |
三、认证流程
sequenceDiagram
participant Client as 客户端
participant Server as 服务端
participant DB as 数据库
Client->>Server: 请求头携带 X-API-Key: sk_xxx
Server->>Server: 对 sk_xxx + 固定盐 做 SHA-256 哈希
Server->>DB: 查询 key_hash = 'a1b2c3...'
DB-->>Server: 返回记录(含 expires_at, user_id, 权限等)
alt 找不到 / 已禁用 / 已过期
Server-->>Client: 403 Forbidden
else 验证通过
Server->>Server: 检查限流、权限、IP 白名单等
Server->>Client: 返回业务数据
end
关键说明:
- API Key 字符串本身不包含任何元数据(如过期时间、权限)
- 所有属性均存储在服务端数据库中,通过
key_hash关联 - 客户端只需原样传递原始 Key,无需任何加密或哈希处理
四、数据模型设计
表名:api_keys
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | BIGINT | ✅ | 主键 |
key_hash | VARCHAR(255) | ✅ | 哈希后的 Key(唯一) |
user_id | BIGINT | ✅ | 所属用户/租户 ID |
app_name | VARCHAR(100) | ✅ | 应用名称(便于管理) |
permissions | TEXT / JSON | ❌ | 权限列表,如 ["read:data", "write:logs"] |
rate_limit | INT | ❌ | 每分钟最大请求数(默认 100) |
expires_at | DATETIME | ❌ | 过期时间(NULL 表示永不过期) |
is_active | BOOLEAN | ✅ | 是否启用(默认 true) |
created_at | DATETIME | ✅ | 创建时间 |
last_used_at | DATETIME | ❌ | 最后使用时间(用于审计) |
🌍 时区建议:所有时间字段统一使用 UTC 时间
五、API Key 生命周期管理
1. 生成规则
- 格式:
sk_<prefix>_<48位随机字符> - 示例:
sk_live_aB3xK9mQpL2vR8nT7yU4wZ1cE6fG0hJ5 - 随机性:使用密码学安全随机数(Java
SecureRandom)
public String generateRawApiKey() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return "sk_" + new SecureRandom().ints(48, 0, chars.length())
.mapToObj(chars::charAt)
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
}
2. 存储安全
- 绝不存储原始 Key
- 使用 固定盐 + SHA-256 哈希(因 API Key 为高熵随机串,无需 bcrypt)
- 盐值从环境变量读取,禁止硬编码
// application.yml
app:
security:
api-key-salt: ${API_KEY_SALT}
public String hashApiKey(String rawKey) {
return DigestUtils.sha256Hex(rawKey + fixedSalt);
}
3. 过期机制
- 创建时可指定有效期(如 30/90/365 天)或“永不过期”
- 验证时检查:
if (expiresAt != null && now > expiresAt) → 403 - 过期 Key 无法恢复,需创建新 Key(轮换)
4. 轮换(Rotate)
- 用户点击“重新生成” → 创建新 Key + 禁用旧 Key
- 原始 Key 仅在创建时返回一次,之后不可见
六、Java 实现(Spring Boot)
1. 实体类
@Entity
@Table(name = "api_keys")
public class ApiKey {
private Long id;
private String keyHash; // 哈希值
private Long userId;
private String appName;
private LocalDateTime expiresAt;
private Boolean active = true;
// ... 其他字段
}
2. 认证过滤器
@Component
public class ApiKeyAuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
String rawKey = ((HttpServletRequest) req).getHeader("X-API-Key");
if (rawKey == null) {
// 返回 401
}
String keyHash = apiKeyHasher.hash(rawKey);
ApiKey record = apiKeyRepo.findByKeyHash(keyHash);
if (record == null || !record.getActive()) {
// 返回 403
}
if (record.getExpiresAt() != null &&
LocalDateTime.now(ZoneOffset.UTC).isAfter(record.getExpiresAt())) {
// 返回 403: "API key has expired"
}
// 继续请求
chain.doFilter(req, res);
}
}
3. 创建接口
@PostMapping("/api-keys")
public ResponseEntity<?> createApiKey(@RequestBody CreateKeyRequest req) {
String rawKey = apiKeyService.generateRawApiKey();
String keyHash = apiKeyHasher.hash(rawKey);
LocalDateTime expiresAt = null;
if (req.getExpireDays() != null) {
expiresAt = LocalDateTime.now(ZoneOffset.UTC).plusDays(req.getExpireDays());
}
apiKeyRepo.save(new ApiKey(keyHash, ..., expiresAt));
// 仅在此处返回原始 Key!
return ok(Map.of("api_key", rawKey, "expires_at", expiresAt));
}
七、安全最佳实践
| 措施 | 说明 |
|---|---|
| 强制 HTTPS | 防止 Key 在传输中被窃听 |
| Header 传递 | 禁止通过 URL 参数(避免日志泄露) |
| 限流保护 | 基于 Key 的请求频率控制(Redis + 滑动窗口) |
| IP 白名单(可选) | 高安全场景可绑定调用 IP |
| 审计日志 | 记录每次 Key 使用(时间、IP、Endpoint) |
| 定期轮换 | 默认 90 天过期,支持自动提醒 |
| 泄露监控 | 使用工具扫描代码仓库是否泄露 Key |
八、与其他认证方式对比
| 方式 | 适用场景 | 安全性 | 复杂度 |
|---|---|---|---|
| API Key | 服务间调用、内部系统、简单集成 | 中 | 低 |
| OAuth 2.0 | 第三方授权、用户委托 | 高 | 高 |
| JWT | 无状态用户会话、微服务 | 中高 | 中 |
| Basic Auth | 内部测试、临时调试 | 低(需 HTTPS) | 极低 |
✅ 选择建议:
- 如果是你的服务调用你的 API → 用 API Key
- 如果是第三方代表用户访问数据 → 用 OAuth 2.0
九、常见问题 FAQ
Q1:API Key 能防重放攻击吗?
❌ 不能。如需防重放,需额外引入 nonce + timestamp 机制(通常用于支付等高安全场景)。
Q2:为什么不用 JWT 存储过期信息?
JWT 适合自包含令牌,但 API Key 更强调集中管理(如随时禁用、修改权限)。JWT 一旦签发就无法撤销(除非引入黑名单)。
Q3:盐值泄露了怎么办?
因 API Key 是高熵随机值,即使知道盐和哈希,也几乎无法暴力破解。但应立即轮换所有 Key 并更换盐值。
Q4:为什么 JWT一旦签发就难以撤销,而 API Key 可以随时禁用
✅一句话总结:
JWT 是“一次性通行证”,发出去就管不了; API Key 是“门禁卡”,后台随时可以拉黑。
| 能力 | JWT | API Key |
|---|---|---|
| 是否需要查库验证 | 否(无状态) | 是(有状态) |
| 能否立即禁用 | ❌ 不能(除非加黑名单) | ✅ 能(改 is_active) |
| 适合场景 | 用户会话、需要自包含信息 | 机器身份、服务间调用 |
| 性能 | 极高(无 IO) | 中(需查库/缓存) |
| 安全性控制粒度 | 粗(靠过期时间) | 细(可随时开关、限流、改权限) |