给接口加把锁:Bearer Token 鉴权(附冒烟验收)

32 阅读5分钟

「从零到 AI 应用工程师」专栏 · 第 8 篇
阶段 1 · chat-api 收官补篇


上一篇用 Docker Compose 把整栈拉起来了。你会发现:几乎每条 curl 都带着这一行——

-H "Authorization: Bearer 你的令牌"

却很少有人单独讲清楚:这把锁是怎么挂上的?漏带会发生什么?如何用脚本证明「鉴权真的生效」?

chat-api 阶段再补这一刀:把鉴权做成可复用的依赖,再用一份冒烟脚本当交付验收。做完,阶段 1 才算真正能交出去。


一、今天要解决什么

目标:未带 Token / Token 错误 → 401;正确 Token → 正常进业务。

最终效果(示意):

# 没锁 → 进不去
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 常见做法:在请求头里带:

Authorization: Bearer <token字符串>
含义
Bearer「持票人」模式:谁持有这串令牌,谁就被当成已授权
Token一串密钥;本阶段用配置里的固定字符串即可(演示/内网够用)
依赖 DependsFastAPI 在进路由函数之前跑校验;失败直接 401,业务代码零侵入

本阶段不做完整 OAuth2 / JWT 签发体系。先把「接口默认不裸奔」立住;以后再换成验签、过期、多级 Key。

安全底线(写进习惯):

  • Token 放环境变量 / .env不进 Git、不写进镜像层
  • 对外文档用 你的令牌 / sk-xxx 占位,不贴真实值;
  • 日志里不要打印完整 Authorization。

三、最小实现:verify_token 依赖

# 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"},
        )

挂到路由上——两种常见写法:

# 写法 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 也会变成:

{
  "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(名字随意),至少覆盖:

  1. /ping/healthz 成功;
  2. 错误 Token → 401
  3. 正确 Token → /chat 成功;
  4. (可选)再问同一句,看缓存命中字段;
  5. (可选)拉一页历史。

伪代码节奏:

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 起来之后跑一遍:

docker compose up -d --build
./scripts/smoke_test.sh

全绿,你才有底气说:别人按 README 操作,十分钟能验证你的交付。


六、常见坑

  1. 只校验「有没有 Header」,不校验值。 → 任意 Bearer xxx 都能进。
  2. Token 写死在代码里并提交。 → 换成环境变量;仓库只留 .env.example
  3. /healthz 也鉴权了,却忘了给探针配 Header。 → 探活失败,编排系统反复杀容器。
  4. 401 仍返回 HTTP 200 + code:401 → 监控和网关会误判;状态码要真实。
  5. Swagger 里点「Authorize」忘了填,误以为接口挂了。 → 先确认 401 文案是鉴权,再查业务。
  6. 日志打印完整 Token。 → 最多打前后几位,或只打「已校验」。

七、阶段 1 真正收官

到这一篇,chat-api 骨架可以这样交卷:

能力
02FastAPI + 大模型,打通 /chat
03路由 / 业务 / 客户端分层
04统一响应、统一异常、request_id
05PostgreSQL 历史 + 分页
06Redis 缓存重复问题(可降级)
07Docker Compose 一键拉起整栈
08Bearer 鉴权 + 冒烟脚本验收

底座齐了:能对话、能记账、能省钱、能一键起、默认不裸奔


八、带走这三条

  1. 鉴权用 Depends 前置拦截:失败不到业务、不打模型。
  2. 探活与业务权限分离/healthz 宽松,/chat 收紧。
  3. 冒烟脚本里必须有一条「错误 Token → 401」:证明锁是真的。

下一阶段进入专栏最硬核的一块:RAG——让大模型读懂你自己的资料,还不许瞎编。 第 9 篇先把概念和全貌讲清,再进入解析、分块、向量检索。

这是专栏第 8 篇,chat-api 阶段正式收官。两到三天一更,RAG 见。