不止于JWT:用FastAPI的Depends实现细粒度权限控制

🤔 "JWT都验证通过了,怎么用户还能删别人的文章?"------这个问题,很常见,但也很不该。

用FastAPI写公司后台应用时,觉得:只要把JWT(JSON Web Token)鉴权加上,把token往Depends里一扔,妥了!上线!结果往往就会是,运营小姐姐截图过来:"为什么张三能看到李四的工资条?"

JWT只是"身份证",它只证明"你是你",但证明不了"你能干嘛"。 今天咱们就聊聊,怎么用FastAPI亲生的Depends,搭一套真正能打、能上生产的细粒度权限控制。全程口语+代码,保证你看完就能动手改项目。
📌 本文摘要

很多FastAPI初学者把JWT认证当成权限控制的终点,结果上线后频繁出现越权操作。本文通过一个真实的"多租户Todo"案例,带你从0搭建基于角色的访问控制(RBAC)和数据级权限(ABAC),手撕权限拦截代码,分享我常用的依赖封装方案。

🚨 第一部分:只有JWT,到底缺了啥?

先别急着写代码,咱们捋一捋最常见的翻车现场。

想象你的API是一个高档小区。JWT就像门禁卡------刷卡能进小区大门(通过认证)。但进门之后,你能随便进别人家吗?能去物业办公室调监控吗?显然不行。你需要"房间钥匙"和"工作证"。

很多初版代码长这样:

复制代码
@app.get("/tasks/{task_id}")
def get_task(task_id: int, user: User = Depends(get_current_user)):
    task = db.query(Task).filter(Task.id == task_id).first()
    return task   # 危险!没检查这个task是不是该用户的!

**⚠️ 警告:这段代码等于让张三拿着身份证,就能进李四家拿东西。**只要用户伪造或篡改了token里的user_id,甚至只是猜到别人的ID,数据就裸奔了。

🎯 第二部分:细粒度权限,到底"细"在哪儿?

咱们做权限,通常分三个层级,你可以对着看看自己项目到哪一步了:
全局认证(Authentication):你是谁?------JWT搞定。

角色权限(RBAC):你有什么身份?------比如"管理员"能删除,"普通用户"只能看。

数据权限(ABAC/行级):你对这个具体的数据有什么权限?------比如"只能改自己创建的任务"或"状态为'草稿'的文章才能编辑"。

今天重点聊,因为这是最容易被忽略的"隐形漏洞"。

🛠️ 第三部分:实战!用Depends写一个"带脑子的"权限拦截器

FastAPI的Depends简直是权限控制的瑞士军刀。它不仅能拿用户信息,还能嵌套依赖、提前拦截请求 ,比在路由函数里写一堆 if 判断优雅一万倍。

🔧 第一步:给JWT加点"料"

别再只存个user_id了!生成token的时候,把角色(role) 和**权限标识(permissions)**塞进payload里,后续查询会省很多事儿。

复制代码
# 登录时生成token
def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    # 👇 除了user_id,把角色和权限列表放进去
    to_encode.update({
        "role": user.role,
        "perms": user.permissions  # 比如 ["task:create", "task:delete_own"]
    })
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

⚙️ 第二步:封装权限校验依赖(核心干货)

这里咱们写一个"可配置"的权限依赖。它的工作流程是:拿到token -> 解析用户 -> 检查角色 -> 检查具体权限 。只要不通过,直接抛 403,路由函数根本不会执行。

复制代码
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from typing import List, Optional

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# 模拟获取当前用户(实际项目中会查DB或解码JWT)
async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        role: str = payload.get("role")
        perms: List = payload.get("perms", [])
        if user_id is None:
            raise HTTPException(status_code=401, detail="无效凭证")
        return {"id": user_id, "role": role, "permissions": perms}
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="无效凭证")

# 🚀 重磅:可组合的权限依赖
class PermissionChecker:
    def __init__(self, required_role: Optional[str] = None, required_perm: Optional[str] = None):
        self.required_role = required_role
        self.required_perm = required_perm

    def __call__(self, user: dict = Depends(get_current_user)):
        # 角色检查(RBAC)
        if self.required_role and user.get("role") != self.required_role:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"需要 {self.required_role} 角色"
            )
        # 权限检查(细粒度)
        if self.required_perm and self.required_perm not in user.get("permissions", []):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"缺少权限: {self.required_perm}"
            )
        return user  # 通过检查,把用户信息传给路由用

📝 第三步:在路由中使用,清爽到飞起

