文章目录
- [API Key 生成和鉴权机制:从随机凭证生成到请求拦截校验](#API Key 生成和鉴权机制:从随机凭证生成到请求拦截校验)
API Key 生成和鉴权机制:从随机凭证生成到请求拦截校验
本文介绍一种数据库查表型 API Key 实现方式,核心流程包括:API Key 生成、过期时间计算、数据库存储、前端展示、脱敏展示、请求头携带、后端拦截器校验和用户身份切换。
用户点击生成 API Key
↓
前端调用生成接口
↓
后端获取当前登录用户 ID
↓
生成 ak- + 无连字符 UUID
↓
计算过期时间
↓
保存完整 Key 到 api_key 表
↓
前端弹窗展示完整 API Key
↓
关闭弹窗后,列表接口只返回脱敏 Key
↓
外部系统通过 Authorization 请求头携带完整 Key
↓
后端拦截器提取 Bearer Token
↓
判断是否为 ak- 开头
↓
查询 api_key 表
↓
校验存在性、删除状态、过期状态
↓
切换到 API Key 所属用户身份
↓
写入请求上下文
↓
放行业务接口
一、API Key 生成规则
本方案中的 API Key 不是 JWT,也不是加密 Token,而是一个随机字符串,格式如下:
text
ak- + 32 位无连字符 UUID
示例:
text
ak-550e8400e29b41d4a716446655440000
后端生成逻辑可以简化为:
java
public ApiKey generateApiKey(Long userId, String remark, Integer expireDays) {
ApiKey entity = new ApiKey();
entity.setUserId(userId);
entity.setRemark(remark);
entity.setApiKey("ak-" + generateRandomUuidWithoutDash());
if (expireDays != null && expireDays > 0) {
entity.setExpiredAt(now().plusDays(expireDays));
}
save(entity);
return entity;
}
其中核心代码是:
java
entity.setApiKey("ak-" + generateRandomUuidWithoutDash());
ak- 用于标识这是 API Key 类型凭证,便于和普通登录 Token、JWT 或其他认证凭证区分。
二、过期时间处理
生成 API Key 时,可以传入有效天数 expireDays:
java
if (expireDays != null && expireDays > 0) {
entity.setExpiredAt(now().plusDays(expireDays));
}
规则如下:
| expireDays | expired_at | 含义 |
|---|---|---|
| 7 | 当前时间 + 7 天 | 7 天后过期 |
| 30 | 当前时间 + 30 天 | 30 天后过期 |
| 0 | null | 永不过期 |
| null | null | 永不过期 |
如果 expired_at 为空,则表示 API Key 永不过期;如果不为空,则后续鉴权时需要判断当前时间是否已经超过该时间。
三、数据库表结构
API Key 最终保存到数据库中,一个简化表结构如下:
sql
CREATE TABLE api_key (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
api_key VARCHAR(128) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
expired_at DATETIME DEFAULT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_deleted TINYINT NOT NULL DEFAULT 0,
UNIQUE KEY uk_api_key (api_key),
KEY idx_user_id (user_id)
);
关键字段说明:
| 字段 | 作用 |
|---|---|
| user_id | API Key 所属用户 |
| api_key | 实际访问凭证 |
| remark | Key 的备注说明 |
| expired_at | 过期时间,空表示永不过期 |
| is_deleted | 软删除标记 |
| uk_api_key | 保证 API Key 唯一 |
这种设计的特点是,Key 本身不携带用户信息、权限信息和过期信息,所有状态都通过数据库查询获得。
四、生成接口
后端提供生成 API Key 的接口,简化逻辑如下:
java
public ApiResponse<ApiKey> generate(ApiKeyGenerateRequest request) {
Long currentUserId = getCurrentLoginUserId();
ApiKey apiKey = apiKeyService.generateApiKey(
currentUserId,
request.getRemark(),
request.getExpireDays()
);
return ApiResponse.success(apiKey);
}
关键点:
- 生成 API Key 的用户必须是当前登录用户;
- 前端传入
remark和expireDays; - 后端根据当前用户 ID 生成 API Key 并落库;
- 生成后将完整 API Key 返回给前端展示。
五、前端生成与首次展示
前端请求方法可以封装为:
ts
export async function generateApiKey(params: {
remark?: string;
expireDays?: number;
}) {
return request.post('/api-key/generate', params);
}
页面调用逻辑可以简化为:
ts
async function handleGenerate() {
if (!form.remark) {
showError('请输入备注');
return;
}
const data = await generateApiKey(form);
showSuccessModal({
title: '生成成功',
content: `请妥善保管您的 API Key,关闭后将无法再次查看全文:\n\n${data.apiKey}`,
});
refreshList();
}
这里有一个关键设计:完整 API Key 只在生成成功时展示一次。
生成接口 /api-key/generate 返回的是 ApiKeyEntity,其中包含完整的 apiKey。前端拿到完整值后,通过 Modal.success 弹窗展示给用户,并提示用户妥善保存。
用户关闭弹窗后,再回到 API Key 列表页时,就只能看到脱敏后的 Key。
六、列表页脱敏展示
API Key 的脱敏展示只发生在返回列表时的 VO 转换阶段,也就是展示层处理,不影响数据库中的真实存储。
数据库中的 api_key 字段仍然保存完整 API Key,后续鉴权时也仍然使用完整 Key 进行查询。
后端列表接口 /api-key/list 返回的是 ApiKeyVO,在实体转换为 VO 时,通过 ApiKeyConvertor.maskApiKey() 对 apiKey 字段进行脱敏处理。
脱敏逻辑如下:
java
@Named("maskApiKey")
default String maskApiKey(String apiKey) {
if (apiKey == null || apiKey.length() < 12) {
return apiKey;
}
return apiKey.substring(0, 8) + "***" + apiKey.substring(apiKey.length() - 4);
}
脱敏规则是:
text
前 8 位 + "***" + 后 4 位
例如:
text
ak-7ebfc***b045
也就是说,列表接口拿到的 apiKey 本身就是脱敏后的值,而不是完整 API Key。
对应链路可以理解为:
text
api_key 表保存完整 Key
↓
/api-key/list 查询数据
↓
ApiKeyEntity 转 ApiKeyVO
↓
MapStruct 调用 maskApiKey()
↓
返回脱敏后的 apiKey
↓
前端列表展示
这样可以保证列表页即使被打开,也不会直接暴露完整 API Key。
前端列表页使用 Ant Design Vue 的 Table 展示数据,并通过自定义单元格 #bodyCell 处理 apiKey 列的渲染。
在 apiKey 列中,可以使用 <code> 标签配合 Tailwind 类展示脱敏后的 Key,使其更接近凭证样式:
vue
<code class="...">
{{ record.apiKey }}
</code>
如果前端拿到的是完整 Key,也可以通过同样的规则进行二次脱敏拼接:
ts
apiKey.substring(0, 8) + '***' + apiKey.substring(apiKey.length - 4)
不过在当前实现中,后端列表接口已经返回脱敏值,因此前端主要负责样式渲染。
列表页中的时间字段也会在前端格式化展示:
ts
dayjs(value).format('YYYY-MM-DD HH:mm:ss')
其中:
createTime展示为标准时间格式;expiredAt如果为空,则显示为"永不过期";expiredAt如果不为空,则格式化为YYYY-MM-DD HH:mm:ss。
这一设计把"存储"和"展示"分离开:
text
数据库:保存完整 API Key
生成弹窗:展示一次完整 API Key
列表接口:返回脱敏 API Key
列表页面:展示脱敏值和时间信息
鉴权逻辑:仍使用完整 API Key 查询数据库
七、请求鉴权流程
客户端调用接口时,需要在请求头中携带 API Key:
http
Authorization: Bearer ak-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
后端通过拦截器统一处理 API Key 鉴权逻辑:
java
public boolean preHandle(HttpRequest request, HttpResponse response, Object handler) {
String authHeader = request.getHeader("Authorization");
if (isBlank(authHeader)) {
return true;
}
String token = authHeader.replace("Bearer ", "").trim();
if (!token.startsWith("ak-")) {
return true;
}
ApiKey apiKey = apiKeyService.getByApiKey(token);
if (apiKey == null || apiKey.isDeleted()) {
throw new UnauthorizedException("API Key 无效");
}
if (apiKey.getExpiredAt() != null && apiKey.getExpiredAt().isBefore(now())) {
throw new UnauthorizedException("API Key 已过期");
}
switchToUser(apiKey.getUserId());
requestContext.set("API_KEY_AUTH", true);
requestContext.set("API_KEY_ID", apiKey.getId());
return true;
}
鉴权流程如下:
text
读取 Authorization 请求头
↓
提取 Bearer 后面的 Token
↓
判断是否以 ak- 开头
↓
不是 API Key,则交给后续常规鉴权流程
↓
是 API Key,则查询数据库
↓
判断 Key 是否存在
↓
判断 Key 是否被删除
↓
判断 Key 是否过期
↓
校验通过后切换到所属用户身份
↓
写入当前请求上下文
↓
放行请求
需要注意的是,列表页展示的脱敏 Key 不能用于接口调用。外部系统调用接口时,必须使用生成时弹窗中展示过的完整 API Key。
八、用户身份切换
API Key 校验通过后,需要将当前请求临时绑定到 API Key 所属用户:
java
switchToUser(apiKey.getUserId());
这样后续业务逻辑就可以继续复用原有的用户上下文,例如:
- 查询当前用户资源;
- 保存当前用户数据;
- 执行用户维度的接口逻辑;
- 记录用户维度的操作日志。
需要注意的是,API Key 认证只是在当前请求上下文中临时切换用户身份,不等于创建真实登录会话。
九、删除与撤销
前端删除 API Key 的请求可以封装为:
ts
export async function deleteApiKey(id: number | string) {
return request.delete(`/api-key/${id}`);
}
后端可以通过软删除实现撤销:
java
apiKey.setDeleted(true);
updateById(apiKey);
后续请求如果继续携带已删除的 API Key,拦截器会在校验阶段拦截:
java
if (apiKey == null || apiKey.isDeleted()) {
throw new UnauthorizedException("API Key 无效");
}
这样可以在不物理删除数据的情况下,实现 API Key 的失效和审计保留。
十、安全实现要点
1. API Key 只展示一次
生成成功后完整展示一次,后续管理页面只展示脱敏内容,例如:
text
ak-550e****0000
2. 脱敏只发生在展示层
脱敏展示不修改数据库中的真实 API Key,只在列表接口返回 ApiKeyVO 时处理。
也就是说:
text
数据库存储:完整 Key
列表返回:脱敏 Key
接口鉴权:完整 Key
这样既能保证安全展示,又不会影响后端通过完整 Key 查库鉴权。
3. 不在日志中打印完整 Key
避免如下日志:
java
log.warn("API Key {} 无效", apiKey);
建议改为脱敏输出:
java
log.warn("API Key {} 无效", maskApiKey(apiKey));
4. 支持过期和撤销
通过 expired_at 控制过期时间,通过 is_deleted 控制撤销状态。
5. 可以扩展权限边界
如果后续安全要求更高,可以继续扩展:
- 绑定接口权限;
- 区分只读和读写;
- 限制来源 IP;
- 增加调用频率限制;
- 记录最近使用时间;
- 记录 API Key 调用日志。
十一、完整调用链路
text
用户点击生成 API Key
↓
前端调用生成接口
↓
后端获取当前登录用户 ID
↓
生成 ak- + 无连字符 UUID
↓
计算过期时间
↓
保存完整 Key 到 api_key 表
↓
前端弹窗展示完整 API Key
↓
关闭弹窗后,列表接口只返回脱敏 Key
↓
外部系统通过 Authorization 请求头携带完整 Key
↓
后端拦截器提取 Bearer Token
↓
判断是否为 ak- 开头
↓
查询 api_key 表
↓
校验存在性、删除状态、过期状态
↓
切换到 API Key 所属用户身份
↓
写入请求上下文
↓
放行业务接口
十二、总结
这是一种典型的数据库查表型 API Key 机制。
它的核心实现是:
text
生成阶段:ak- + 随机字符串,保存完整 Key 到数据库
展示阶段:生成时展示一次完整 Key,列表页只返回脱敏 Key
鉴权阶段:请求头携带完整 Key,后端查库校验
该方案的优点是实现简单、状态可控、撤销方便,并且通过"只展示一次全文 + 列表脱敏展示"的方式降低了 Key 泄露风险。
它适合用于第三方系统调用、自动化脚本、AI Agent、内部工具和轻量级服务集成等场景。