第 15 章 · 安全进阶:Scopes 与 API Key
本章目标:用 OAuth2 scopes 实现"登录后还要有对应权限"的细粒度授权,并掌握服务间调用常用的三种 API Key 方案。
15.1 OAuth2 scopes:权限的最小单元
第 14 章解决了"你是谁",本章解决"你能干什么"。OAuth2 把权限定义为空格分隔的字符串列表(scope),例如 me、items、users:read。GitHub、Google 等第三方登录用的正是这套机制。FastAPI 把它无缝集成进了 OpenAPI 文档:登录时可以勾选要授予的 scopes。
声明可用 scopes 只需在 OAuth2PasswordBearer 上加 scopes 参数:
from fastapi.security import OAuth2PasswordBearer
# key 是 scope 名,value 是描述(会显示在文档里)
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={"me": "读取当前用户信息", "items": "读取物品列表"},
)15.2 Security() 与 SecurityScopes:在依赖树中校验权限
Security 用法与 Depends 完全一致,只是多了 scopes 参数,用于声明"这条依赖链需要哪些权限":
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
import jwt
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={"me": "读取当前用户信息", "items": "读取物品"},
)
app = FastAPI()
async def get_current_user(
security_scopes: SecurityScopes, # 自动收集依赖树上所有 Security 声明的 scopes
token: Annotated[str, Security(oauth2_scheme)],
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_scopes = payload.get("scopes", [])
user = get_user_from_db(payload.get("sub"))
if user is None:
raise credentials_exception
# 逐个检查 token 里的 scope 是否覆盖依赖链要求的所有 scope
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=403, # 注意:权限不足是 403,不是 401
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return user
async def get_current_active_user(
current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
):
return current_user
@app.get("/users/me/items/")
async def read_own_items(
# 依赖树会累积要求:me(来自上一级)+ items(这里声明)
current_user: Annotated[dict, Security(get_current_active_user, scopes=["items"])],
):
return {"item_id": "Foo", "owner": current_user["username"]}理解三个关键点:
SecurityScopes参数会自动汇总整条依赖链上所有Security(..., scopes=[...])声明的 scopes,无需手动传递;- scope 校验失败返回 403(已认证但权限不足),与 token 无效的 401 区分开;
- 签发 token 时把该用户实际拥有的 scopes 写进 payload:
create_access_token(data={"sub": username, "scopes": form_data.scopes})。生产中必须校验请求的 scopes 是否在该用户的允许范围内,而不是照单全收。
登录表单 OAuth2PasswordRequestForm 自带 scopes 属性(就是文档 Authorize 弹窗里勾选的那些)。
15.3 API Key 三种位置
服务间调用(没有"用户登录"概念)通常用 API Key。fastapi.security 提供三个类,分别从不同位置提取 key,且都会写进 OpenAPI 文档:
from fastapi import Security
from fastapi.security import APIKeyCookie, APIKeyHeader, APIKeyQuery
# 从查询参数 ?api_key=xxx 提取
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
# 从请求头 X-API-Key: xxx 提取(最常用)
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
# 从 Cookie 提取
api_key_cookie = APIKeyCookie(name="api_key", auto_error=False)
async def get_api_key(
header_key: Annotated[str | None, Security(api_key_header)],
query_key: Annotated[str | None, Security(api_key_query)],
) -> str:
key = header_key or query_key
if key and key in VALID_KEYS: # 换成查数据库/密钥管理服务
return key
raise HTTPException(status_code=401, detail="Invalid or missing API Key")auto_error=False 的意义:设为 True(默认)时,缺 key 会立刻抛 401,你无法自定义错误格式,也无法实现"多渠道取 key"的回退逻辑。设为 False 后拿到的可能是 None,由你决定如何报错。
15.4 封装成可复用的依赖
把校验逻辑收敛到一个依赖工厂,避免每个端点重复写:
from typing import Annotated
from fastapi import Depends, FastAPI, Security
app = FastAPI()
ApiKeyDep = Annotated[str, Depends(get_api_key)]
@app.get("/reports/")
async def get_reports(api_key: ApiKeyDep):
return {"reports": [...], "key_used": api_key[:4] + "****"}与 OAuth2 共存的典型场景:面向用户的端点走 JWT + scopes,面向内部服务的端点走 API Key。两者可以放在同一个应用里,各自用各自的依赖声明即可,互不干扰;也可以写一个"双通道"依赖(JWT 或 API Key 任一通过即可),适合网关场景。
API Key 的安全边界
API Key 是身份标识而非强凭证:它通常长期有效、无过期、权限粗。泄露风险高于短命 JWT。生产中应:走 HTTPS、只放请求头(避免进访问日志的 query)、支持轮换、按 key 记录审计日志。
15.5 本章小结
- scope 是空格分隔的权限字符串,通过
OAuth2PasswordBearer(scopes=...)声明、Security(dep, scopes=[...])消费; SecurityScopes依赖自动收集依赖树上的全部 scopes 要求;scope 不足返回 403;- 签发 token 时只应写入用户实际拥有的 scopes;
APIKeyHeader/APIKeyQuery/APIKeyCookie覆盖三种提取位置,auto_error=False换取自定义错误与回退能力;- 用户侧用 JWT、服务间用 API Key,可在同一应用中共存。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. token 有效但缺少依赖要求的 scope 时,FastAPI 应用应返回哪个状态码?
2. 依赖函数里声明参数 security_scopes: SecurityScopes 的作用是?
3. APIKeyHeader(auto_error=False) 与默认行为相比,主要好处是?
4. 关于 API Key 的安全实践,下列哪项是错误的?
🛠️ 动手实践
- 在第 14 章的代码上加入 scopes:登录时勾选
me/items,实现/users/me/(要求me)和/users/me/items/(要求me+items),用只勾me的 token 访问后者,确认返回 403。 - 实现一个"JWT 或 API Key 二选一"的依赖:先尝试解析 Bearer JWT,失败再查
X-API-Key头,两种都无效返回 401。 - 把 API Key 校验改造成从 SQLite 表读取(key、所属服务、启用状态),写一个测试用例验证"被禁用的 key 返回 401"。
并发与延迟处理是下一个必修课——请进入下一章:异步并发与后台任务。