用户权限与多角色
本教程共 50 篇 · 第 30 篇 · 更新于 2026-08-12 · 约 9 分钟阅读
本节目标:学会用 OAuth2 的 scopes(作用域)给令牌挂权限,区分管理员和普通用户,在路径操作上用 Security() 声明需要的权限,并在依赖里校验当前用户到底有没有这个权限。
30-1 认证之后,要管”能做什么”
前两章我们解决了”你是谁”:登录发令牌、带令牌访问、解码拿到用户。但真实系统里,不是所有登录用户都能干所有事。
举个常见的例子:普通用户能看自己的资料,管理员能删全站数据。这就需要授权——在确认身份之后,再判断”你配不配做这件事”。
OAuth2 提供了一套现成的概念叫 scopes(作用域)。你可以把它理解成”权限标签”,比如 users:read、users:write、admin。令牌上挂着哪些标签,就拥有哪些能力。
Note本章基于上一章的认证代码继续扩展。scopes 是 OAuth2 的进阶用法,新手可以先读懂思路,不必一开始就上生产。
30-2 scopes 就是一串权限字符串
OAuth2 里,一个 scope 就是一个不带空格的字符串,代表一种权限。具体怎么命名随你,常见风格:
users:read:读用户信息users:write:改用户信息items:write:管理物品- 甚至整条 URL:
https://www.googleapis.com/auth/drive(Google 就是这么干的)
规范只要求”每个 scope 不含空格”,多个 scope 用空格拼成一长串。比如 "me items" 表示同时拥有 me 和 items 两个权限。
这些 scope 会出现在 OpenAPI 文档里。用户点 Authorize 登录时,能勾选给这个令牌哪些权限——和你用微信登录第三方时”授权通讯录、授权相册”是一模一样的机制。
30-3 在 OAuth2PasswordBearer 上声明可用 scopes
上一章我们写的是 OAuth2PasswordBearer(tokenUrl="token")。要让文档里出现 scope 勾选框,创建方案时就要用 scopes 参数登记所有可用的权限(一个 名称: 说明 的字典):
from fastapi import Depends, FastAPI, HTTPException, status, Security
from fastapi.security import (
OAuth2PasswordBearer,
OAuth2PasswordRequestForm,
SecurityScopes,
)
from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"me": "读取当前用户信息",
"items": "读取自己的物品",
"admin": "管理员权限",
},
)
app = FastAPI()
这个字典就是 30-9 文档里 Authorize 弹窗中那些复选框的来源。没登记的 scope 不会出现在界面上。
Security 和 SecurityScopes 是本章节的新主角,都从 fastapi / fastapi.security 导入。
Tip
Security其实是Depends的子类,多带一个scopes参数。当你要声明”这个接口需要某权限”时,用Security;单纯取依赖仍用Depends。
30-4 登录时把角色写进 JWT
以下代码沿用第 29 章的完整文件(app、SECRET_KEY、ALGORITHM、oauth2_scheme、authenticate_user、User、fake_users_db 等都来自那里),只替换 /token 并新增权限相关逻辑。记得在文件顶部补上 from datetime import timedelta, timezone、import jwt(第 28、29 章已用 PyJWT,不再用 python-jose)。
登录接口照旧收表单。但这次,我们把用户”申请”的 scopes 也写进 JWT 的载荷里。还是用 OAuth2PasswordRequestForm,它自带 scopes 属性(已按空格拆好的列表)。
class Token(BaseModel):
access_token: str
token_type: str
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="账号或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 把用户申请的权限写进令牌
access_token = create_access_token(
data={"sub": user.username, "scopes": form_data.scopes},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
)
return {"access_token": access_token, "token_type": "bearer"}
Warning这里为了演示,把前端申请的 scopes 直接写进令牌。真实项目里必须先校验”这个用户到底配不配拥有这些 scope”,只放行他该有的,否则普通用户能自己申请
admin权限。
30-5 用 Security() 在接口上要求权限
现在到了关键:在路径操作上声明”访问我需要哪些 scope”。语法是把 Depends 换成 Security,并传 scopes 列表。
@app.get("/users/me/items/")
async def read_own_items(
current_user: User = Security(get_current_active_user, scopes=["items"]),
):
return current_user
这行表示:这个接口需要 items 权限,且要从 get_current_active_user 拿当前用户。get_current_active_user 内部又会要求 me 权限(见下)。
注意第 29 章的 get_current_active_user 用的是 Depends,这里要改成 Security 并声明 scopes=["me"],否则它要求的 me 不会被汇总进依赖树:
async def get_current_active_user(
current_user: User = Security(get_current_user, scopes=["me"]),
):
if current_user.disabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户已被禁用",
)
return current_user
FastAPI 会把这些 scope 要求收集起来,传给最底层负责校验的依赖,由它统一核对。
30-6 用 SecurityScopes 集中校验权限
真正做校验的地方,是 get_current_user 这个”根依赖”。它除了取令牌、解码、查用户,还要检查:当前令牌拥有的 scopes,是否覆盖了本次请求要求的所有 scopes。
class TokenData(BaseModel):
username: str | None = None
scopes: list[str] = []
async def get_current_user(
security_scopes: SecurityScopes,
token: str = Depends(oauth2_scheme),
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="没有足够权限",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(username=username, scopes=token_scopes)
except InvalidTokenError:
raise credentials_exception
user = fake_users_db.get(username)
if user is None:
raise credentials_exception
# 逐项比对:要求的每个 scope,令牌都必须有
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise credentials_exception
return user
SecurityScopes 是个特殊参数,FastAPI 会自动把”本请求链路里所有 Security(..., scopes=[...]) 声明的 scope”汇总到它的 .scopes 列表。我们用一个 for 循环逐個检查:只要有一个要求的权限令牌没有,就 401。
Note
security_scopes.scope_str是把这些 scope 用空格拼回的字符串,放进WWW-Authenticate头里,符合规范,也方便客户端知道”还差哪个权限”。
30-7 依赖树里的 scope 是怎么汇总的
这套机制最妙的地方在于:scope 是跟着依赖树自动叠加的。看这个结构:
- 接口
read_own_items要求items,依赖get_current_active_user。 get_current_active_user要求me,又依赖get_current_user。- 那么
get_current_user收到的security_scopes.scopes就是["me", "items"]。
也就是说,根依赖一个地方写校验逻辑,不同接口声明不同权限,互不干扰。换个接口如果只要 me,根依赖收到的就只有 ["me"]。
这比在每个接口里手动 if 判断清爽太多,也更容易扩展:加新权限只改声明,不动校验核心。
30-8 角色分级:admin 与 user
scopes 也能直接表达”角色”。比如你给管理员令牌挂 admin,普通用户挂 user。然后在敏感接口上要求 admin:
@app.delete("/users/{username}", response_model=User)
async def delete_user(
username: str,
admin: User = Security(get_current_active_user, scopes=["admin"]),
):
if username not in fake_users_db:
raise HTTPException(status_code=404, detail="用户不存在")
del fake_users_db[username]
return admin
普通用户的令牌里没有 admin scope,调这个删除接口就会被根依赖拦下,收到 401。登录时在 form_data.scopes 里放什么,就决定令牌拥有什么能力。
Tip角色和细粒度权限可以并存。小系统用
admin/user两个角色就够;大系统用users:read这类细权限,能精确控制”只能看不能改”。scopes 两种都支持。
30-9 在文档里体验权限勾选
启动后打开 /docs,点 Authorize,会看到可以勾选 me、items、admin 等 scope(就是 30-3 在 scopes={...} 里登记的那些)。不勾任何 scope 也能登录,但调 /users/me/items/ 时会提示权限不足;勾上 items 就能访问。
这正是第三方应用”请求授权”的缩影:用户只给了部分权限,应用就只能做这部分事。
30-10 小结
权限分级用 scopes 三板斧:
- 登录时把权限写进 JWT 的
scopes字段。 - 接口用
Security(依赖, scopes=[...])声明需要哪些权限。 - 根依赖用
SecurityScopes汇总并逐项校验,缺一个就 401。
这样认证(你是谁)和授权(你能做什么)就彻底分开了,且都接进 OpenAPI 文档。下一章我们看两套更轻量的认证:API Key 与 HTTP Basic。