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);
}

关键点:

  1. 生成 API Key 的用户必须是当前登录用户;
  2. 前端传入 remarkexpireDays
  3. 后端根据当前用户 ID 生成 API Key 并落库;
  4. 生成后将完整 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、内部工具和轻量级服务集成等场景。

相关推荐
Volunteer Technology1 小时前
SpringSecurity中的权限管理
java·数据库·servlet
段ヤシ.1 小时前
回顾Java知识点,面试题汇总Day13:数据库MySQL(持续更新)
java·数据库·mysql
mN9B2uk172 小时前
在Qt中使用SQLite数据库
数据库·qt·sqlite
network_tester2 小时前
SENT/PSI5传感器TSN集成测试:打通传统传感与未来车载网络的“最后一公里”
数据库·网络协议·tcp/ip·自动驾驶·信息与通信·信号处理·tcpdump
桌面运维家2 小时前
校园机房vDisk IDV云桌面建设方案价格参考
linux·服务器·数据库
念越2 小时前
SQL 基础语法复习
数据库·sql·数据库系统概论
ULIi096kr2 小时前
MySQL磁盘爆满快速排查方案:一键查询库表空间、定位占用大户(RDS/自建通用)
数据库·mysql
华山令狐虫2 小时前
告别手写 SQL——DBAPI 企业版 v4.6.0 推出 AI 助手
数据库·人工智能·sql·dbapi
Cx330❀2 小时前
【MySQL基础】库与表的全面操纵指南
linux·服务器·网络·数据库·c++·mysql