API Key 生成和鉴权机制:从随机凭证生成到请求拦截校验
文章目录
- API Key 生成和鉴权机制:从随机凭证生成到请求拦截校验
- 一、API Key 生成规则
- 二、过期时间处理
- 三、数据库表结构
- 四、生成接口
- 五、前端生成与首次展示
- 六、列表页脱敏展示
- 七、请求鉴权流程
- 八、用户身份切换
- 九、删除与撤销
- 十、安全实现要点
- 1. API Key 只展示一次
- 2. 脱敏只发生在展示层
- 3. 不在日志中打印完整 Key
- 4. 支持过期和撤销
- 5. 可以扩展权限边界
- 十一、完整调用链路
- 十二、总结
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,而是一个随机字符串,格式如下:
ak- + 32 位无连字符 UUID示例:
ak-550e8400e29b41d4a716446655440000后端生成逻辑可以简化为:
publicApiKeygenerateApiKey(LonguserId,Stringremark,IntegerexpireDays){ApiKeyentity=newApiKey();entity.setUserId(userId);entity.setRemark(remark);entity.setApiKey("ak-"+generateRandomUuidWithoutDash());if(expireDays!=null&&expireDays>0){entity.setExpiredAt(now().plusDays(expireDays));}save(entity);returnentity;}其中核心代码是:
entity.setApiKey("ak-"+generateRandomUuidWithoutDash());ak-用于标识这是 API Key 类型凭证,便于和普通登录 Token、JWT 或其他认证凭证区分。
二、过期时间处理
生成 API Key 时,可以传入有效天数expireDays:
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 最终保存到数据库中,一个简化表结构如下:
CREATETABLEapi_key(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,api_keyVARCHAR(128)NOTNULL,remarkVARCHAR(255)DEFAULTNULL,expired_atDATETIMEDEFAULTNULL,create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMEDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,is_deletedTINYINTNOTNULLDEFAULT0,UNIQUEKEYuk_api_key(api_key),KEYidx_user_id(user_id));关键字段说明:
| 字段 | 作用 |
|---|---|
| user_id | API Key 所属用户 |
| api_key | 实际访问凭证 |
| remark | Key 的备注说明 |
| expired_at | 过期时间,空表示永不过期 |
| is_deleted | 软删除标记 |
| uk_api_key | 保证 API Key 唯一 |
这种设计的特点是,Key 本身不携带用户信息、权限信息和过期信息,所有状态都通过数据库查询获得。
四、生成接口
后端提供生成 API Key 的接口,简化逻辑如下:
publicApiResponse<ApiKey>generate(ApiKeyGenerateRequestrequest){LongcurrentUserId=getCurrentLoginUserId();ApiKeyapiKey=apiKeyService.generateApiKey(currentUserId,request.getRemark(),request.getExpireDays());returnApiResponse.success(apiKey);}关键点:
- 生成 API Key 的用户必须是当前登录用户;
- 前端传入
remark和expireDays; - 后端根据当前用户 ID 生成 API Key 并落库;
- 生成后将完整 API Key 返回给前端展示。
五、前端生成与首次展示
前端请求方法可以封装为:
exportasyncfunctiongenerateApiKey(params:{remark?:string;expireDays?:number;}){returnrequest.post('/api-key/generate',params);}页面调用逻辑可以简化为:
asyncfunctionhandleGenerate(){if(!form.remark){showError('请输入备注');return;}constdata=awaitgenerateApiKey(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字段进行脱敏处理。
脱敏逻辑如下:
@Named("maskApiKey")defaultStringmaskApiKey(StringapiKey){if(apiKey==null||apiKey.length()<12){returnapiKey;}returnapiKey.substring(0,8)+"***"+apiKey.substring(apiKey.length()-4);}脱敏规则是:
前 8 位 + "***" + 后 4 位例如:
ak-7ebfc***b045也就是说,列表接口拿到的apiKey本身就是脱敏后的值,而不是完整 API Key。
对应链路可以理解为:
api_key 表保存完整 Key ↓ /api-key/list 查询数据 ↓ ApiKeyEntity 转 ApiKeyVO ↓ MapStruct 调用 maskApiKey() ↓ 返回脱敏后的 apiKey ↓ 前端列表展示这样可以保证列表页即使被打开,也不会直接暴露完整 API Key。
前端列表页使用 Ant Design Vue 的Table展示数据,并通过自定义单元格#bodyCell处理apiKey列的渲染。
在apiKey列中,可以使用<code>标签配合 Tailwind 类展示脱敏后的 Key,使其更接近凭证样式:
<code class="..."> {{ record.apiKey }} </code>如果前端拿到的是完整 Key,也可以通过同样的规则进行二次脱敏拼接:
apiKey.substring(0,8)+'***'+apiKey.substring(apiKey.length-4)不过在当前实现中,后端列表接口已经返回脱敏值,因此前端主要负责样式渲染。
列表页中的时间字段也会在前端格式化展示:
dayjs(value).format('YYYY-MM-DD HH:mm:ss')其中:
createTime展示为标准时间格式;expiredAt如果为空,则显示为“永不过期”;expiredAt如果不为空,则格式化为YYYY-MM-DD HH:mm:ss。
这一设计把“存储”和“展示”分离开:
数据库:保存完整 API Key 生成弹窗:展示一次完整 API Key 列表接口:返回脱敏 API Key 列表页面:展示脱敏值和时间信息 鉴权逻辑:仍使用完整 API Key 查询数据库七、请求鉴权流程
客户端调用接口时,需要在请求头中携带 API Key:
Authorization: Bearer ak-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx后端通过拦截器统一处理 API Key 鉴权逻辑:
publicbooleanpreHandle(HttpRequestrequest,HttpResponseresponse,Objecthandler){StringauthHeader=request.getHeader("Authorization");if(isBlank(authHeader)){returntrue;}Stringtoken=authHeader.replace("Bearer ","").trim();if(!token.startsWith("ak-")){returntrue;}ApiKeyapiKey=apiKeyService.getByApiKey(token);if(apiKey==null||apiKey.isDeleted()){thrownewUnauthorizedException("API Key 无效");}if(apiKey.getExpiredAt()!=null&&apiKey.getExpiredAt().isBefore(now())){thrownewUnauthorizedException("API Key 已过期");}switchToUser(apiKey.getUserId());requestContext.set("API_KEY_AUTH",true);requestContext.set("API_KEY_ID",apiKey.getId());returntrue;}鉴权流程如下:
读取 Authorization 请求头 ↓ 提取 Bearer 后面的 Token ↓ 判断是否以 ak- 开头 ↓ 不是 API Key,则交给后续常规鉴权流程 ↓ 是 API Key,则查询数据库 ↓ 判断 Key 是否存在 ↓ 判断 Key 是否被删除 ↓ 判断 Key 是否过期 ↓ 校验通过后切换到所属用户身份 ↓ 写入当前请求上下文 ↓ 放行请求需要注意的是,列表页展示的脱敏 Key 不能用于接口调用。外部系统调用接口时,必须使用生成时弹窗中展示过的完整 API Key。
八、用户身份切换
API Key 校验通过后,需要将当前请求临时绑定到 API Key 所属用户:
switchToUser(apiKey.getUserId());这样后续业务逻辑就可以继续复用原有的用户上下文,例如:
- 查询当前用户资源;
- 保存当前用户数据;
- 执行用户维度的接口逻辑;
- 记录用户维度的操作日志。
需要注意的是,API Key 认证只是在当前请求上下文中临时切换用户身份,不等于创建真实登录会话。
九、删除与撤销
前端删除 API Key 的请求可以封装为:
exportasyncfunctiondeleteApiKey(id:number|string){returnrequest.delete(`/api-key/${id}`);}后端可以通过软删除实现撤销:
apiKey.setDeleted(true);updateById(apiKey);后续请求如果继续携带已删除的 API Key,拦截器会在校验阶段拦截:
if(apiKey==null||apiKey.isDeleted()){thrownewUnauthorizedException("API Key 无效");}这样可以在不物理删除数据的情况下,实现 API Key 的失效和审计保留。
十、安全实现要点
1. API Key 只展示一次
生成成功后完整展示一次,后续管理页面只展示脱敏内容,例如:
ak-550e****00002. 脱敏只发生在展示层
脱敏展示不修改数据库中的真实 API Key,只在列表接口返回ApiKeyVO时处理。
也就是说:
数据库存储:完整 Key 列表返回:脱敏 Key 接口鉴权:完整 Key这样既能保证安全展示,又不会影响后端通过完整 Key 查库鉴权。
3. 不在日志中打印完整 Key
避免如下日志:
log.warn("API Key {} 无效",apiKey);建议改为脱敏输出:
log.warn("API Key {} 无效",maskApiKey(apiKey));4. 支持过期和撤销
通过expired_at控制过期时间,通过is_deleted控制撤销状态。
5. 可以扩展权限边界
如果后续安全要求更高,可以继续扩展:
- 绑定接口权限;
- 区分只读和读写;
- 限制来源 IP;
- 增加调用频率限制;
- 记录最近使用时间;
- 记录 API Key 调用日志。
十一、完整调用链路
用户点击生成 API Key ↓ 前端调用生成接口 ↓ 后端获取当前登录用户 ID ↓ 生成 ak- + 无连字符 UUID ↓ 计算过期时间 ↓ 保存完整 Key 到 api_key 表 ↓ 前端弹窗展示完整 API Key ↓ 关闭弹窗后,列表接口只返回脱敏 Key ↓ 外部系统通过 Authorization 请求头携带完整 Key ↓ 后端拦截器提取 Bearer Token ↓ 判断是否为 ak- 开头 ↓ 查询 api_key 表 ↓ 校验存在性、删除状态、过期状态 ↓ 切换到 API Key 所属用户身份 ↓ 写入请求上下文 ↓ 放行业务接口十二、总结
这是一种典型的数据库查表型 API Key 机制。
它的核心实现是:
生成阶段:ak- + 随机字符串,保存完整 Key 到数据库 展示阶段:生成时展示一次完整 Key,列表页只返回脱敏 Key 鉴权阶段:请求头携带完整 Key,后端查库校验该方案的优点是实现简单、状态可控、撤销方便,并且通过“只展示一次全文 + 列表脱敏展示”的方式降低了 Key 泄露风险。
它适合用于第三方系统调用、自动化脚本、AI Agent、内部工具和轻量级服务集成等场景。
