「从零到 AI 应用工程师」专栏 · 第 8 篇
阶段 1 · chat-api 收官补篇
上一篇用 Docker Compose 把整栈拉起来了。你会发现:几乎每条 curl 都带着这一行------
bash
-H "Authorization: Bearer 你的令牌"
却很少有人单独讲清楚:这把锁是怎么挂上的?漏带会发生什么?如何用脚本证明「鉴权真的生效」?
chat-api 阶段再补这一刀:把鉴权做成可复用的依赖,再用一份冒烟脚本当交付验收。做完,阶段 1 才算真正能交出去。
一、今天要解决什么
目标 :未带 Token / Token 错误 → 401;正确 Token → 正常进业务。
最终效果(示意):
bash
# 没锁 → 进不去
curl -i -X POST http://127.0.0.1:8000/chat \
-H "Content-Type: application/json" \
-d '{"user_id":"u1","session_id":"s1","message":"你好"}'
# HTTP/1.1 401 ...
# 有锁 → 正常
curl -i -X POST http://127.0.0.1:8000/chat \
-H "Authorization: Bearer 你的令牌" \
-H "Content-Type: application/json" \
-d '{"user_id":"u1","session_id":"s1","message":"你好"}'
# HTTP/1.1 200 ...
探活接口(/ping、/healthz)可以不鉴权------运维探活和业务读写要分开。
二、Bearer 到底是什么
HTTP 常见做法:在请求头里带:
http
Authorization: Bearer <token字符串>
| 词 | 含义 |
|---|---|
| Bearer | 「持票人」模式:谁持有这串令牌,谁就被当成已授权 |
| Token | 一串密钥;本阶段用配置里的固定字符串即可(演示/内网够用) |
| 依赖 Depends | FastAPI 在进路由函数之前跑校验;失败直接 401,业务代码零侵入 |
本阶段不做完整 OAuth2 / JWT 签发体系。先把「接口默认不裸奔」立住;以后再换成验签、过期、多级 Key。
安全底线(写进习惯):
- Token 放环境变量 /
.env,不进 Git、不写进镜像层; - 对外文档用
你的令牌/sk-xxx占位,不贴真实值; - 日志里不要打印完整 Authorization。
三、最小实现:verify_token 依赖
python
# core/auth.py(示意)
from fastapi import HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
# auto_error=False:不要框架默认文案,自己抛,方便统一成「说人话」的 JSON
security = HTTPBearer(auto_error=False)
# 从配置读取,例如环境变量 API_TOKEN
EXPECTED_TOKEN = "你的令牌" # 实际:os.getenv("API_TOKEN")
async def verify_token(request: Request):
credentials: HTTPAuthorizationCredentials | None = await security(request)
if not credentials or credentials.credentials != EXPECTED_TOKEN:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效或未提供 Token",
headers={"WWW-Authenticate": "Bearer"},
)
挂到路由上------两种常见写法:
python
# 写法 A:整个路由模块统一要鉴权
router = APIRouter(
prefix="/chat",
tags=["对话"],
dependencies=[Depends(verify_token)],
)
# 写法 B:单个接口要鉴权
@router.post("/chat", dependencies=[Depends(verify_token)])
async def chat(...):
...
业务函数里不用 再写一遍 if token != ...。鉴权失败到不了你的 service 层,也就打不到大模型------既省钱,也少一次无效调用。
和「统一异常」篇衔接:若你已把 HTTPException 转成统一 JSON,401 也会变成:
json
{
"code": 401,
"message": "无效或未提供 Token",
"data": null,
"request_id": "req_xxxxxxxxxxxx"
}
前端只认一种结构;监控也能按真实 HTTP 401 计数。
四、哪些要锁、哪些别锁
| 接口 | 建议 | 原因 |
|---|---|---|
POST /chat、历史查询等业务 |
要锁 | 直接花钱、读用户数据 |
GET /ping |
可不锁 | 进程活着即可 |
GET /healthz |
可不锁 | K8s/Compose 探活;再加鉴权反而难运维 |
文档 /docs |
开发开、生产关或加保护 | 避免接口清单裸奔 |
原则:探活宽松,业务收紧。
五、冒烟验收:用脚本证明「锁生效」
光靠手测容易漏。阶段 1 收官时,准备一份 smoke_test.sh(名字随意),至少覆盖:
/ping、/healthz成功;- 错误 Token → 401;
- 正确 Token →
/chat成功; - (可选)再问同一句,看缓存命中字段;
- (可选)拉一页历史。
伪代码节奏:
bash
BASE_URL="${BASE_URL:-http://127.0.0.1:8000}"
API_TOKEN="${API_TOKEN:-你的令牌}"
# 1) 探活
curl -sf "$BASE_URL/healthz" >/dev/null || { echo "healthz FAIL"; exit 1; }
# 2) 鉴权必须失败
code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$BASE_URL/chat" \
-H "Authorization: Bearer wrong_token" \
-H "Content-Type: application/json" \
-d '{"user_id":"u","session_id":"s","message":"ping"}')
[ "$code" = "401" ] || { echo "auth FAIL want 401 got $code"; exit 1; }
# 3) 鉴权通过
curl -sf -X POST "$BASE_URL/chat" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"user_id":"u","session_id":"s","message":"冒烟你好"}' \
| grep -q '"code": *200' || { echo "chat FAIL"; exit 1; }
echo "smoke PASS"
Compose 起来之后跑一遍:
bash
docker compose up -d --build
./scripts/smoke_test.sh
全绿,你才有底气说:别人按 README 操作,十分钟能验证你的交付。
六、常见坑
- 只校验「有没有 Header」,不校验值。 → 任意
Bearer xxx都能进。 - Token 写死在代码里并提交。 → 换成环境变量;仓库只留
.env.example。 - 把
/healthz也鉴权了,却忘了给探针配 Header。 → 探活失败,编排系统反复杀容器。 - 401 仍返回 HTTP 200 +
code:401。 → 监控和网关会误判;状态码要真实。 - Swagger 里点「Authorize」忘了填,误以为接口挂了。 → 先确认 401 文案是鉴权,再查业务。
- 日志打印完整 Token。 → 最多打前后几位,或只打「已校验」。
七、阶段 1 真正收官
到这一篇,chat-api 骨架可以这样交卷:
| 篇 | 能力 |
|---|---|
| 02 | FastAPI + 大模型,打通 /chat |
| 03 | 路由 / 业务 / 客户端分层 |
| 04 | 统一响应、统一异常、request_id |
| 05 | PostgreSQL 历史 + 分页 |
| 06 | Redis 缓存重复问题(可降级) |
| 07 | Docker Compose 一键拉起整栈 |
| 08 | Bearer 鉴权 + 冒烟脚本验收 |
底座齐了:能对话、能记账、能省钱、能一键起、默认不裸奔。
八、带走这三条
- 鉴权用 Depends 前置拦截:失败不到业务、不打模型。
- 探活与业务权限分离 :
/healthz宽松,/chat收紧。 - 冒烟脚本里必须有一条「错误 Token → 401」:证明锁是真的。
下一阶段进入专栏最硬核的一块:RAG------让大模型读懂你自己的资料,还不许瞎编。 第 9 篇先把概念和全貌讲清,再进入解析、分块、向量检索。
这是专栏第 8 篇,chat-api 阶段正式收官。两到三天一更,RAG 见。