看看封装后的效果,是不是比在函数里写一堆 if 舒服多了?

复制代码
# 实例化不同的权限检查器
require_admin = PermissionChecker(required_role="admin")
require_task_create = PermissionChecker(required_perm="task:create")
require_task_delete_own = PermissionChecker(required_perm="task:delete_own")

# 只有admin能删任何任务
@app.delete("/tasks/{task_id}")
async def delete_task_any(task_id: int, admin_user: dict = Depends(require_admin)):
    # 业务逻辑:直接删,反正已经确认是admin
    return {"msg": "任务已删除"}

# 用户只能删自己的任务(这里只检查了"是否有删除自己任务的权限",具体数据还得再查)
@app.delete("/tasks/my/{task_id}")
async def delete_my_task(task_id: int, user: dict = Depends(require_task_delete_own)):
    task = db.query(Task).filter(Task.id == task_id).first()
    if task.owner_id != user["id"]:
        raise HTTPException(status_code=403, detail="只能删除自己的任务")
    db.delete(task)
    return {"msg": "自己的任务已删除"}

🧠 第四部分:进阶玩法------数据级权限(ABAC)

上面代码里还有个瑕疵:require_task_delete_own 只保证了"用户有删除自己任务的权限",但没保证"这个任务真的是他的"。这就是典型的数据级权限缺失。

更优雅的做法是:把"数据归属校验"也封装进依赖里。

参考fastapi-permissions库的思路,可以给每个数据模型定义一个"权限控制列表"(ACL):

复制代码
from fastapi_permissions import Allow, Authenticated

class Task:
    def __acl__(self):
        return [
            (Allow, Authenticated, "view"),
            (Allow, f"user:{self.owner_id}", "edit"),
            (Allow, "role:admin", "delete"),
        ]

然后在依赖里,把当前用户的principals(身份列表)资源的ACL 进行比对。如果 "user:123" 不在允许列表里,直接 403。这样就把权限逻辑和数据模型绑定了,维护起来特别清晰。

💣 第五部分:这些坑我帮你踩过了

💥 坑1:把权限逻辑写在路由函数里

我当时为了赶进度,在每个路由里都写 if user.role != 'admin': raise ...。后来权限逻辑变了,要加个"超级管理员",我改了18个文件,改到吐。所以一定用Depends集中管理

💥 坑2:JWT里不放权限,每次都查DB

虽然查DB更实时,但高频接口扛不住啊。我后来折中方案:核心权限放JWT(只读),易变权限单独查缓存(Redis)

💥 坑3:忘了异常处理,导致服务器500

权限校验失败一定要 raise HTTPException,不要 return None 或者直接 pass,不然FastAPI会继续执行路由,可能触发更奇怪的数据库错误。

📦 总结:权限设计的三个"黄金法则"

🔸 法则一:认证是门票,授权是门禁,缺一不可。

🔸 法则二:谁(Who)对什么(What)能做什么(Action)------永远用这个公式检查你的代码。

🔸 法则三:权限逻辑离业务逻辑越远越好,Depends就是那道防火墙。

其实写权限系统,就像装修时埋水管。前期多花点心思规划好"分层"和"走线",后面几十年住着都安心;要是图省事随便接一下,指不定哪天楼下就找上门来了🚰。

今天分享的这段PermissionChecker,你直接复制过去改吧改吧就能用。如果你公司用的是更复杂的权限模型(比如多租户、属性权限)或使用中有什么问题,留言告诉我,咱们一起讨论!


💡 老朋友说:别光看过,对照着去把你项目里那个裸奔的@app.get修一下吧!

顺手点个"收藏",关注下,下次踩坑的时候还能翻出来救命🆘

相关推荐
IVEN_19 小时前
只会Python皮毛?深入理解这几点,轻松进阶全栈开发
python·全栈
Ray Liang20 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
AI攻城狮21 小时前
如何给 AI Agent 做"断舍离":OpenClaw Session 自动清理实践
python
千寻girling21 小时前
一份不可多得的 《 Python 》语言教程
人工智能·后端·python
AI攻城狮1 天前
用 Playwright 实现博客一键发布到稀土掘金
python·自动化运维
曲幽1 天前
FastAPI分布式系统实战:拆解分布式系统中常见问题及解决方案
redis·python·fastapi·web·httpx·lock·asyncio
孟健2 天前
Karpathy 用 200 行纯 Python 从零实现 GPT:代码逐行解析
python
码路飞2 天前
写了个 AI 聊天页面,被 5 种流式格式折腾了一整天 😭
javascript·python