首页 / FastAPI 入门教程 / 用户权限与多角色

FastAPI 入门教程

用户权限与多角色

本教程共 50 篇 · 第 30 篇 · 更新于 2026-08-12 · 约 9 分钟阅读

FastAPIFastAPI 入门教程scopes角色权限SecurityOAuth2

本节目标:学会用 OAuth2 的 scopes(作用域)给令牌挂权限,区分管理员和普通用户,在路径操作上用 Security() 声明需要的权限,并在依赖里校验当前用户到底有没有这个权限。

30-1 认证之后,要管”能做什么”

前两章我们解决了”你是谁”:登录发令牌、带令牌访问、解码拿到用户。但真实系统里,不是所有登录用户都能干所有事。

举个常见的例子:普通用户能看自己的资料,管理员能删全站数据。这就需要授权——在确认身份之后,再判断”你配不配做这件事”。

OAuth2 提供了一套现成的概念叫 scopes(作用域)。你可以把它理解成”权限标签”,比如 users:readusers:writeadmin。令牌上挂着哪些标签,就拥有哪些能力。

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" 表示同时拥有 meitems 两个权限。

这些 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 不会出现在界面上。

SecuritySecurityScopes 是本章节的新主角,都从 fastapi / fastapi.security 导入。

Tip

Security 其实是 Depends 的子类,多带一个 scopes 参数。当你要声明”这个接口需要某权限”时,用 Security;单纯取依赖仍用 Depends

30-4 登录时把角色写进 JWT

以下代码沿用第 29 章的完整文件(appSECRET_KEYALGORITHMoauth2_schemeauthenticate_userUserfake_users_db 等都来自那里),只替换 /token 并新增权限相关逻辑。记得在文件顶部补上 from datetime import timedelta, timezoneimport 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,会看到可以勾选 meitemsadmin 等 scope(就是 30-3 在 scopes={...} 里登记的那些)。不勾任何 scope 也能登录,但调 /users/me/items/ 时会提示权限不足;勾上 items 就能访问。

这正是第三方应用”请求授权”的缩影:用户只给了部分权限,应用就只能做这部分事。

30-10 小结

权限分级用 scopes 三板斧:

  • 登录时把权限写进 JWT 的 scopes 字段。
  • 接口用 Security(依赖, scopes=[...]) 声明需要哪些权限。
  • 根依赖用 SecurityScopes 汇总并逐项校验,缺一个就 401。

这样认证(你是谁)和授权(你能做什么)就彻底分开了,且都接进 OpenAPI 文档。下一章我们看两套更轻量的认证:API Key 与 HTTP Basic。