AI应用架构师避坑:智能文本生成系统的API鉴权问题
AI应用架构师避坑指南:智能文本生成系统的API鉴权设计与实战
副标题:从原理到实践:如何构建安全、可扩展的鉴权体系
摘要/引言
随着大语言模型(LLM)技术的爆发,智能文本生成系统(如ChatGPT、Claude、开源LLaMA等)已成为企业级应用的核心组件。从智能客服、内容创作到代码生成,这些系统通过API接口对外提供服务,支撑着成千上万的业务场景。然而,API鉴权机制的设计缺陷往往成为系统安全的“阿喀琉斯之踵”——轻则导致服务滥用、成本失控,重则引发数据泄露、合规风险,甚至业务中断。
作为AI应用架构师,我们该如何设计既安全又灵活的API鉴权体系?本文将从实际问题出发,深入剖析智能文本生成系统的API鉴权痛点,系统梳理鉴权机制的选型策略,通过实战案例演示从“简陋鉴权”到“企业级安全架构”的演进过程,并总结20+个避坑要点。
读完本文,你将获得:
- 理解智能文本生成系统API鉴权的特殊性与核心挑战;
- 掌握5种主流鉴权机制的优缺点及选型依据;
- 学会设计“安全+可扩展+低成本”的鉴权架构;
- 规避静态API密钥泄露、权限过度授权、限流失效等常见陷阱;
- 获得可直接复用的代码模板与最佳实践清单。
目标读者与前置知识
目标读者
- AI应用架构师、后端工程师、安全工程师
- 负责LLM API集成或智能文本生成系统开发的技术负责人
- 对API安全与系统设计感兴趣的技术人员
前置知识
- 基础:了解HTTP协议、RESTful API设计规范
- 经验:1年以上后端开发经验(Python/Node.js/Java等)
- 概念:熟悉API Key、JWT、OAuth2.0等基础鉴权术语(无需深入)
- 工具:会使用Postman/Curl调试API,了解Docker基础(可选)
文章目录
-
引言与基础
- 问题背景:为什么智能文本生成系统的API鉴权“坑特别多”?
- 核心概念:从“认证”到“授权”,鉴权体系的底层逻辑
-
鉴权机制深度对比:选型前必须知道的5个维度
- API Key:最简单但最危险的选择?
- JWT:无状态的理想与现实的骨感
- OAuth2.0/OIDC:第三方授权的正确打开方式
- HMAC与mTLS:高安全场景下的“小众方案”
- 选型决策树:如何根据场景选对鉴权机制?
-
实战:构建智能文本生成API的鉴权体系(Python+FastAPI)
- 环境准备:从0搭建开发环境
- Step 1:危险的起点——静态API Key的实现与漏洞分析
- Step 2:JWT鉴权实战:生成、验证、刷新与吊销
- Step 3:细粒度权限控制:基于RBAC模型限制“生成能力”
- Step 4:限流与成本控制:防止“一条API调用拖垮公司”
- Step 5:监控与审计:谁在调用API生成了违规内容?
-
避坑指南:20+个架构师必须知道的实战陷阱
- 安全陷阱:从“硬编码密钥”到“JWT签名算法误用”
- 性能陷阱:鉴权逻辑成为API响应延迟的元凶?
- 可扩展性陷阱:多租户、多模型场景下的权限混乱
- 合规陷阱:GDPR/CCPA要求下,鉴权日志该记哪些内容?
-
高级话题:鉴权体系的“进化之路”
- 动态密钥:让静态API Key“活”起来
- 零信任架构:LLM API的终极安全防护?
- AI驱动的异常检测:识别“不正常”的API调用模式
-
总结与工具推荐
一、引言与基础
1.1 问题背景:为什么智能文本生成系统的API鉴权“坑特别多”?
普通API的鉴权目标通常是“防未授权访问”,而智能文本生成系统(尤其是基于LLM的系统)的鉴权要复杂得多——它不仅要“防人”,还要“防钱”“防合规风险”“防内容滥用”。以下是3个让鉴权“坑变多”的核心原因:
1.1.1 成本敏感:一次API调用可能烧掉“一杯咖啡钱”
- LLM API的“按量计费”模式:例如GPT-4 Turbo的API调用成本约为$0.01/1K tokens,若API密钥泄露,恶意用户可能通过批量调用导致单日账单暴增数万元(真实案例:某企业因密钥泄露3天产生$23万账单)。
- 生成内容的“隐性成本”:若未授权用户调用API生成违法/侵权内容,企业可能面临法律诉讼(如欧盟《AI法案》要求生成内容可追溯)。
1.1.2 权限粒度:“能调用API”不代表“能调用所有功能”
普通API的权限通常是“能用/不能用”,而智能文本生成系统需要更细粒度的控制:
- 模型粒度:允许用户调用GPT-3.5但禁止GPT-4(成本控制);
- 功能粒度:允许“文本续写”但禁止“代码生成”(内容安全);
- 参数粒度:限制temperature参数(控制生成随机性)、禁止调用“长上下文模型”(防资源滥用)。
1.1.3 合规要求:鉴权日志可能成为“呈堂证供”
- GDPR/CCPA的“可解释性”要求:用户有权要求“删除所有我的API调用记录”,若鉴权系统未记录用户与调用的关联关系,将无法合规响应;
- 内容审计要求:中国《生成式人工智能服务管理暂行办法》规定“对生成内容进行标识和记录”,而记录的前提是准确识别“谁调用了API”。
1.2 核心概念:从“认证”到“授权”,鉴权体系的底层逻辑
在深入技术细节前,先明确3个核心概念,避免后续混淆:
1.2.1 认证(Authentication):“你是谁?”
- 定义:验证调用者的身份(如“用户A”“服务B”)。
- 智能文本生成系统的特殊需求:
- 不仅要验证“人”的身份,还要验证“调用来源”(如是否来自前端SDK/后端服务/第三方系统);
- 需支持“匿名用户+临时权限”场景(如免费试用的Demo功能)。
1.2.2 授权(Authorization):“你能做什么?”
- 定义:在认证通过后,判断调用者是否有权限执行操作。
- 智能文本生成系统的特殊需求:
- 权限需关联“生成能力”(如模型类型、生成长度、敏感内容过滤开关);
- 需支持“动态权限”(如按用户套餐临时开放高级功能)。
1.2.3 审计(Audit):“你做过什么?”
- 定义:记录所有鉴权相关操作,用于追溯与合规。
- 智能文本生成系统的特殊需求:
- 日志需包含“谁(身份)在何时(时间戳)用什么模型(模型ID)生成了什么内容(摘要)”;
- 需满足“不可篡改”要求(如写入区块链或带签名的日志系统)。
二、鉴权机制深度对比:选型前必须知道的5个维度
选择鉴权机制时,90%的坑源于“为了简单选了不匹配的方案”。以下是5种主流机制的深度对比,涵盖安全性、成本、可扩展性等核心维度:
2.1 API Key:最简单但最危险的选择?
原理
- 调用者在请求头/参数中携带一段静态字符串(如
X-API-Key: sk-xxxx),服务端验证其是否在“允许列表”中。 - 典型场景:内部服务间调用、简单的第三方集成(如早期OpenAI API)。
优点
- 实现成本极低:服务端只需“查数据库/配置文件比对”;
- 调用方友好:无需复杂的令牌生成逻辑,复制粘贴即可用。
缺点(智能文本生成系统视角)
- 安全隐患大:静态字符串易泄露(如前端代码硬编码、日志打印、抓包);
- 权限颗粒度粗:一个Key对应“所有权限”,无法限制模型/功能;
- 吊销成本高:若Key泄露,需手动轮换所有依赖该Key的系统;
- 无法追溯:Key本身不包含用户信息,难以定位“谁在滥用”。
避坑要点
- 禁止前端暴露API Key:若必须在前端使用,通过后端“代理转发”(如前端→企业后端→LLM API);
- 强制轮换机制:设置Key的有效期(如90天),到期自动失效;
- 关联上下文:Key中嵌入环境标识(如
prod-xxx/test-xxx),防止测试Key误用于生产。
2.2 JWT(JSON Web Token):无状态的理想与现实的骨感
原理
- 服务端通过密钥签名生成包含用户信息的Token(如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...),客户端携带Token请求,服务端验证签名即可(无需查数据库)。 - 典型场景:用户登录后的API授权、无状态服务架构。
优点
- 无状态:服务端无需存储Token,适合分布式系统;
- 自包含:可嵌入用户ID、角色等信息,减少数据库查询;
- 权限灵活:Payload中可自定义权限字段(如
model: "gpt-3.5-turbo")。
缺点(智能文本生成系统视角)
- 吊销难题:Token一旦生成无法主动撤回(除非设置极短有效期);
- Payload暴露:Base64编码的Payload可被解码(敏感信息不能放!);
- 签名风险:若使用对称加密(HS256)且密钥泄露,攻击者可伪造Token;
- 性能开销:高频调用下,签名验证比API Key比对更耗时(毫秒级差异)。
避坑要点
- 永远不要用HS256! 改用非对称加密(RS256/ES256),即使公钥泄露也无法伪造Token;
- Payload最小化:只放必要信息(用户ID、角色、过期时间),禁止放API密钥、权限策略等;
- 短期有效期:访问Token有效期设为15分钟内,配合“刷新Token”机制(存储在数据库,支持吊销)。
2.3 OAuth2.0/OIDC:第三方授权的正确打开方式
原理
- OAuth2.0是“授权框架”(非协议),定义了4种授权流程(授权码、密码、客户端凭证、隐式),核心是通过“授权服务器”颁发Token。
- OIDC(OpenID Connect)是基于OAuth2.0的身份认证协议,增加了用户信息接口(
/userinfo)。 - 典型场景:第三方登录(如“用Google账号调用API”)、多系统统一身份认证。
优点
- 职责分离:认证逻辑交给专业授权服务器(如Keycloak、Auth0),降低业务系统复杂度;
- 细粒度授权:支持“权限范围”(Scope)控制(如
scope=text:generate code:generate); - 安全合规:符合GDPR等法规对“用户数据控制”的要求(用户可撤销授权)。
缺点(智能文本生成系统视角)
- 实现复杂:需部署授权服务器,理解“授权码流程”“PKCE”等概念;
- 性能损耗:每次授权需跳转授权服务器,增加延迟(不适合高频API调用);
- 成本较高:自建授权服务器需维护,第三方服务(如Auth0)有费用。
避坑要点
- 禁止使用“密码流程”:直接传递用户密码,违反OAuth2.0设计初衷;
- 必须启用PKCE:防止授权码被拦截(尤其是移动端/前端场景);
- Scope设计需“原子化”:将权限拆分为最小单位(如
text:generate:short/text:generate:long),避免过度授权。
2.4 HMAC与mTLS:高安全场景下的“小众方案”
HMAC(哈希消息认证码)
- 原理:客户端用密钥对“请求参数+时间戳”计算哈希值(如HMAC-SHA256),服务端用相同密钥验证哈希。
- 优点:密钥不在网络传输,防抓包;时间戳可防重放攻击。
- 缺点:实现复杂(客户端需处理哈希计算);不适合浏览器场景(密钥无法安全存储)。
- 适用场景:企业级API对接(如合作伙伴系统调用你的文本生成API)。
mTLS(双向TLS)
- 原理:客户端与服务端互相验证证书(普通TLS仅服务端验证),证书中包含身份信息。
- 优点:安全性极高(证书难伪造);适合零信任架构。
- 缺点:证书管理成本高(签发、轮换、吊销);不适合动态扩展场景(如容器化环境)。
- 适用场景:金融级LLM API、涉及国家机密/医疗数据的文本生成系统。
2.5 选型决策树:如何根据场景选对鉴权机制?
以下是基于“安全等级”“用户类型”“系统复杂度”的决策路径:
1. 安全等级要求?
├─ 极高(金融/医疗数据)→ mTLS
├─ 高(企业内部/付费用户)→ HMAC 或 JWT(RS256)+ 刷新Token
└─ 中/低(公开API/免费试用)→ 继续
2. 是否需要第三方授权?
├─ 是(如支持Google账号调用)→ OAuth2.0/OIDC(授权码流程+PKCE)
└─ 否 → 继续
3. 用户类型是“系统”还是“人”?
├─ 系统间调用(如服务A→服务B)→ API Key(带轮换机制)或HMAC
└─ 人调用(如用户通过APP调用)→ JWT(RS256)+ 刷新Token
4. 成本敏感?
├─ 是(初创公司/小团队)→ API Key(严格限制前端使用)+ 监控告警
└─ 否 → OAuth2.0/OIDC(使用Auth0等托管服务降低复杂度)
智能文本生成系统的典型组合:
- 开源项目/免费工具:API Key(前端隐藏+后端代理)+ 简单限流
- SaaS产品(如AI写作平台):JWT(RS256)+ RBAC权限 + OAuth2.0(第三方登录)
- 企业私有部署:mTLS(服务间)+ JWT(用户)+ HMAC(外部对接)
三、实战:构建智能文本生成API的鉴权体系(Python+FastAPI)
3.1 环境准备:从0搭建开发环境
技术栈
- 后端框架:FastAPI(高性能、类型提示友好、自动生成API文档)
- 语言:Python 3.9+
- 依赖库:
fastapi:Web框架核心uvicorn:ASGI服务器pyjwt:JWT生成与验证python-jose:JWT算法支持(RS256等)passlib:密码哈希(用于用户认证)redis:存储刷新Token、限流计数sqlalchemy:ORM(用户/权限数据存储)
环境搭建步骤
-
创建虚拟环境
python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows -
安装依赖
创建requirements.txt:fastapi==0.104.1 uvicorn==0.24.0 pyjwt==2.8.0 python-jose==3.3.0 cryptography==41.0.5 passlib==1.7.4 redis==5.0.1 sqlalchemy==2.0.23 python-multipart==0.0.6 python-dotenv==1.0.0安装:
pip install -r requirements.txt -
启动Redis(用于存储刷新Token和限流)
docker run -d -p 6379:6379 --name redis-auth redis:alpine -
生成RSA密钥对(用于JWT签名)
# 生成私钥(保存到./keys/private.pem) openssl genrsa -out keys/private.pem 2048 # 生成公钥(保存到./keys/public.pem,用于验证JWT) openssl rsa -in keys/private.pem -pubout -out keys/public.pem
3.2 Step 1:危险的起点——静态API Key的实现与漏洞分析
需求
构建一个简单的“文本生成API”,支持通过API Key鉴权。
代码实现
# main.py
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import os
from dotenv import load_dotenv
# 加载环境变量(存储API Key列表)
load_dotenv()
VALID_API_KEYS = os.getenv("VALID_API_KEYS", "").split(",") # 格式:key1,key2,key3
app = FastAPI(title="智能文本生成API")
# 请求体模型
class TextGenerateRequest(BaseModel):
prompt: str
max_tokens: int = 100
# 文本生成接口(带API Key鉴权)
@app.post("/api/generate/text")
async def generate_text(
request: TextGenerateRequest,
x_api_key: str = Header(..., alias="X-API-Key") # 从请求头获取API Key
):
# 鉴权逻辑:检查API Key是否在允许列表中
if x_api_key not in VALID_API_KEYS:
raise HTTPException(status_code=401, detail="无效的API Key")
# 模拟文本生成(实际场景调用LLM API)
generated_text = f"模拟生成结果:{request.prompt}..." # 简化逻辑
return {"prompt": request.prompt, "generated_text": generated_text}
启动服务
uvicorn main:app --reload --port 8000
测试API
# 正确调用(用有效的API Key)
curl -X POST "http://localhost:8000/api/generate/text" \
-H "Content-Type: application/json" \
-H "X-API-Key: key1" \
-d '{"prompt":"Hello, World","max_tokens":50}'
# 错误调用(无效Key)
curl -X POST "http://localhost:8000/api/generate/text" \
-H "Content-Type: application/json" \
-H "X-API-Key: fake_key" \
-d '{"prompt":"Hello, World","max_tokens":50}'
漏洞分析:静态API Key的6个致命问题
-
硬编码泄露风险
- 若代码中直接写死
VALID_API_KEYS = ["key1"],提交到Git仓库后将永久泄露; - 即使通过环境变量加载,运维人员/服务器管理员仍可轻易获取所有Key。
- 若代码中直接写死
-
无权限区分
- 所有Key拥有相同权限,无法限制“key1只能调用GPT-3.5,key2可以调用GPT-4”;
- 若某个Key用于前端Demo,攻击者获取后可调用付费模型,导致成本失控。
-
无法追溯滥用
- Key本身不关联用户信息,若Key泄露并被滥用,无法定位“谁的Key泄露了”;
- 无法审计“哪个Key在什么时间生成了违规内容”。
-
吊销成本高
- 若Key泄露,需手动轮换所有依赖该Key的系统(如客户的服务、内部脚本);
- 若未记录Key的使用方,轮换时可能导致业务中断。
-
重放攻击风险
- 攻击者可通过抓包获取API Key,然后无限次重复调用(除非额外加时间戳/HMAC)。
-
无法应对动态场景
- 无法临时禁用某个Key(如检测到异常调用时);
- 无法根据用户套餐动态调整权限(如免费用户升级后开放更多模型)。
3.3 Step 2:JWT鉴权实战:生成、验证、刷新与吊销
为解决静态API Key的问题,我们用“JWT+刷新Token”机制重构鉴权逻辑。
架构设计
- 访问Token(Access Token):短期有效(15分钟),用于API调用,JWT格式(RS256签名);
- 刷新Token(Refresh Token):长期有效(7天),用于获取新的访问Token,存储在Redis中(支持吊销);
- 流程:用户登录→获取访问Token+刷新Token→用访问Token调用API→Token过期→用刷新Token换新访问Token。
Step 2.1:准备工作
-
创建数据库模型(SQLAlchemy)
创建models.py,定义用户表(存储用户账号密码):from sqlalchemy import Column, Integer, String, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) hashed_password = Column(String) # 存储哈希后的密码(非明文) is_active = Column(Boolean, default=True) role = Column(String, default="user") # 角色:user/admin -
生成RSA密钥对
已在3.1节生成,路径:./keys/private.pem(私钥,用于签名JWT)和./keys/public.pem(公钥,用于验证JWT)。 -
Redis连接配置
创建core/redis.py:import redis from dotenv import load_dotenv import os load_dotenv() redis_client = redis.Redis( host=os.getenv("REDIS_HOST", "localhost"), port=int(os.getenv("REDIS_PORT", 6379)), db=0, decode_responses=True )
Step 2.2:实现JWT工具类
创建core/jwt.py,封装JWT生成、验证、刷新逻辑:
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from jose import JWTError, jwt
from fastapi import HTTPException, status
from core.redis import redis_client
import os
# 配置
PRIVATE_KEY = open(os.getenv("JWT_PRIVATE_KEY_PATH", "keys/private.pem")).read()
PUBLIC_KEY = open(os.getenv("JWT_PUBLIC_KEY_PATH", "keys/public.pem")).read()
ALGORITHM = "RS256" # 非对称加密算法
ACCESS_TOKEN_EXPIRE_MINUTES = 15 # 访问Token有效期:15分钟
REFRESH_TOKEN_EXPIRE_DAYS = 7 # 刷新Token有效期:7天
def create_access_token(
subject: Dict[str, Any], # JWT的主体(Payload)
expires_delta: Optional[timedelta] = None
) -> str:
"""生成访问Token(JWT)"""
to_encode = subject.copy()
# 设置过期时间
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "type": "access"}) # 增加类型标识
# 用私钥签名JWT
encoded_jwt = jwt.encode(to_encode, PRIVATE_KEY, algorithm=ALGORITHM)
return encoded_jwt
def create_refresh_token(
user_id: int,
expires_delta: Optional[timedelta] = None
) -> str:
"""生成刷新Token(存储在Redis)"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
# 生成随机字符串作为刷新Token(UUID)
import uuid
refresh_token = str(uuid.uuid4())
# 存储到Redis:key=refresh_token:{token}, value=user_id, 过期时间=expire
redis_key = f"refresh_token:{refresh_token}"
redis_client.setex(redis_key, expire - datetime.utcnow(), user_id)
return refresh_token
def verify_access_token(token: str) -> Dict[str, Any]:
"""验证访问Token,返回Payload"""
try:
payload = jwt.decode(token, PUBLIC_KEY, algorithms=[ALGORITHM])
token_type: str = payload.get("type")
if token_type != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的Token类型"
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token验证失败(无效或已过期)"
)
def verify_refresh_token(token: str) -> int:
"""验证刷新Token,返回用户ID(若有效)"""
redis_key = f"refresh_token:{token}"
user_id = redis_client.get(redis_key)
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="刷新Token无效或已过期"
)
return int(user_id)
def revoke_refresh_token(token: str) -> None:
"""吊销刷新Token(从Redis删除)"""
redis_key = f"refresh_token:{token}"
redis_client.delete(redis_key)
Step 2.3:实现登录接口(获取Token)
创建auth.py,实现用户登录逻辑:
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from datetime import timedelta
from models import User
from core.jwt import create_access_token, create_refresh_token
from core.database import get_db # 数据库连接依赖(需自行实现,简化省略)
router = APIRouter(prefix="/auth", tags=["认证"])
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 密码哈希工具
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""生成密码哈希"""
return pwd_context.hash(password)
def authenticate_user(db: Session, username: str, password: str) -> User:
"""用户认证:验证用户名密码"""
user = db.query(User).filter(User.username == username).first()
if not user or not verify_password(password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
return user
# 登录接口(OAuth2密码流程)
@router.post("/token", response_model=dict)
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
user = authenticate_user(db, form_data.username, form_data.password)
# 构建JWT Payload(包含用户ID和角色)
access_token_payload = {
"sub": user.id, # subject,标准字段
"role": user.role, # 自定义字段:用户角色
"username": user.username # 可选:用户名(非敏感信息)
}
# 生成访问Token和刷新Token
access_token = create_access_token(access_token_payload)
refresh_token = create_refresh_token(user_id=user.id)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": 15 * 60 # 访问Token有效期(秒)
}
# 刷新访问Token接口
@router.post("/token/refresh", response_model=dict)
async def refresh_access_token(refresh_token: str, db: Session = Depends(get_db)):
user_id = verify_refresh_token(refresh_token) # 验证刷新Token
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# 生成新的访问Token
access_token_payload = {"sub": user.id, "role": user.role, "username": user.username}
new_access_token = create_access_token(access_token_payload)
return {"access_token": new_access_token, "token_type": "bearer", "expires_in": 15 * 60}
Step 2.4:用JWT保护文本生成接口
修改main.py,添加JWT鉴权依赖:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from core.jwt import verify_access_token
from models import User
from core.database import get_db
from sqlalchemy.orm import Session
# OAuth2 Bearer Token提取器(从请求头Authorization: Bearer <token>获取)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
# 依赖:获取当前用户(验证JWT并返回用户信息)
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无法验证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = verify_access_token(token) # 验证JWT
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# 从数据库获取用户信息(验证用户是否存在/激活)
user = db.query(User).filter(User.id == int(user_id)).first()
if user is None or not user.is_active:
raise credentials_exception
return user
# 修改文本生成接口:依赖JWT鉴权
@app.post("/api/generate/text")
async def generate_text(
request: TextGenerateRequest,
current_user: User = Depends(get_current_user) # 注入当前用户
):
# 鉴权通过,执行生成逻辑(此处简化)
generated_text = f"用户{current_user.username}生成结果:{request.prompt}..."
return {"prompt": request.prompt, "generated_text": generated_text}
Step 2.5:测试JWT鉴权流程
-
创建测试用户(通过数据库脚本或管理接口插入):
# 示例:插入用户(username=test, password=test123, role=user) from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from models import Base, User from auth import get_password_hash DATABASE_URL = "sqlite:///./test.db" # 简化使用SQLite engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.create_all(bind=engine) # 创建表 db = SessionLocal() test_user = User( username="test", hashed_password=get_password_hash("test123"), # 密码哈希 role="user", is_active=True ) db.add(test_user) db.commit() -
登录获取Token:
curl -X POST "http://localhost:8000/auth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=test&password=test123" # 响应示例: # {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","refresh_token":"a1b2c3...","token_type":"bearer","expires_in":900} -
用访问Token调用API:
curl -X POST "http://localhost:8000/api/generate/text" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ # 替换为实际access_token -d '{"prompt":"Hello JWT","max_tokens":50}' -
刷新访问Token(当旧Token过期时):
curl -X POST "http://localhost:8000/auth/token/refresh" \ -H "Content-Type: application/json" \ -d '{"refresh_token":"a1b2c3..."}' # 替换为登录时获取的refresh_token -
吊销刷新Token(用户登出/Token泄露时):
# 调用revoke_refresh_token函数 from core.jwt import revoke_refresh_token revoke_refresh_token("a1b2c3...") # 传入需吊销的refresh_token
3.4 Step 3:细粒度权限控制:基于RBAC模型限制“生成能力”
JWT解决了“认证”和“短期授权”问题,但未解决“细粒度权限”(如限制用户调用特定模型)。我们用RBAC(基于角色的访问控制)模型实现权限管理。
RBAC模型设计
- 核心实体:用户(User)→ 角色(Role)→ 权限(Permission)→ 资源(Resource)
- 权限定义:格式为
resource:action:attribute,如:text:generate:gpt-3.5:允许用GPT-3.5生成文本text:generate:gpt-4:允许用GPT-4生成文本text:generate:max_tokens:500:允许最大生成长度为500 tokens
Step 3.1:扩展数据库模型
修改models.py,添加角色、权限、用户角色关联表:
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Table
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
# 权限表
class Permission(Base):
__tablename__ = "permissions"
id = Column(Integer, primary_key=True, index=True)
code = Column(String, unique=True, index=True, comment="权限编码,如text:generate:gpt-3.5")
description = Column(String, nullable=True, comment="权限描述")
# 角色表
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True, comment="角色名称,如admin/user/vip")
description = Column(String, nullable=True)
# 角色-权限多对多关系
permissions = relationship("Permission", secondary="role_permissions", backref="roles")
# 角色-权限关联表(多对多)
role_permissions = Table(
"role_permissions",
Base.metadata,
Column("role_id", Integer, ForeignKey("roles.id"), primary_key=True),
Column("permission_id", Integer, ForeignKey("permissions.id"), primary_key=True),
)
# 用户表(增加角色关联)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
# 用户-角色多对多关系(一个用户可拥有多个角色)
roles = relationship("Role", secondary="user_roles", backref="users")
# 用户-角色关联表(多对多)
user_roles = Table(
"user_roles",
Base.metadata,
Column("user_id", Integer, ForeignKey("users.id"), primary_key=True),
Column("role_id", Integer, ForeignKey("roles.id"), primary_key=True),
)
Step 3.2:权限检查依赖
创建permissions.py,实现权限验证逻辑:
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session
from models import User, Permission
from core.database import get_db
def get_user_permissions(db: Session, user: User) -> set[str]:
"""获取用户的所有权限(合并所有角色的权限)"""
permissions = set()
for role in user.roles:
role_permissions = db.query(Permission.code).filter(Permission.roles.any(id=role.id)).all()
permissions.update([p[0] for p in role_permissions])
return permissions
def require_permission(permission_code: str):
"""权限依赖:检查用户是否拥有指定权限"""
def decorator(
current_user: User = Depends(get_current_user), # 依赖当前用户
db: Session = Depends(get_db)
):
user_permissions = get_user_permissions(db, current_user)
if permission_code not in user_permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"权限不足:需要'{permission_code}'权限"
)
return current_user
return decorator
# 示例:检查是否允许调用GPT-3.5
require_gpt35 = require_permission("text:generate:gpt-3.5")
# 示例:检查是否允许调用GPT-4
require_gpt4 = require_permission("text:generate:gpt-4")
Step 3.3:修改文本生成接口,支持多模型与权限控制
from permissions import require_gpt35, require_gpt4
# 请求体增加模型参数
class TextGenerateRequest(BaseModel):
prompt: str
model: str = "gpt-3.5-turbo" # 支持的模型:gpt-3.5-turbo/gpt-4
max_tokens: int = 100
# 根据模型类型动态检查权限
@app.post("/api/generate/text")
async def generate_text(
request: TextGenerateRequest,
current_user: User = Depends(
require_gpt35 if request.model == "gpt-3.5-turbo" else require_gpt4
)
):
# 权限通过,调用对应模型生成文本(模拟)
generated_text = f"用{request.model}生成:{request.prompt}..."
return {"model": request.model, "generated_text": generated_text}
测试权限控制
-
为用户分配角色和权限:
- 创建角色
user,分配权限text:generate:gpt-3.5; - 创建角色
vip,分配权限text:generate:gpt-3.5和text:generate:gpt-4; - 将用户
test的角色设为user。
- 创建角色
-
测试无权限调用:
# 用户test(角色user)调用GPT-4,应返回403 curl -X POST "http://localhost:8000/api/generate/text" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <access_token>" \ -d '{"prompt":"Hello","model":"gpt-4","max_tokens":50}' -
测试有权限调用:
# 调用GPT-3.5,应成功 curl -X POST "http://localhost:8000/api/generate/text" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <access_token>" \ -d '{"prompt":"Hello","model":"gpt-3.5-turbo","max_tokens":50}'
3.5 Step 4:限流与成本控制:防止“一条API调用拖垮公司”
智能文本生成API的调用成本与max_tokens成正比,若用户恶意设置max_tokens=10000并高频调用,可能导致账单暴增。我们需实现“基于用户/角色的限流与配额控制”。
限流方案设计
- 限流维度:用户级(每个用户每秒最多N次调用)、令牌桶级(每个用户每天最多生成M tokens);
- 限流算法:令牌桶算法(支持突发流量,平滑限制);
- 存储:Redis(分布式环境下共享限流计数)。
Step 4.1:实现限流依赖
创建rate_limit.py:
import time
from typing import Optional, Tuple
from fastapi import HTTPException, Request
from core.redis import redis_client
class RateLimiter:
"""令牌桶限流实现"""
def __init__(
self,
rate: int, # 令牌生成速率(个/秒)
capacity: int, # 令牌桶容量
key_prefix: str = "rate_limit:" # Redis键前缀
):
self.rate = rate
self.capacity = capacity
self.key_prefix = key_prefix
async def __call__(self, request: Request, current_user: User = Depends(get_current_user)):
更多推荐


所有评论(0)