跳转到内容

权限设计规范

RBAC、数据权限


[Standard] 采用 RBAC0(用户-角色-权限) 基础模型,复杂场景可扩展 RBAC1(角色继承)或 RBAC2(角色约束)。

用户(User) ──多对多── 角色(Role) ──多对多── 权限(Permission)
│ │
└── 可直接绑定少量权限(特例) └── 关联资源 + 动作

[Requirement] 权限点是最小粒度,角色是权限的集合,用户通过角色获得权限。禁止直接给用户分配大量权限点(应通过角色)。

表关键字段说明
userid, username用户
roleid, name, code角色
permissionid, code, resource, action, type权限点
user_roleuser_id, role_id用户-角色关联
role_permissionrole_id, permission_id角色-权限关联

[Requirement] 权限点编码格式:{资源}:{动作},全小写,动作用动词,如 user:read、order:create、payment:refund。

动作含义HTTP 方法
read查看GET
create新增POST
update修改PUT/PATCH
delete删除DELETE
export导出GET(特殊)
approve审批POST(特殊)

[Standard] 角色按业务职责设计,数量适中(一般 10~50 个),避免”一人一角”导致角色爆炸。

[Caution] 禁止创建”超级管理员”角色给普通业务人员使用,超级管理员仅限运维/安全人员,且操作需审计。


[Standard] 在 RBAC 功能权限之外,增加数据权限控制用户能看到哪些数据,常见维度:

维度说明示例
本人只能看自己创建的数据WHERE creator_id = current_user.id
本部门看本部门数据WHERE dept_id = current_user.dept_id
本部门及下级看本部门及子部门WHERE dept_id IN (子部门树)
全部看所有数据无数据过滤
自定义按特定规则WHERE region IN ('北京','上海')

[Requirement] 数据权限在查询层注入过滤条件实现,禁止在返回结果后再过滤(性能差且容易遗漏)。

# 数据权限过滤示例(在 Repository 层统一处理)
def build_data_scope_filter(query, current_user):
    scope = current_user.data_scope  # all / dept / dept_and_sub / self
    if scope == "all":
        return query
    elif scope == "self":
        return query.filter(Order.creator_id == current_user.id)
    elif scope == "dept":
        return query.filter(Order.dept_id == current_user.dept_id)
    elif scope == "dept_and_sub":
        sub_dept_ids = get_sub_dept_ids(current_user.dept_id)
        return query.filter(Order.dept_id.in_(sub_dept_ids))

[Standard] 通过 MyBatis 拦截器 / SQLAlchemy 事件 / Repository 基类统一注入,避免每个查询手写。

用户请求 → 功能权限校验(能否访问该接口)→ 数据权限过滤(能看到哪些数据)→ 返回结果

[Caution] 功能权限通过才能进入数据权限过滤,两者缺一不可。


[Requirement] 每个需要鉴权的接口必须声明所需权限点,在网关或 Controller 入口统一校验。

from functools import wraps
 
def require_permission(perm_code: str):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, current_user, **kwargs):
            if perm_code not in current_user.permissions:
                raise PermissionDeniedError(f"缺少权限: {perm_code}")
            return await func(*args, current_user=current_user, **kwargs)
        return wrapper
    return decorator
 
@require_permission("order:read")
@router.get("/orders/{order_id}")
async def get_order(order_id: int, current_user: User = Depends(get_current_user)):
    ...

[Standard] 对敏感字段(如金额、手机号)做字段级权限控制,无权限的用户看不到该字段(返回 null 或不返回)。

def filter_fields(data: dict, current_user) -> dict:
    if "amount:read" not in current_user.permissions:
        data.pop("amount", None)
    if "phone:read" not in current_user.permissions:
        data["phone"] = mask_phone(data.get("phone", ""))
    return data

见上文「数据权限」章节。

[Standard] 用户权限列表在登录后缓存(Redis),TTL 30 分钟,权限变更时主动清除缓存。

[Caution] 权限缓存必须支持主动失效(用户角色变更、角色权限变更时清除对应用户缓存),否则权限变更不生效。


[Requirement] 以下操作必须记录审计日志:

  • 角色创建、修改、删除
  • 角色分配 / 回收权限
  • 用户分配 / 回收角色
  • 权限点变更
  • 越权访问尝试
字段说明
operator_id操作者 ID
action操作类型(create_role / assign_permission / …)
target操作对象(角色 ID、用户 ID)
before变更前内容(JSON)
after变更后内容(JSON)
ip操作者 IP
created_at操作时间
def audit_permission_change(operator, action, target, before, after):
    AuditLog.create(
        operator_id=operator.id,
        action=action,
        target=target,
        before=before,
        after=after,
        ip=operator.current_ip,
    )

[Standard] 对越权访问(PermissionDenied)做统计告警,同一用户短时间内多次越权可能是攻击行为。

[Default] 每季度做一次权限复核:检查角色权限是否合理、是否有离职人员仍有账号、是否有长期未使用的角色。


反模式说明风险
前端控制权限只在前端隐藏按钮/菜单,后端不校验攻击者直接调接口,权限形同虚设
硬编码权限判断在业务代码中 if user.role == "admin"角色变更需改代码,无法动态配置
信任客户端传的用户 ID用前端传的 user_id 查数据水平越权(必须从 Token 取)
超级管理员直通if user.is_superuser: return 跳过所有校验账号泄露即全量数据泄露
权限缓存不失效角色变更后缓存不清权限变更不生效,存在窗口期
SQL 拼接权限条件f"WHERE dept_id = {user.dept_id}"SQL 注入(应用 ORM 参数化)
返回后过滤数据查出全部再在内存过滤无权限数据性能差,且分页/统计数据不准
一人一角每个用户建一个专属角色角色爆炸,无法管理
权限点粒度过粗只有”管理”和”查看”两种无法细粒度控制,过度授权
不审计权限变更角色权限修改不留痕无法追溯谁在何时改了权限