用户访问控制
通过 JWT 实现应用 Key 的用户级别访问控制
概述
对于应用类型的 API Key,您可以开启用户级别的访问控制。网关会在处理请求时校验客户端传入的 JWT Token。
适用场景:当多个用户共享同一个应用 Key 调用 AI 服务时,通过 JWT 区分每个用户的身份。
- 了解哪些员工使用频率最高
- 发现异常用量(如某个账号的 Token 消耗远超平均水平)
- 按部门或人员做成本核算
此时只需将 Key 的类型设为「应用」,打开「按用户统计」开关,并配置域名白名单。客户端在调用时额外传入包含员工身份信息的 JWT,网关即可自动区分每个用户的用量,在管理后台的统计页面中按用户维度展示 Token 消耗数据。
工作原理
网关的访问控制流程如下:
客户端携带 JWT 发起请求
客户端在请求头中添加 X-JWT-Token,值为已签发的 JWT Token。
网关验证 API Key
网关首先验证 API Key 的有效性,然后判断该 Key 的类型是否为「应用」且开启了按用户统计。
验证 JWT Token
网关使用该 Key 关联的 secret 对 JWT 进行签名校验。
域名白名单校验
网关从 JWT payload 的 email 字段中提取域名,与 Key 配置的域名白名单进行比对。
请求转发与用量统计
验证全部通过后,网关将请求转发至 AI 供应商,并在日志和统计中关联用户信息。
配置步骤
创建应用类型 Key
在管理后台的「Key 管理」页面,点击「创建 Key」。
- 类型:选择「应用」
- 是否按用户统计:开启此选项:开启此选项
- 域名白名单:配置允许访问的邮箱域名:配置允许访问的邮箱域名(如
example.com)
重要:重要:Key 创建成功后,系统会返回一个 secret,请妥善保存。
配置域名白名单
在「系统设置」→「域名管理」页面,配置允许的邮箱域名白名单。
示例:如果域名白名单配置为 example.com,则只有 *@example.com 的用户才能通过校验。
服务端生成 JWT Token
在您的服务端使用 Key 对应的 secret 签发 JWT Token。
{
"name": "张三", // 必填:用户真实姓名
"email": "zhangsan@example.com", // 必填:用户邮箱(用于域名校验)
"dept_name": "技术部", // 可选:用户所属部门(用于日志和统计)
"iat": 1715328000, // 签发时间(Unix 秒级时间戳)
"exp": 1715331600 // 过期时间(Unix 秒级时间戳)
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 用户真实姓名 |
| string | 是 | 用户邮箱(域名部分用于白名单校验) | |
| dept_name | string | 否 | 用户所属部门(用于日志和统计) |
| iat | number | 建议 | 签发时间(Unix 秒级时间戳) |
| exp | number | 建议 | 过期时间(Unix 秒级时间戳) |
客户端携带 JWT 调用 API
客户端在发起 API 请求时,需要在请求头中同时携带 API Key 和 JWT Token。
Authorization头:Bearer <API_KEY>X-JWT-Token头:服务端签发的 JWT Token
签名算法
Tarogo AI 网关支持以下 JWT 签名算法。
| 算法 | 类型 | 密钥 | 适用场景 |
|---|---|---|---|
| HS256 | 对称加密 | 共享 Secret | 服务端签发和验证使用同一密钥,配置简单 |
| HS384 | 对称加密 | 共享 Secret | 更高安全级别,签名长度更长 |
| HS512 | 对称加密 | 共享 Secret | 最高安全级别的 HMAC 签名 |
| RS256 | 非对称加密 | 私钥 + 公钥 | 服务端持私钥签发,网关持公钥验证,更安全 |
| RS384 / RS512 | 非对称加密 | 私钥 + 公钥 | 更高安全级别的 RSA 签名 |
HMAC 对称加密模式(HS256)
对称加密模式下,服务端和网关共享同一个 secret 密钥。
获取 Secret
创建应用 Key 并开启「按用户统计」后,系统会自动生成一个 secret。
安全提示:安全提示:secret 相当于密码,请存储在服务端环境变量中。
服务端签发 JWT
服务端使用 secret 对 JWT 进行 HMAC-SHA256 签名。
客户端使用流程
客户端向您的服务端请求 JWT Token
客户端在请求头中携带 JWT Token 调用 API
网关验证 JWT Token 并转发请求
const crypto = require('crypto');
// JWT 生成函数
function base64UrlEncode(obj) {
return Buffer.from(JSON.stringify(obj))
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function generateJWT(payload, secret) {
const header = { alg: 'HS256', typ: 'JWT' };
const encodedHeader = base64UrlEncode(header);
const encodedPayload = base64UrlEncode(payload);
const signature = crypto
.createHmac('sha256', secret)
.update(encodedHeader + '.' + encodedPayload)
.digest('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return encodedHeader + '.' + encodedPayload + '.' + signature;
}
// 生成 JWT Token
const payload = {
name: '张三',
email: 'zhangsan@example.com',
dept_name: '技术部',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // 1 小时过期
};
const secret = 'YOUR_KEY_SECRET'; // Key 创建成功后返回的 secret
const token = generateJWT(payload, secret);
console.log('JWT Token:', token);RSA 非对称加密模式(RS256)
非对称加密模式下,服务端持有私钥签发 JWT,网关持有公钥验证。安全性更高。
第一步:生成 RSA 密钥对
在您的服务端使用 OpenSSL 生成 RSA 私钥和公钥:
# ===== 服务端:生成 RSA 密钥对 =====
# 生成 RSA 私钥(2048 位)
openssl genrsa -out private_key.pem 2048
# 从私钥提取公钥
openssl rsa -in private_key.pem -pubout -out public_key.pem
# 查看公钥内容(复制此内容配置到管理后台)
cat public_key.pem- 私钥(
private_key.pem):妥善保存在服务端,切勿泄露 - 公钥(
public_key.pem):配置到 Tarogo AI 管理后台
第二步:配置公钥到管理后台
创建应用 Key 时,在「JWT 签名算法」中选择 RS256,然后将公钥内容粘贴到公钥配置框中。
网关在收到请求后,会使用您配置的公钥验证 JWT 的 RS256 签名。私钥仅由您的服务端保管,网关无法签发 JWT,即使公钥泄露也不会影响签名安全。
第三步:服务端使用私钥签发 JWT
服务端使用私钥对 JWT 进行 RSA-SHA256 签名,JWT header 中的 alg 字段应设置为 RS256。
客户端使用流程
客户端向您的服务端请求 JWT Token(与 HS256 模式完全相同)
服务端使用私钥签发 JWT(而非共享 secret)
客户端将 JWT 附加到 X-JWT-Token 请求头
网关使用您配置的公钥验证签名
import crypto from 'crypto';
import fs from 'fs';
// 服务端:使用私钥签发 JWT(RS256)
function base64UrlEncode(obj: object | string): string {
const input = typeof obj === 'string' ? obj : JSON.stringify(obj);
return Buffer.from(input)
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function signRS256(data: string, privateKey: string): string {
const sign = crypto.createSign('RSA-SHA256');
sign.update(data);
sign.end();
return sign.sign(privateKey, 'base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
const privateKey = fs.readFileSync('private_key.pem', 'utf8');
const header = base64UrlEncode({ alg: 'RS256', typ: 'JWT' });
const payload = base64UrlEncode({
name: '张三',
email: 'zhangsan@example.com',
dept_name: '技术部',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
});
const token = header + '.' + payload + '.' + signRS256(header + '.' + payload, privateKey);
console.log('JWT Token:', token);
// 客户端:使用 OpenAI SDK 调用
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://your-gateway-domain.com/v1',
defaultHeaders: { 'X-JWT-Token': token },
});API 调用示例
无论使用 HS256 还是 RS256,客户端的调用方式完全相同——只需在请求头中添加 X-JWT-Token。
使用 cURL
curl -X POST https://your-gateway-domain.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-JWT-Token: YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat@deepseek",
"messages": [
{
"role": "user",
"content": "你好,请介绍一下自己"
}
]
}'使用 OpenAI SDK(Node.js)
import crypto from 'crypto';
function generateJWT(payload: object, secret: string): string {
const base64UrlEncode = (obj: object) =>
Buffer.from(JSON.stringify(obj))
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const encodedHeader = base64UrlEncode({ alg: 'HS256', typ: 'JWT' });
const encodedPayload = base64UrlEncode(payload);
const signature = crypto
.createHmac('sha256', secret)
.update(encodedHeader + '.' + encodedPayload)
.digest('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return encodedHeader + '.' + encodedPayload + '.' + signature;
}
const token = generateJWT(
{
name: '张三',
email: 'zhangsan@example.com',
dept_name: '技术部',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
},
'YOUR_KEY_SECRET'
);
// 使用 OpenAI SDK 调用
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://your-gateway-domain.com/v1',
defaultHeaders: {
'X-JWT-Token': token,
},
});
const response = await client.chat.completions.create({
model: 'deepseek-chat@deepseek',
messages: [{ role: 'user', content: '你好,请介绍一下自己' }],
});常见错误
| 错误码 | 说明 | 解决方法 |
|---|---|---|
MISS_USERINFO | X-JWT-Token header 缺失 | 确保请求中包含 X-JWT-Token 请求头 |
INVALID_USERINFO | JWT 校验失败 | 检查 JWT Token 是否有效、secret 是否正确、payload 中是否包含 email 和 name |
INVALID_USERINFO (domain) | 邮箱域名不在允许列表中 | 确认用户邮箱域名已在 Key 的域名白名单中配置 |
401 Unauthorized | API Key 无效或已过期 | 检查 API Key 是否正确、是否处于启用状态、是否在有效期内 |
注意事项
- 访问控制仅在 Key 类型为「应用」且开启「按用户统计」时生效,个人 Key 不经过 JWT 校验
- JWT Token 的签发必须在服务端完成,不要在客户端直接使用 secret
- 建议为 JWT 设置合理的过期时间(如 1 小时),避免长期有效的 Token 泄露后被滥用
- secret 是敏感信息,需妥善保管。如果怀疑 secret 泄露,可通过管理后台「重置 Key」功能更换
- 域名白名单的修改会实时生效,无需重新创建 Key
- 网关日志中会记录 JWT 中的用户信息(email、name、dept_name),便于审计和问题排查