做后端开发这些年,权限系统大概是最容易被低估、又最容易在生产环境炸锅的模块。刚开始写接口的时候,很多人图省事,直接在函数里写一堆 if user.role == "admin",代码能跑,看着也顺眼。可等到角色一多、资源一复杂,这种写法立马变成一团乱麻,改一个权限规则得满仓库搜索,改漏一处就是安全事故。
FastAPI 官方并没有内置一套完整的权限框架,社区里因此长出了不少解决方案,风格各异。这篇文章挑最有代表性的 fastapi-permissions 库来细聊,顺带把它背后那套源自 Pyramid 框架的权限哲学讲透,再对比一下其他常见做法,帮你判断到底该选哪条路。
fastapi-permissions 是个什么东西
这个库由开发者 holgi 维护,核心卖点是 行级别的权限控制(row level security)。什么意思呢,普通的角色权限只关心用户是谁,而行级权限还要看这个用户操作的是哪一个具体对象。同一个 admin 角色,可能对 A 文章有编辑权,对 B 文章却没有,这种细粒度的判断,光靠角色标签是搞不定的。
作者坦言这套设计几乎是照搬了 Python Web 老牌框架 Pyramid 的安全模型,用他自己的话说算是"一次光明正大的抄袭"。这倒不是贬义,Pyramid 的权限系统在业界口碑一直不错,能把这套成熟思路移植到 FastAPI 生态里,对开发者来说反而是件好事。
项目托管在 GitHub 上,star 数六百多,规模不算庞大但足够稳定,也已经上架 PyPI,一行 pip install fastapi_permissions 就能装上。
四个核心概念,理解了就等于打通任督二脉
这套系统跟 FastAPI 自带的 OAuth2 scopes 是两个思路。scopes 只看用户本身携带了什么权限标签,而 fastapi-permissions 还要结合资源当时的状态一起判断。举个例子,一篇论文在草稿、提交、评审、发表这几个阶段,不同用户能做的事完全不一样,这种场景用 scopes 硬写会很别扭,用这套系统就是天然适配。
整套体系围绕四个概念展开,缺一不可。
资源(resource) ,指的是任何提供了访问控制列表的对象,可以是一个 Pydantic 模型,也可以就是一个普通的 Python 类。判断依据是它有没有一个叫 __acl__ 的属性或方法。
访问控制列表(ACL) ,说白了就是一份规则清单,每条规则是个三元组,格式是(动作,principal,permission)。动作只有两种,Allow 或者 Deny。系统检查规则时是按顺序从上往下走的,一旦匹配到就立刻生效,不会继续往下看,这个顺序敏感的特性用好了很省事,用不好容易埋坑。
principal ,是身份标识符,可以代表某个具体用户,也可以代表一个角色或群组。习惯上会写成 user:bob 或者 role:admin 这样带前缀的字符串,方便区分类型。系统里还预留了两个特殊值,Everyone 表示所有人无论是否登录都适用,Authenticated 则只对已登录用户生效。
permission ,其实就是一个代表动作的字符串,比如 view 、edit 、delete ,你可以自己随便定义,系统不做限制。另外还有个通配符 All,代表任意权限都匹配。
手把手怎么用起来
装好包之后,整个使用流程大致分成三步,定义资源、定义用户身份、在路由里挂上权限检查。
第一步,给资源定义 ACL
python
from fastapi_permissions import Allow, Deny, Authenticated, Everyone
class Item:
def __init__(self, name, owner):
self.name = name
self.owner = owner
def __acl__(self):
return [
(Allow, Authenticated, "view"),
(Allow, "role:admin", "edit"),
(Allow, f"user:{self.owner}", "delete"),
]
这段代码翻译成人话就是,任何登录用户都能看这条数据,只有 admin 角色能编辑,只有这条数据的主人才能删除它。注意这里没写任何拒绝规则,是因为系统默认在清单末尾隐含了一条兜底的拒绝,没被明确允许的,一律不通过。
如果 ACL 是固定不变的,还能偷懒直接写成类属性,甚至直接用一个列表当资源用,省得再包一层类。
第二步,定义用户的 principals
系统要求你必须提供一个函数,告诉它当前用户身上挂了哪些身份标签。
python
def get_active_principals(user = Depends(get_current_user)):
if user:
principals = [Everyone, Authenticated]
principals.extend(user.principals) # 比如 ["role:admin", "user:bob"]
else:
principals = [Everyone]
return principals
这一步是整个系统的入口,相当于告诉框架"现在是谁在敲门"。
第三步,配置并在路由里使用
python
from fastapi_permissions import configure_permissions
Permission = configure_permissions(get_active_principals)
@app.get("/item/{item_id}")
async def show_item(item: Item = Permission("view", get_item)):
return {"item": item.name}
configure_permissions 只需要调用一次,拿到的 Permission 已经是包好 Depends() 的产物,直接当依赖注入用就行。路径函数里写 Permission("view", get_item),意思是先用 get_item 这个函数把资源找出来,再检查当前用户对这个资源有没有 view 权限,没有的话框架会自动抛出权限异常,业务代码里完全不用手写判断逻辑。
运行时到底发生了什么
用一张时序图把这个校验过程摊开看,会清楚很多。
这个流程最大的好处是权限规则和业务逻辑彻底分离,改权限只需要改 __acl__ 里的清单,路径函数一行都不用动。
跟其他方案比一比,到底该选谁
社区里其实还有不少同类竞品,各自的适用场景不太一样。有开发者在 FastAPI 官方仓库的一个 issue 里专门讨论过这个问题,提到了 Casbin、OSO、Cerbos 这些第三方策略引擎,也有人干脆自己手写一个 RoleChecker 类当依赖使用,简单粗暴但缺乏细粒度控制。另外还有像 00-Python/FastAPI-Role-and-Permissions 这样的开源脚手架,直接把 JWT 认证、PostgreSQL 数据库、RBAC 表结构打包成一套完整方案,适合想要开箱即用的团队。业界也有专门的教程从零讲解如何在 FastAPI 里搭建完整的 RBAC 体系,覆盖角色管理、权限分配、接口鉴权全流程。
做个简单的横向对比方便你挑选。
| 方案 | 核心思路 | 适用场景 | 学习成本 |
|---|---|---|---|
| FastAPI 原生 scopes | 权限绑定在用户身上,不看资源状态 | 权限规则简单固定的场景 | 低,官方文档自带 |
| fastapi-permissions | 基于 ACL,权限同时看用户和资源状态 | 需要行级别、按资源状态动态判断的场景 | 中,需要理解 Pyramid 式概念 |
| 自定义 RoleChecker | 手写依赖函数直接判断角色 | 角色种类少,逻辑简单的小项目 | 低,但扩展性差 |
| 00-Python RBAC 脚手架 | 数据库表驱动的角色权限多对多关系 | 需要完整用户系统、想快速起步的项目 | 中,需要吃透数据库表结构 |
| Casbin / OSO / Cerbos | 独立策略引擎,语言无关 | 大型系统、需要跨语言统一策略的场景 | 较高,引入额外基础设施 |
如果你的系统权限规则不复杂,用户角色也就那么几种,scopes 完全够用,没必要折腾额外的库。可一旦涉及到"同一个角色对不同数据行有不同权限"这种场景,比如多租户系统、内容审核流程里不同状态对应不同操作权限,fastapi-permissions 这种基于 ACL 的思路就明显更省心,规则集中管理,改动成本低。
要是团队本身就有 Pyramid 开发背景,或者未来考虑用一套策略引擎统一管理多个后端服务的权限,那不妨看看 Casbin 或者 OSO,虽然接入门槛高一点,但长期维护性会更好。
写在最后
权限系统这东西,说难不难,说简单也真不简单。难的地方不在代码怎么写,而在于设计阶段有没有把"谁能对什么东西做什么事"这件事想清楚。fastapi-permissions 的价值就在于逼着你用一种结构化的方式去表达这套规则,资源自己声明访问清单,用户自己声明身份标签,中间由框架去做匹配,这种关注点分离的设计思路,哪怕最后你没用这个库,理解了它也能让你自己写权限系统时少踩很多坑。
参考资料