API 鉴权方式设计方案
概述
Coze Plus 提供了灵活的多层鉴权体系,支持 Web 应用和 OpenAPI 两种访问方式,满足不同场景下的安全认证需求。系统通过中间件链式调用实现了统一的鉴权管理,确保 API 的安全性和可扩展性。
鉴权架构设计
架构层次
┌─────────────────────────────────────────────────────────┐
│ Client Request │
│ - Web Browser (Cookie) │
│ - API Client (Bearer Token) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Request Inspector Middleware │
│ - Detect Request Type │
│ - Set Auth Type (WebAPI/OpenAPI/StaticFile) │
└────────────────────┬────────────────────────────────────┘
│
┌────────────┴─────────────┐
│ │
┌───────▼──────────┐ ┌──────────▼─────────┐
│ Session Auth MW │ │ OpenAPI Auth MW │
│ (Web Console) │ │ (API Access) │
│ - Cookie Check │ │ - Bearer Token │
│ - Session Valid │ │ - API Key Check │
└───────┬──────────┘ └──────────┬─────────┘
│ │
└────────────┬─────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Permission Middleware │
│ - Check User Permissions │
│ - Space Role Validation │
└────────────────────┬────────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────────┐
│ Business Handler │
└─────────────────────────────────────────────────────────┘鉴权类型(RequestAuthType)
系统支持三种请求鉴权类型:
type RequestAuthType = int32
const (
RequestAuthTypeWebAPI RequestAuthType = 0 // Web 控制台鉴权
RequestAuthTypeOpenAPI RequestAuthType = 1 // OpenAPI 鉴权
RequestAuthTypeStaticFile RequestAuthType = 2 // 静态文件(无需鉴权)
)| 类型 | 值 | 描述 | 鉴权方式 | 适用场景 |
|---|---|---|---|---|
| WebAPI | 0 | Web 控制台访问 | Session Cookie | 用户通过浏览器访问控制台 |
| OpenAPI | 1 | API 接口访问 | Bearer Token (API Key) | 第三方应用、脚本调用 API |
| StaticFile | 2 | 静态资源访问 | 无需鉴权 | 前端静态文件、公共资源 |
鉴权方式详解
1. Session 鉴权(Web 控制台)
工作原理
Session 鉴权用于 Web 控制台的用户认证,采用传统的 Cookie-Session 模式:
- 用户登录成功后,服务器生成唯一的 Session ID
- Session ID 通过 Cookie 返回给浏览器
- 后续请求自动携带 Cookie,服务器验证 Session 有效性
- Session 关联用户信息,实现有状态会话
实现细节
Session 实体结构:
type Session struct {
SessionKey string `json:"session_key"` // Session 唯一标识
UserID int64 `json:"user_id"` // 用户ID
UserEmail string `json:"user_email"` // 用户邮箱
UniqueName string `json:"unique_name"` // 用户名
IconURL string `json:"icon_url"` // 头像URL
SpaceID int64 `json:"space_id"` // 当前工作空间ID
CreatedAt int64 `json:"created_at"` // 创建时间
ExpiresAt int64 `json:"expires_at"` // 过期时间
}登录流程:
func Login(ctx context.Context, email, password string) (*User, error) {
// 1. 验证用户凭证
user, err := userRepo.GetUsersByEmail(ctx, email)
if err != nil || !verifyPassword(password, user.Password) {
return nil, errors.New("invalid credentials")
}
// 2. 生成唯一 Session ID
sessionID, err := idGenerator.GenID(ctx)
if err != nil {
return nil, err
}
// 3. 生成 Session Key(HMAC-SHA256)
sessionKey := generateSessionKey(sessionID)
// 4. 存储 Session Key 到数据库
err = userRepo.UpdateSessionKey(ctx, user.ID, sessionKey)
if err != nil {
return nil, err
}
// 5. 设置 Cookie
c.SetCookie("session_key", sessionKey,
maxAge, "/", domain, secure, httpOnly)
return user, nil
}Session 验证中间件:
func SessionAuthMW() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
// 1. 检查请求类型
requestAuthType := ctx.GetInt32(RequestAuthTypeStr)
if requestAuthType != int32(RequestAuthTypeWebAPI) {
ctx.Next(c)
return
}
// 2. 跳过不需要检查的路径
if noNeedSessionCheckPath[string(ctx.GetRequest().URI().Path())] {
ctx.Next(c)
return
}
// 3. 获取 Cookie 中的 session_key
sessionKey := ctx.Cookie("session_key")
if len(sessionKey) == 0 {
httputil.Unauthorized(ctx, "missing session_key in cookie")
return
}
// 4. 验证 Session 有效性
session, err := userService.ValidateSession(c, string(sessionKey))
if err != nil || session == nil {
httputil.Unauthorized(ctx, "invalid or expired session")
return
}
// 5. 将 Session 数据存入上下文
ctxcache.Store(c, consts.SessionDataKeyInCtx, session)
ctx.Next(c)
}
}免鉴权路径:
var noNeedSessionCheckPath = map[string]bool{
"/api/passport/web/email/login/": true, // 登录接口
"/api/passport/web/email/register/v2/": true, // 注册接口
}优势与限制
优势:
- ✅ 实现简单,服务器完全控制会话
- ✅ 支持服务器主动失效(登出、过期)
- ✅ 适合有状态的 Web 应用
- ✅ 安全性高(HttpOnly Cookie 防止 XSS)
限制:
- ❌ 需要服务器存储 Session 状态
- ❌ 跨域访问需要特殊处理
- ❌ 不适合分布式无状态架构
- ❌ 移动端和第三方应用支持不佳
2. API Key 鉴权(OpenAPI)
工作原理
API Key 鉴权用于第三方应用和程序化访问,采用 Bearer Token 模式:
- 用户在控制台创建 API Key
- API Key 以 MD5 哈希形式存储在数据库
- 客户端在 HTTP Header 中携带
Authorization: Bearer {API_KEY} - 服务器验证 API Key 的有效性和权限
API Key 实体结构
type ApiKey struct {
ID int64 `json:"id"` // API Key ID
Name string `json:"name"` // API Key 名称
ApiKey string `json:"api_key"` // API Key(MD5哈希)
ConnectorID int64 `json:"connector"` // 关联的连接器ID
UserID int64 `json:"user_id"` // 所有者用户ID
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
ExpiredAt int64 `json:"expired_at"` // 过期时间
CreatedAt int64 `json:"created_at"` // 创建时间
UpdatedAt int64 `json:"updated_at"` // 更新时间
}
// API Key 类型
type AkType int32
const (
AkTypeCustomer AkType = 0 // 客户 API Key(长期有效)
AkTypeTemporary AkType = 1 // 临时 API Key(短期有效)
)数据库表设计
CREATE TABLE IF NOT EXISTS `api_key` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'Primary Key ID',
`api_key` varchar(255) NOT NULL DEFAULT '' COMMENT 'API Key hash (MD5)',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT 'API Key Name',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0 normal, 1 deleted',
`user_id` bigint NOT NULL DEFAULT 0 COMMENT 'API Key Owner',
`expired_at` bigint NOT NULL DEFAULT 0 COMMENT 'API Key Expired Time',
`created_at` bigint unsigned NOT NULL DEFAULT 0 COMMENT 'Create Time in Milliseconds',
`updated_at` bigint unsigned NOT NULL DEFAULT 0 COMMENT 'Update Time in Milliseconds',
`last_used_at` bigint NOT NULL DEFAULT 0 COMMENT 'Used Time in Milliseconds',
`ak_type` tinyint NOT NULL DEFAULT 0 COMMENT 'API key type',
PRIMARY KEY (`id`),
INDEX `idx_api_key` (`api_key`),
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'API Key Table';API Key 创建流程
func CreateApiKey(ctx context.Context, req *CreateApiKey) (*ApiKey, error) {
// 1. 生成随机 API Key(32字节)
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
return nil, err
}
apiKey := base64.URLEncoding.EncodeToString(randomBytes)
// 2. 计算 MD5 哈希(用于存储)
md5Hash := md5.Sum([]byte(apiKey))
md5Key := hex.EncodeToString(md5Hash[:])
// 3. 计算过期时间
expiredAt := time.Now().Add(req.Expire * time.Second).UnixMilli()
// 4. 存储到数据库
apiKeyModel := &model.APIKey{
ID: genID(),
APIKey: md5Key, // 存储 MD5 哈希
Name: req.Name,
UserID: req.UserID,
ExpiredAt: expiredAt,
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
AkType: int32(req.AkType),
}
err = dao.Create(ctx, apiKeyModel)
if err != nil {
return nil, err
}
// 5. 返回明文 API Key(只显示一次)
return &ApiKey{
ID: apiKeyModel.ID,
Name: apiKeyModel.Name,
ApiKey: apiKey, // 明文,仅在创建时返回
UserID: apiKeyModel.UserID,
ExpiredAt: expiredAt,
}, nil
}API Key 验证中间件
func OpenapiAuthMW() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
// 1. 检查请求类型
requestAuthType := c.GetInt32(RequestAuthTypeStr)
if requestAuthType != int32(RequestAuthTypeOpenAPI) {
c.Next(ctx)
return
}
// 2. 获取 Authorization Header
authHeader := c.Request.Header.Get("Authorization")
if len(authHeader) == 0 {
httputil.Unauthorized(c, "missing authorization in header")
return
}
// 3. 解析 Bearer Token
apiKey := parseBearerAuthToken(authHeader)
if len(apiKey) == 0 {
httputil.Unauthorized(c, "invalid bearer token format")
return
}
// 4. 计算 API Key 的 MD5 哈希
md5Hash := md5.Sum([]byte(apiKey))
md5Key := hex.EncodeToString(md5Hash[:])
// 5. 验证 API Key
apiKeyInfo, err := openauth.CheckPermission(ctx, md5Key)
if err != nil || apiKeyInfo == nil {
httputil.Unauthorized(c, "invalid or expired api key")
return
}
// 6. 检查过期时间
if apiKeyInfo.ExpiredAt > 0 && time.Now().UnixMilli() > apiKeyInfo.ExpiredAt {
httputil.Unauthorized(c, "api key expired")
return
}
// 7. 更新最后使用时间
_ = openauth.UpdateLastUsedAt(ctx, apiKeyInfo.ID, apiKeyInfo.UserID)
// 8. 将 API Key 信息存入上下文
ctxcache.Store(ctx, consts.OpenapiAuthKeyInCtx, apiKeyInfo)
c.Next(ctx)
}
}
// 解析 Bearer Token
func parseBearerAuthToken(authHeader string) string {
if len(authHeader) == 0 {
return ""
}
// 格式: "Bearer {token}"
parts := strings.Split(authHeader, "Bearer")
if len(parts) != 2 {
return ""
}
token := strings.TrimSpace(parts[1])
return token
}需要 OpenAPI 鉴权的路径
固定路径:
var needAuthPath = map[string]bool{
"/v3/chat": true, // 聊天接口
"/v1/conversations": true, // 会话管理
"/v1/conversation/create": true, // 创建会话
"/v1/conversation/message/list": true, // 消息列表
"/v1/files/upload": true, // 文件上传
"/v1/workflow/run": true, // 工作流运行
"/v1/workflow/stream_run": true, // 流式工作流
"/v1/workflow/stream_resume": true, // 恢复工作流
"/v1/workflow/get_run_history": true, // 运行历史
"/v1/bot/get_online_info": true, // 获取Bot信息
"/v1/workflows/chat": true, // 工作流聊天
"/v1/workflow/conversation/create": true, // 工作流会话
"/v3/chat/cancel": true, // 取消聊天
}正则路径:
var needAuthFunc = map[string]bool{
"^/v1/conversations/[0-9]+/clear$": true, // 清空会话
"^/v1/bots/[0-9]+$": true, // 获取Bot
"^/v1/conversations/[0-9]+$": true, // 获取会话
"^/v1/workflows/[0-9]+$": true, // 获取工作流
"^/v1/apps/[0-9]+$": true, // 获取应用
}
func isNeedOpenapiAuth(c *app.RequestContext) bool {
uriPath := c.URI().Path()
// 检查正则路径
for rule, needAuth := range needAuthFunc {
if regexp.MustCompile(rule).MatchString(string(uriPath)) {
return needAuth
}
}
// 检查固定路径
return needAuthPath[string(uriPath)]
}优势与限制
优势:
- ✅ 无状态,易于分布式部署
- ✅ 适合第三方应用和程序化访问
- ✅ 支持长期有效的访问令牌
- ✅ 可以设置过期时间和权限范围
- ✅ 易于撤销(删除数据库记录)
限制:
- ❌ API Key 泄露风险较高
- ❌ 无法像 Session 一样实时失效
- ❌ 需要客户端妥善保管密钥
请求鉴权流程
请求类型识别(Request Inspector)
系统通过 RequestInspectorMW 中间件自动识别请求类型:
func RequestInspectorMW() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
authType := RequestAuthTypeWebAPI // 默认为 Web API
// 1. 检查是否需要 OpenAPI 鉴权
if isNeedOpenapiAuth(ctx) {
authType = RequestAuthTypeOpenAPI
}
// 2. 检查是否是静态文件
else if isStaticFile(ctx) {
authType = RequestAuthTypeStaticFile
}
// 3. 将鉴权类型存入上下文
ctx.Set(RequestAuthTypeStr, authType)
ctx.Next(c)
}
}
// 静态文件路径
var staticFilePath = map[string]bool{
"/static": true,
"/": true,
"/sign": true,
"/favicon.png": true,
}
func isStaticFile(ctx *app.RequestContext) bool {
path := string(ctx.GetRequest().URI().Path())
// 检查固定路径
if staticFilePath[path] {
return true
}
// 检查路径前缀
if strings.HasPrefix(path, "/static/") ||
strings.HasPrefix(path, "/explore/") ||
strings.HasPrefix(path, "/admin/") ||
strings.HasPrefix(path, "/space/") {
return true
}
return false
}完整鉴权流程图
┌─────────────────────────────────────────────────────────────┐
│ Incoming Request │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ RequestInspectorMW │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Path Analysis: │ │
│ │ - Check if needAuthPath or needAuthFunc │ │
│ │ - Check if staticFilePath │ │
│ │ - Determine RequestAuthType │ │
│ └─────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────┐ ┌─────────────┐
│ WebAPI (0) │ │ OpenAPI (1) │ │StaticFile(2)│
│ │ │ │ │ │
│ SessionAuthMW │ │OpenapiAuthMW │ │ No Auth │
└───────┬───────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│Cookie Check │ │Bearer Token │ │
│Session Valid │ │API Key Check │ │
└──────┬───────┘ └──────┬───────┘ │
│ │ │
└────────────────┼────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PermissionMiddleware │
│ - Check Space Membership │
│ - Validate Resource Access │
│ - Role-based Permission Check │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Business Handler │
│ - Process Request │
│ - Return Response │
└─────────────────────────────────────────────────────────────┘API 管理
API Key 生命周期管理
创建 API Key
POST /api/openapi/v1/api_key/create
Content-Type: application/json
{
"name": "Production API Key",
"expire": 7776000, // 90天(秒)
"ak_type": 0 // 0: 客户Key, 1: 临时Key
}
Response:
{
"code": 0,
"msg": "success",
"data": {
"id": 123456,
"name": "Production API Key",
"api_key": "coz_abc123def456ghi789jkl...", // 明文,仅显示一次
"user_id": 1001,
"expired_at": 1756723200000,
"created_at": 1756723200000
}
}重要提示:
- ⚠️ API Key 明文仅在创建时返回一次,请妥善保管
- 🔒 后端存储 MD5 哈希,无法恢复明文
- ⏰ 过期时间单位为秒,0 表示永不过期
列出 API Keys
GET /api/openapi/v1/api_key/list?page=1&limit=20
Authorization: session_key_from_cookie
Response:
{
"code": 0,
"msg": "success",
"data": {
"api_keys": [
{
"id": 123456,
"name": "Production API Key",
"api_key": "coz_abc...***...", // 脱敏显示
"user_id": 1001,
"last_used_at": 1756723100000,
"expired_at": 1756723200000,
"created_at": 1756723000000
}
],
"has_more": false
}
}删除 API Key
POST /api/openapi/v1/api_key/delete
Content-Type: application/json
{
"id": 123456
}
Response:
{
"code": 0,
"msg": "success"
}管理员鉴权
针对管理功能,系统提供了额外的管理员鉴权中间件:
func AdminAuthMW() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
// 1. 获取 Session 数据
session, ok := ctxcache.Get[*entity.Session](c, consts.SessionDataKeyInCtx)
if !ok {
httputil.Unauthorized(ctx, "session required")
return
}
// 2. 获取管理员邮箱配置
baseConf, err := config.Base().GetBaseConfig(c)
if err != nil {
httputil.InternalError(c, ctx, err)
return
}
// 3. 检查用户邮箱是否在管理员列表中
adminEmails := strings.Split(baseConf.AdminEmails, ",")
for _, adminEmail := range adminEmails {
if adminEmail == session.UserEmail {
ctx.Next(c)
return
}
}
httputil.Forbidden(ctx, "admin access required")
}
}管理员配置:
管理员邮箱在配置文件中定义:
# backend/conf/base/base.yaml
admin_emails: "admin@example.com,superuser@example.com"使用示例:
// 管理员专用路由
adminRouter.POST("/api/admin/users/list",
middleware.SessionAuthMW(),
middleware.AdminAuthMW(),
handler.ListUsers,
)安全最佳实践
1. Session 安全
Cookie 配置:
// 设置安全的 Cookie 属性
c.SetCookie(
"session_key", // name
sessionKey, // value
86400, // maxAge (1天)
"/", // path
"yourdomain.com", // domain
true, // secure (仅HTTPS)
true, // httpOnly (防XSS)
)Session 过期策略:
- ✅ 设置合理的过期时间(推荐1-7天)
- ✅ 支持"记住我"功能(延长过期时间)
- ✅ 登出时立即失效 Session
- ✅ 检测异常登录行为(IP变化、设备变化)
2. API Key 安全
存储安全:
- ✅ 后端仅存储 MD5 哈希,不存储明文
- ✅ 使用 HTTPS 传输 API Key
- ✅ 客户端使用环境变量或密钥管理服务存储
权限控制:
- ✅ API Key 与用户账号关联,继承用户权限
- ✅ 支持设置过期时间,定期轮换
- ✅ 记录
last_used_at,检测异常使用 - ✅ 支持立即撤销(删除记录)
使用建议:
# ❌ 错误:硬编码在代码中
api_key = "coz_abc123def456ghi789jkl..."
# ✅ 正确:使用环境变量
api_key = os.environ.get("COZE_API_KEY")
# ✅ 正确:使用配置文件(不提交到版本控制)
api_key = config.get("coze_api_key")3. 防止常见攻击
CSRF 防护
Session 鉴权需要防范 CSRF 攻击:
// 生成 CSRF Token
csrfToken := generateCSRFToken(sessionKey)
c.SetCookie("csrf_token", csrfToken, 86400, "/", "", true, false)
// 验证 CSRF Token
func CSRFMiddleware() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
if ctx.Request.Method() != "GET" {
cookieToken := ctx.Cookie("csrf_token")
headerToken := ctx.Request.Header.Get("X-CSRF-Token")
if cookieToken != headerToken {
httputil.Forbidden(ctx, "CSRF token mismatch")
return
}
}
ctx.Next(c)
}
}重放攻击防护
API Key 鉴权需要防范重放攻击:
// 方案1: 添加时间戳和签名
Authorization: Bearer {API_KEY}
X-Timestamp: 1756723200
X-Signature: hmac_sha256(API_KEY + timestamp + request_body)
// 方案2: 使用 nonce(一次性随机数)
Authorization: Bearer {API_KEY}
X-Nonce: random_string_123
// 服务器记录已使用的 nonce(Redis)限流保护
防止暴力破解和 DDoS 攻击:
// 基于 IP 的限流
func RateLimitMiddleware() app.HandlerFunc {
limiter := NewRateLimiter(100, time.Minute) // 每分钟100次
return func(c context.Context, ctx *app.RequestContext) {
clientIP := ctx.ClientIP()
if !limiter.Allow(clientIP) {
httputil.TooManyRequests(ctx, "rate limit exceeded")
return
}
ctx.Next(c)
}
}
// 基于 API Key 的限流
func APIKeyRateLimitMiddleware() app.HandlerFunc {
limiter := NewRateLimiter(1000, time.Hour) // 每小时1000次
return func(c context.Context, ctx *app.RequestContext) {
apiKeyInfo := ctxcache.Get[*entity.ApiKey](c, consts.OpenapiAuthKeyInCtx)
keyID := fmt.Sprintf("api_key:%d", apiKeyInfo.ID)
if !limiter.Allow(keyID) {
httputil.TooManyRequests(ctx, "api key rate limit exceeded")
return
}
ctx.Next(c)
}
}开发示例
客户端调用示例
Web 控制台(Session)
// 登录
async function login(email, password) {
const response = await fetch('/api/passport/web/email/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 重要:携带 Cookie
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (data.code === 0) {
console.log('登录成功,Session 已设置');
}
}
// 调用受保护的 API
async function createAgent(agentData) {
const response = await fetch('/api/agent/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token'), // CSRF 保护
},
credentials: 'include', // 自动携带 session_key Cookie
body: JSON.stringify(agentData),
});
return await response.json();
}OpenAPI 调用(API Key)
import requests
# API Key(从环境变量读取)
API_KEY = os.environ.get('COZE_API_KEY')
BASE_URL = 'https://api.your-coze-plus.com'
# 调用 Chat API
def chat(bot_id, message):
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
}
data = {
'bot_id': bot_id,
'user': 'user_123',
'query': message,
'stream': False,
}
response = requests.post(
f'{BASE_URL}/v3/chat',
headers=headers,
json=data,
)
return response.json()
# 调用工作流 API
def run_workflow(workflow_id, inputs):
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
}
data = {
'workflow_id': workflow_id,
'parameters': inputs,
}
response = requests.post(
f'{BASE_URL}/v1/workflow/run',
headers=headers,
json=data,
)
return response.json()# 使用 cURL 调用 API
curl -X POST https://api.your-coze-plus.com/v3/chat \
-H "Authorization: Bearer coz_abc123def456ghi789jkl..." \
-H "Content-Type: application/json" \
-d '{
"bot_id": "7434343434343434",
"user": "user_123",
"query": "你好,请介绍一下自己",
"stream": false
}'服务端开发示例
添加新的受保护路由
// 1. 定义 Handler
func CreateWorkflow(ctx context.Context, c *app.RequestContext) {
// 获取认证信息
var userID int64
// 从 Session 获取(WebAPI)
if session, ok := ctxcache.Get[*entity.Session](ctx, consts.SessionDataKeyInCtx); ok {
userID = session.UserID
}
// 从 API Key 获取(OpenAPI)
if apiKey, ok := ctxcache.Get[*entity.ApiKey](ctx, consts.OpenapiAuthKeyInCtx); ok {
userID = apiKey.UserID
}
// 业务逻辑
workflow, err := workflowService.Create(ctx, userID, req)
// ...
}
// 2. 注册路由(支持两种鉴权方式)
func RegisterRoutes(r *server.Hertz) {
// 中间件链
r.Use(
middleware.RequestInspectorMW(), // 识别请求类型
middleware.SessionAuthMW(), // Session 鉴权
middleware.OpenapiAuthMW(), // API Key 鉴权
middleware.PermissionMW(), // 权限检查
)
// 注册路由
r.POST("/api/workflow/create", CreateWorkflow)
r.POST("/v1/workflow/create", CreateWorkflow) // OpenAPI 路径
}添加需要 OpenAPI 鉴权的路径
// 在 openapi_auth.go 中添加路径
var needAuthPath = map[string]bool{
// ... 现有路径
"/v1/my_new_api": true, // 新增固定路径
}
var needAuthFunc = map[string]bool{
// ... 现有正则
"^/v1/my_resource/[0-9]+$": true, // 新增正则路径
}监控与审计
鉴权日志
// 记录鉴权日志
func logAuthEvent(ctx context.Context, authType string, userID int64, result bool, reason string) {
log.Info().
Str("auth_type", authType).
Int64("user_id", userID).
Bool("success", result).
Str("reason", reason).
Str("ip", getClientIP(ctx)).
Str("user_agent", getUserAgent(ctx)).
Msg("Authentication Event")
}监控指标
关键监控指标:
- 认证成功率:
auth_success_rate{auth_type="session|api_key"} - 认证失败原因分布:
auth_failure_reason{reason="invalid_token|expired|missing"} - API Key 使用频率:
api_key_requests{api_key_id="123"} - Session 并发数:
active_sessions_count - 鉴权延迟:
auth_middleware_duration_ms
审计日志
-- 审计日志表
CREATE TABLE `auth_audit_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`auth_type` varchar(20) NOT NULL COMMENT 'session|api_key',
`action` varchar(50) NOT NULL COMMENT 'login|logout|api_call',
`result` tinyint NOT NULL COMMENT '0: fail, 1: success',
`reason` varchar(255) COMMENT 'failure reason',
`ip_address` varchar(45) NOT NULL,
`user_agent` varchar(500),
`request_path` varchar(500),
`created_at` bigint unsigned NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB CHARSET utf8mb4;常见问题
Q1: Session 和 API Key 可以同时使用吗?
A: 可以。系统通过 RequestInspectorMW 自动识别请求类型:
- 如果路径匹配 OpenAPI 路径,使用 API Key 鉴权
- 否则使用 Session 鉴权
- 同一个用户可以同时拥有 Session 和多个 API Keys
Q2: API Key 泄露后如何快速响应?
A: 立即执行以下步骤:
- 撤销 API Key:调用删除接口或在控制台删除
- 审计使用记录:检查
last_used_at和访问日志 - 通知用户:如果检测到异常使用,通知 API Key 所有者
- 生成新密钥:创建新的 API Key 并更新客户端配置
Q3: 如何实现 API Key 的权限隔离?
A: API Key 通过 user_id 关联到用户账号,自动继承用户的所有权限:
// API Key 鉴权后,获取用户信息
apiKeyInfo := ctxcache.Get[*entity.ApiKey](ctx, consts.OpenapiAuthKeyInCtx)
userID := apiKeyInfo.UserID
// 后续权限检查基于 userID
hasPermission := permissionService.CheckPermission(ctx, &CheckPermissionRequest{
UserID: userID,
SpaceID: &spaceID,
Resource: "agent",
ResourceID: agentID,
Action: "read",
})Q4: Session 在分布式环境下如何共享?
A: 推荐使用 Redis 存储 Session:
// 使用 Redis 存储 Session
func (s *sessionStore) Set(ctx context.Context, sessionKey string, session *entity.Session) error {
data, _ := json.Marshal(session)
return s.redis.Set(ctx, "session:"+sessionKey, data, 24*time.Hour).Err()
}
func (s *sessionStore) Get(ctx context.Context, sessionKey string) (*entity.Session, error) {
data, err := s.redis.Get(ctx, "session:"+sessionKey).Result()
if err != nil {
return nil, err
}
var session entity.Session
json.Unmarshal([]byte(data), &session)
return &session, nil
}Q5: 如何为不同的 API Key 设置不同的权限?
A: 当前实现中,API Key 继承用户权限。如需更细粒度的控制,可以扩展 API Key 实体:
type ApiKey struct {
// ... 现有字段
Scopes string `json:"scopes"` // 权限范围,如 "agent:read,workflow:execute"
AllowedIPs string `json:"allowed_ips"` // 允许的IP白名单
}
// 验证时检查 Scopes
func validateAPIKeyScope(apiKey *ApiKey, resource, action string) bool {
scopes := strings.Split(apiKey.Scopes, ",")
requiredScope := fmt.Sprintf("%s:%s", resource, action)
for _, scope := range scopes {
if scope == requiredScope || scope == resource+":*" {
return true
}
}
return false
}总结
Coze Plus 的 API 鉴权系统具有以下特点:
✅ 双模式支持:Session(Web 控制台)+ API Key(OpenAPI) ✅ 自动识别:通过路径自动选择鉴权方式 ✅ 安全设计:MD5 哈希存储、HttpOnly Cookie、HTTPS 传输 ✅ 灵活扩展:支持管理员鉴权、权限集成、自定义中间件 ✅ 易于监控:完善的日志、审计和监控指标
通过合理使用 Session 和 API Key 两种鉴权方式,Coze Plus 为不同场景提供了安全、便捷的 API 访问能力。
