权限系统设计:从访问控制理论到工程落地

权限的本质:从现实世界到软件系统

在现实世界中,权限无处不在:门禁卡决定你能进入哪些楼层,银行柜台需要身份证才能办理业务,公司的保密文件只有特定级别的人才能阅读。这些场景的共同点是:一个主体试图对一个客体执行某种操作,而系统需要判断这次操作是否被允许。

这就是访问控制的核心模型 —— 主体-客体-操作三元组(Subject-Object-Action Triple):

概念 定义 软件系统中的映射
Subject(主体) 发起访问请求的实体 用户、服务账号、API Client
Object(客体) 被访问的资源 API 接口、数据记录、页面、文件
Action(操作) 主体对客体执行的动作 读取、写入、删除、审批

在这个三元组之上,安全领域有一个经典的 AAA 模型,定义了访问控制的三个阶段:

Authentication(认证) 回答"你是谁"的问题。用户名密码、OAuth Token、生物识别都属于认证手段。认证的输出是一个可信的身份标识(Identity)。

Authorization(授权) 回答"你能做什么"的问题。在确认身份之后,系统根据策略判断该身份是否有权执行请求的操作。这是权限系统的核心。

Accounting(审计) 回答"你做了什么"的问题。记录所有访问行为,用于事后追溯、合规审查和异常检测。

很多系统把认证和授权混为一谈,典型的表现是在登录接口里同时返回用户信息和权限列表,导致权限逻辑散落在各处。清晰的分层是权限系统设计的第一步:认证层只负责验证身份,授权层只负责判断权限,两者通过标准化的身份上下文(如 JWT Claims)衔接。

一句话总结:权限系统的本质是回答"谁能对什么做什么"的问题,而 AAA 模型将这个问题拆解为认证、授权、审计三个正交的关注点。

访问控制模型演进:从 DAC 到 ABAC

访问控制模型经历了近 50 年的演进,从最初的自主访问控制发展到基于属性的细粒度控制。理解这条演进脉络,才能在实际项目中做出正确的模型选型。

DAC:自主访问控制

DAC(Discretionary Access Control)是最早的访问控制模型,核心思想是资源的拥有者自主决定谁可以访问。Linux 文件系统的 rwx 权限就是典型的 DAC 实现:文件的 owner 可以通过 chmod 命令授权其他用户读写。

# Linux DAC 示例:文件 owner 自主授权
chmod 750 /data/report.csv
# owner: rwx (读写执行)
# group: r-x (只读执行)
# others: --- (无权限)

DAC 的优点是灵活性高,资源拥有者可以按需分配权限。但致命弱点是权限可以被传递 —— 用户 A 把文件共享给用户 B,B 可以再共享给 C,权限管理最终会失控。在企业级系统中,DAC 的这种"自主性"反而是安全隐患。

MAC:强制访问控制

MAC(Mandatory Access Control)是对 DAC 的纠正,由 Bell-LaPadula 模型在 1970 年代提出,核心思想是由系统强制执行安全策略,用户无权更改

MAC 为每个主体和客体分配一个安全等级(Security Level),访问决策由两条规则驱动:

规则 含义 目的
No Read Up(简单安全属性) 主体不能读取高于自身等级的客体 防止信息泄露(向上泄露)
No Write Down(*-属性) 主体不能向低于自身等级的客体写入 防止信息降密(向下流动)

MAC 在军事和政府系统中广泛使用(如 SELinux),但对商业软件来说过于僵化 —— 现实中的权限需求远比"高/低等级"复杂。一个销售经理需要看到自己团队的客户数据但不能看到其他团队的,这种"同级别但不同范围"的需求 MAC 无法优雅表达。

RBAC:基于角色的访问控制

RBAC(Role-Based Access Control)在 1990 年代由 Ferraiolo 和 Kuhn 提出,2004 年被 NIST 标准化为 ANSI/INCITS 359-2004。RBAC 的核心洞察是:权限不应该直接分配给用户,而应该分配给"角色",用户通过担任角色获得权限

这个看似简单的间接层解决了 DAC 和 MAC 的核心问题:管理员可以集中管理角色-权限的映射关系,而不需要为每个用户单独配置。当员工转岗时,只需要调整角色分配,而不是重新配置几百条权限。

RBAC 是目前工业界使用最广泛的访问控制模型,下一章将深入展开。

ABAC:基于属性的访问控制

ABAC(Attribute-Based Access Control)由 NIST SP 800-162 定义,是目前最灵活的访问控制模型。ABAC 不预设任何固定的权限结构,而是基于主体属性、客体属性、环境属性和操作属性的组合来动态计算访问决策

例如一条 ABAC 策略可以表达为:"部门=财务 且 职级>=P7 且 时间在工作日 9:00-18:00 且 IP 在办公网段内,允许读取月度财报"。这种策略在 RBAC 中需要为每种组合创建一个角色,会导致角色爆炸问题。

ABAC 将在后文"ABAC:基于属性的细粒度控制"一节详细展开。

四种模型的对比

维度 DAC MAC RBAC ABAC
控制主体 资源拥有者 系统管理员 系统管理员 策略引擎
灵活性
管理复杂度 低(但不可控) 高(策略编写)
粒度 资源级 等级级 角色级 属性组合级
典型场景 文件系统、Wiki 军事/政府系统 企业管理后台 云平台、金融合规
工业界采用度 增长中
代表实现 Linux FS、Google Docs SELinux、Windows MIC AWS IAM Role、K8s RBAC AWS IAM Policy、OPA

在实际系统中,这四种模型并非互斥。最常见的做法是 RBAC 为主、ABAC 补充:用 RBAC 解决 80% 的常规权限分配,用 ABAC 策略处理需要动态判断的细粒度场景。

一句话总结:访问控制模型的演进是一条从"谁拥有"到"谁担任什么角色"再到"满足什么条件"的抽象化路径,每一步都在用更高的抽象换取更强的表达力。

RBAC 深入:从 NIST 标准到工程实践

RBAC 之所以成为工业界的事实标准,很大程度上得益于 NIST 在 2004 年发布的 RBAC 标准(ANSI/INCITS 359-2004)。该标准定义了四个递进的模型层级:RBAC0(核心)、RBAC1(角色层次)、RBAC2(约束)、RBAC3(统一模型)。

RBAC0:核心模型

RBAC0 定义了 RBAC 的最小集合,包含四个基本元素:

元素 定义 关系
Users(U) 系统中的用户集合
Roles(R) 角色集合,代表组织中的职能
Permissions(P) 权限集合,即对资源的操作许可
Sessions(S) 用户激活角色的会话 一个 Session 映射到一个 User 和一组激活的 Roles

NIST 标准中一个常被忽略的概念是 Session。用户可能拥有多个角色,但在一次会话中未必需要全部激活。Session 允许用户按需激活角色子集,这在实现最小权限原则(Principle of Least Privilege)时非常有用 —— 一个 DBA 同时拥有"只读分析"和"数据修改"两个角色,日常查询时只激活只读角色,需要执行 DDL 时才激活修改角色。

/**
 * RBAC0 核心模型的 Java 建模
 */
public interface RbacCore {

    /** 将角色授予用户 */
    void assignRole(UserId userId, RoleId roleId);

    /** 将权限授予角色 */
    void grantPermission(RoleId roleId, Permission permission);

    /** 创建 Session,激活用户的部分角色 */
    Session createSession(UserId userId, Set<RoleId> activeRoles);

    /** 检查当前 Session 是否拥有指定权限 */
    boolean checkAccess(Session session, Permission permission);
}

public record Permission(
    String resource,    // 资源标识,如 "order", "user"
    String action       // 操作标识,如 "read", "write", "delete"
) {}

RBAC1:角色层次

RBAC1 在 RBAC0 基础上引入角色继承(Role Hierarchy)。角色之间形成偏序关系(Partial Order),高级角色自动继承低级角色的所有权限。

角色继承分为两种结构:

一般继承(General Hierarchy):允许多继承,角色关系构成有向无环图(DAG)。一个"技术总监"角色可以同时继承"后端负责人"和"前端负责人"的权限。

受限继承(Limited Hierarchy):只允许单继承,角色关系构成树形结构。一条线的层级关系:出纳员 → 财务主管 → 财务总监。

/**
 * 角色继承的实现
 * 使用邻接表存储继承关系,通过递归上溯收集所有继承的权限
 */
public class RoleHierarchy {
    // key: 子角色, value: 直接父角色集合
    private final Map<RoleId, Set<RoleId>> parentMap = new HashMap<>();

    public void addInheritance(RoleId child, RoleId parent) {
        parentMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
    }

    /**
     * 获取角色的所有有效权限(含继承)
     * 递归收集所有祖先角色的权限,visited 防止循环引用导致栈溢出
     */
    public Set<Permission> getEffectivePermissions(RoleId roleId,
                                                    Map<RoleId, Set<Permission>> directPerms) {
        return collectPermissions(roleId, directPerms, new HashSet<>());
    }

    private Set<Permission> collectPermissions(RoleId roleId,
                                                Map<RoleId, Set<Permission>> directPerms,
                                                Set<RoleId> visited) {
        if (!visited.add(roleId)) {
            return Set.of(); // 检测到循环继承,终止递归
        }
        Set<Permission> result = new HashSet<>(
            directPerms.getOrDefault(roleId, Set.of())
        );
        Set<RoleId> parents = parentMap.getOrDefault(roleId, Set.of());
        for (RoleId parent : parents) {
            result.addAll(collectPermissions(parent, directPerms, visited));
        }
        return result;
    }
}

RBAC2:约束

RBAC2 在 RBAC0 基础上引入约束(Constraints),用于防止权限冲突和滥用。约束是 RBAC 实现安全合规的关键机制。

静态职责分离(Static Separation of Duty, SSD):在角色分配阶段就禁止冲突。如果"提交人"和"审批人"是互斥角色,那么一个用户不能同时被分配这两个角色。

/**
 * 静态职责分离(SSD)实现
 */
public class StaticSoD {
    // 互斥角色对
    private final Set<RolePair> mutualExclusions = new HashSet<>();
    // 角色的最大持有人数
    private final Map<RoleId, Integer> cardinalityLimits = new HashMap<>();

    public record RolePair(RoleId role1, RoleId role2) {}

    /**
     * 校验是否可以将角色分配给用户
     * @throws SoDViolationException 违反 SSD 约束时抛出
     */
    public void validateAssignment(UserId userId, RoleId newRole,
                                    Set<RoleId> existingRoles) {
        // 1. 互斥校验
        for (RoleId existing : existingRoles) {
            if (mutualExclusions.contains(new RolePair(existing, newRole))
                || mutualExclusions.contains(new RolePair(newRole, existing))) {
                throw new SoDViolationException(
                    "角色 %s 与已持有角色 %s 互斥".formatted(newRole, existing));
            }
        }
        // 2. 基数约束
        int limit = cardinalityLimits.getOrDefault(newRole, Integer.MAX_VALUE);
        int currentCount = countUsersWithRole(newRole);
        if (currentCount >= limit) {
            throw new SoDViolationException(
                "角色 %s 已达持有人数上限 %d".formatted(newRole, limit));
        }
    }
}

动态职责分离(Dynamic Separation of Duty, DSD):允许用户同时持有互斥角色,但在同一个 Session 中不能同时激活。这比 SSD 更灵活 —— 一个审计人员可以持有"审计"和"运维"两个角色,但每次登录只能选择其中一个身份工作。

先决条件约束(Prerequisite Constraint):用户必须先拥有低级角色,才能被分配高级角色。例如要成为"高级审核员"必须先具备"审核员"角色。

RBAC3:统一模型

RBAC3 = RBAC1 + RBAC2,即同时支持角色层次和约束。这是完整的 RBAC 实现目标。需要注意的是,角色层次和约束之间可能产生冲突 —— 如果角色 A 继承了角色 B,而角色 B 和角色 C 互斥,那么持有角色 A 的用户也不应该同时持有角色 C。约束检查必须沿着继承链传播。

一句话总结:NIST RBAC 标准将 RBAC 分为核心(RBAC0)、层次(RBAC1)、约束(RBAC2)、统一(RBAC3)四层,实际项目应根据业务复杂度逐层引入,避免过度设计。

ABAC:基于属性的细粒度控制

当 RBAC 遇到以下场景时会变得力不从心:

  • 权限决策依赖动态条件(时间、IP、设备类型)
  • 权限粒度需要到数据行级别(只能看到自己部门的数据)
  • 策略组合爆炸,导致角色数量膨胀到难以维护

这正是 ABAC 的用武之地。

XACML 架构

ABAC 最具影响力的标准化框架是 OASIS 的 XACML(eXtensible Access Control Markup Language),它定义了四个核心组件:

组件 全称 职责
PAP Policy Administration Point 策略管理入口,管理员在此创建和维护策略
PDP Policy Decision Point 策略决策引擎,接收请求并计算允许/拒绝
PEP Policy Enforcement Point 策略执行点,拦截请求并调用 PDP 获取决策
PIP Policy Information Point 属性提供方,为 PDP 补充决策所需的环境信息

一次访问控制的完整流程是:PEP 拦截请求 → 组装属性上下文 → 调用 PDP 决策 → PDP 从 PIP 获取补充属性 → PDP 评估策略 → 返回 Permit/Deny → PEP 执行决策。

PDP 做决策时依赖的属性分为四类,这也是 ABAC "基于属性"的核心含义:

属性类别 含义 典型属性 数据来源(PIP)
Subject Attributes 请求发起者的属性 部门、职级、角色、地理位置 用户目录(LDAP/AD)、HR 系统
Resource Attributes 被访问资源的属性 数据分类级别、所属部门、创建者 资源元数据库、文件系统
Action Attributes 操作本身的属性 操作类型(读/写/删除)、是否批量 请求上下文
Environment Attributes 请求发生时的环境 当前时间、IP 地址、设备类型、网络位置 网络信息、时间服务

策略引擎通过组合这四类属性表达访问规则,比如"Subject.dept=财务 AND Resource.level=机密 AND Environment.network=办公网 → Permit"。

策略语言与现代引擎

XACML 本身使用 XML 表达策略,语法冗长。现代实践中更常见的策略引擎和语言包括:

OPA(Open Policy Agent)+ Rego:CNCF 毕业项目,使用 Rego 语言编写策略,以 Sidecar 或 Library 模式嵌入应用。Kubernetes Admission Control、Envoy 外部鉴权都有 OPA 的深度集成。

# OPA Rego 策略示例:只允许财务部 P7 以上在工作时间访问财报
package finance.report

import rego.v1

default allow := false

allow if {
    input.subject.department == "finance"
    input.subject.level >= 7
    is_working_hours
    is_office_network
}

is_working_hours if {
    now := time.now_ns()
    hour := time.clock(now)[0]
    hour >= 9
    hour < 18
    day := time.weekday(now)
    day != "Saturday"
    day != "Sunday"
}

is_office_network if {
    net.cidr_contains("10.0.0.0/8", input.environment.ip)
}

AWS Cedar:Amazon 开源的策略语言,语法比 Rego 更接近自然语言,支持基于实体关系的策略表达。AWS Verified Permissions 底层就是 Cedar。

// Cedar 策略示例:允许文档 owner 执行所有操作
permit (
    principal,
    action,
    resource
) when {
    principal == resource.owner
};

// 允许财务角色读取财报,限定工作时间
permit (
    principal in Role::"finance-analyst",
    action == Action::"read",
    resource in ResourceType::"financial-report"
) when {
    context.hour >= 9 && context.hour < 18
};

Casbin:Go 语言实现的通用访问控制库,支持 ACL、RBAC、ABAC 等多种模型,通过配置文件切换模型,适合中小型项目快速集成。

RBAC + ABAC 混合模式

在实际工程中,纯 ABAC 的策略管理成本很高(需要有人写和维护策略),而纯 RBAC 又不够灵活。最佳实践是分层组合

层级 模型 职责 示例
粗粒度功能权限 RBAC 控制用户能访问哪些功能模块 "运营角色"可以进入订单管理页面
细粒度数据权限 ABAC 控制用户在功能模块内能看到哪些数据 "华东区运营"只能看到华东区的订单
敏感操作审批 ABAC 高风险操作追加动态条件 批量退款需要在办公网络且双人审批

这种混合模式下,RBAC 决定"能不能进门",ABAC 决定"进门之后能看到什么"。

一句话总结:ABAC 通过属性组合实现了 RBAC 无法表达的动态细粒度控制,在实际项目中以 RBAC + ABAC 分层混合为最佳实践,粗粒度用 RBAC,细粒度用 ABAC。

权限的分类与粒度

权限系统设计中最容易犯的错误是把"功能权限"和"数据权限"混为一谈。这两者的控制维度、实现机制和治理策略完全不同。

功能权限:能做什么

功能权限控制的是用户能够执行哪些操作,按粒度从粗到细分为三层:

粒度 描述 前端表现 后端校验
菜单权限 控制用户能看到哪些导航菜单 动态渲染菜单树 路由守卫
页面权限 控制用户能访问哪些页面 无权限时跳转 403 API 网关层校验
按钮/操作权限 控制用户能执行哪些操作 按钮禁用/隐藏 接口级别鉴权

功能权限的建模通常采用树形结构,菜单和按钮构成父子关系:

/**
 * 权限资源的树形建模
 */
public class PermissionResource {
    private Long id;
    private Long parentId;            // 父节点 ID,顶层为 null
    private String code;              // 权限编码,如 "order:refund:approve"
    private String name;              // 显示名称
    private ResourceType type;        // MENU / PAGE / BUTTON / API
    private String path;              // 前端路由路径或后端 API 路径
    private int sortOrder;            // 同级排序

    public enum ResourceType {
        MENU,      // 菜单
        PAGE,      // 页面
        BUTTON,    // 按钮/操作
        API        // API 接口
    }
}

权限编码的设计直接影响后续的权限校验效率。推荐使用 冒号分隔的层级编码(如 order:refund:approve),这样可以通过前缀匹配实现通配:order:* 表示订单模块的所有权限。

数据权限:能看到什么

数据权限控制的是在具有功能权限的前提下,用户能看到哪些数据行或字段。这是权限系统中实现难度最高的部分。

行级权限(Row-Level Security):控制用户能查看或操作哪些数据行。典型场景是"销售只能看到自己负责的客户"。

列级权限(Column-Level Security):控制用户能查看哪些字段。典型场景是"客服能看到订单信息但看不到用户手机号"。

字段级脱敏:介于列级权限和全部可见之间,用户能看到字段但内容被脱敏。典型场景是"手机号显示为 138****5678"。

数据权限的实现有三种主流策略:

策略一:SQL 改写

在 SQL 执行前动态注入过滤条件。MyBatis-Plus 的 DataPermissionInterceptor 就是这种思路。

/**
 * 数据权限 SQL 拦截器(基于 MyBatis Interceptor)
 * 在 SQL 执行前根据用户的数据权限范围注入 WHERE 条件
 */
@Intercepts(@Signature(
    type = Executor.class, method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class DataPermissionInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取当前用户的数据权限范围
        DataScope scope = SecurityContext.currentUser().getDataScope();
        if (scope == DataScope.ALL) {
            return invocation.proceed(); // 全部数据权限,不注入条件
        }

        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
        String originalSql = boundSql.getSql();

        // 根据数据权限范围注入过滤条件
        String filteredSql = switch (scope) {
            case DEPT ->
                injectDeptFilter(originalSql);       // 本部门数据
            case DEPT_AND_BELOW ->
                injectDeptTreeFilter(originalSql);   // 本部门及下属部门
            case SELF ->
                injectSelfFilter(originalSql);       // 仅本人数据
            default -> originalSql;
        };

        // 用改写后的 SQL 替换原始 SQL
        resetBoundSql(ms, boundSql, filteredSql);
        return invocation.proceed();
    }
}

策略二:应用层数据过滤

在 Service 层通过 Repository 的查询参数传入数据权限范围,将过滤逻辑显式化。

/**
 * 应用层数据过滤:在 Service 层显式传入数据权限范围
 */
public class OrderQueryService {

    public Page<OrderDTO> queryOrders(OrderQueryRequest request) {
        DataScope scope = SecurityContext.currentUser().getDataScope();

        // 将数据权限范围转换为查询条件
        OrderQuery query = OrderQuery.builder()
            .status(request.getStatus())
            .dateRange(request.getDateRange())
            .dataScope(scope)                  // 显式传入数据权限
            .scopeDeptIds(resolveDeptIds(scope)) // 解析为部门 ID 集合
            .build();

        return orderRepository.findByQuery(query);
    }

    private Set<Long> resolveDeptIds(DataScope scope) {
        return switch (scope) {
            case ALL -> Set.of();   // 空集表示不限
            case DEPT -> Set.of(SecurityContext.currentUser().getDeptId());
            case DEPT_AND_BELOW -> deptService.getSubTree(
                SecurityContext.currentUser().getDeptId());
            case SELF -> null;      // null 表示按 creator 过滤
        };
    }
}

策略三:数据库视图/行级安全策略

直接利用数据库内置的 RLS(Row-Level Security)能力,在数据库层面强制过滤。PostgreSQL 和 SQL Server 都原生支持 RLS。

-- PostgreSQL 行级安全策略示例
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 策略:用户只能查看本部门的订单
CREATE POLICY dept_isolation ON orders
    FOR SELECT
    USING (dept_id = current_setting('app.current_dept_id')::bigint);

-- 策略:管理员可以查看所有数据
CREATE POLICY admin_full_access ON orders
    FOR ALL
    USING (current_setting('app.current_role') = 'admin');

三种策略的对比:

维度 SQL 改写 应用层过滤 数据库 RLS
侵入性 低(拦截器透明注入) 中(需改造查询接口) 低(数据库层透明)
安全性 中(绕过拦截器即失效) 中(依赖开发规范) 高(数据库强制执行)
灵活性 中(受限于 SQL 表达力)
调试难度 高(SQL 被改写后不易排查) 低(逻辑显式)
适用场景 MyBatis 技术栈 领域驱动设计 多租户 SaaS

多租户场景的权限隔离

在 SaaS 系统中,租户隔离是数据权限的特殊形态。常见的隔离策略按安全级别从高到低排列:

策略 隔离度 成本 适用场景
独立数据库 最高 金融、医疗等强合规行业
共享数据库 + 独立 Schema 中大型 SaaS
共享表 + tenant_id 字段 中小型 SaaS
行级安全策略(RLS) 与共享表配合使用

无论采用哪种隔离策略,权限系统都需要在每一条查询中强制注入租户上下文,遗漏一处就是跨租户数据泄露。

一句话总结:功能权限控制"能做什么"、数据权限控制"能看到什么",两者必须分层设计,数据权限的实现可在 SQL 改写、应用层过滤、数据库 RLS 三种策略中选择。

用户模型设计:组织、用户组与职位

在基础 RBAC 模型中,权限通过角色分配给用户。但当用户规模达到数百上千人时,逐一为用户分配角色变得不可行。需要在用户和角色之间引入分组机制来实现批量化管理。

组织树

企业的组织架构天然是一棵树。将角色绑定到组织节点,用户加入组织后自动继承该节点的角色,新入职员工归入部门即可获得基础权限,转岗只需变更组织归属。

组织树还承担数据权限的范围定义 —— 将用户的数据可见范围限定为其所在组织及下属组织的数据,这是实现"部门数据隔离"的基础。

/**
 * 组织树服务
 * 负责根据组织层级归集角色
 */
public class OrganizationRoleService {
    private final OrganizationRepository orgRepository;

    /**
     * 获取用户通过组织继承的所有角色(含祖先节点的角色)
     * @param deptId 用户所属部门 ID
     */
    public Set<RoleId> getInheritedRoles(Long deptId) {
        Set<RoleId> roles = new HashSet<>();
        Organization current = orgRepository.findById(deptId);
        while (current != null) {
            roles.addAll(current.getBoundRoles());
            current = current.getParentId() != null
                ? orgRepository.findById(current.getParentId())
                : null;
        }
        return roles;
    }
}

public class Organization {
    private Long id;
    private Long parentId;
    private String name;
    private String path;              // 物化路径,如 "/1/3/7/",加速子树查询
    private Set<RoleId> boundRoles;   // 该组织绑定的角色

    // getters...
}

用户组

用户组和组织的区别在于:组织反映行政架构,用户组反映协作关系。一个跨部门项目组、一批需要临时权限的外包人员、所有参与某次灰度测试的用户,都可以用用户组建模。

用户组与角色是多对多关系。角色分配给用户组,组内所有成员自动获得权限。与组织树不同,用户组通常是扁平结构,没有层级继承。

职位

同一个组织内部,不同职位的人需要不同的权限。财务部的总监、主管和出纳,虽然同属财务组织,但权限范围差异很大。

职位与角色的关系是多对多 —— 一个职位可以对应多个角色,一个角色也可以被多个职位使用。用户与职位通常是一对一关系(一个人在组织内只有一个职位)。

完整的用户权限归集

当组织、用户组、职位同时存在时,一个用户的有效权限是多个来源的并集:

用户的有效权限 = 直接分配的角色权限 ∪ 所属组织(含祖先节点)绑定的角色权限 ∪ 所属用户组绑定的角色权限 ∪ 当前职位绑定的角色权限

这个归集逻辑在用户登录或权限变更时执行,计算结果缓存到 Redis,避免每次请求都遍历整棵组织树。

一句话总结:组织树、用户组、职位是用户分组的三个维度,分别对应行政架构、协作关系和职能角色,用户的有效权限是所有维度的权限并集。

数据库设计与核心数据模型

基础五表

最小可用的 RBAC 数据模型由五张表组成:用户表、角色表、权限表,加上两张关联表。

-- 1. 用户表
CREATE TABLE sys_user (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    username    VARCHAR(64)  NOT NULL UNIQUE,
    password    VARCHAR(128) NOT NULL,          -- BCrypt 哈希
    real_name   VARCHAR(64),
    dept_id     BIGINT,                         -- 所属部门
    status      TINYINT      NOT NULL DEFAULT 1, -- 1:启用 0:禁用
    created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_dept (dept_id)
);

-- 2. 角色表
CREATE TABLE sys_role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(64)  NOT NULL UNIQUE,   -- 角色编码,如 "finance_manager"
    name        VARCHAR(128) NOT NULL,          -- 角色名称
    parent_id   BIGINT       DEFAULT NULL,      -- 父角色(RBAC1 角色继承)
    data_scope  TINYINT      NOT NULL DEFAULT 0, -- 数据权限范围:0全部 1本部门 2本部门及下属 3仅本人(注①)
    status      TINYINT      NOT NULL DEFAULT 1,
    created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_parent (parent_id)
);

-- 3. 权限资源表(树形结构)
CREATE TABLE sys_permission (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    parent_id   BIGINT       DEFAULT NULL,      -- 父节点
    code        VARCHAR(128) NOT NULL UNIQUE,   -- 权限编码 "order:refund:approve"
    name        VARCHAR(128) NOT NULL,
    type        TINYINT      NOT NULL,          -- 1:菜单 2:页面 3:按钮 4:API
    path        VARCHAR(256),                   -- 前端路由 / API 路径
    tree_path   VARCHAR(512),                   -- 物化路径 "/1/5/12/",加速子树查询
    sort_order  INT          NOT NULL DEFAULT 0,
    INDEX idx_parent (parent_id),
    INDEX idx_tree_path (tree_path)
);

-- 4. 用户-角色关联表
CREATE TABLE sys_user_role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id     BIGINT NOT NULL,
    role_id     BIGINT NOT NULL,
    UNIQUE KEY uk_user_role (user_id, role_id),
    INDEX idx_role (role_id)
);

-- 5. 角色-权限关联表
CREATE TABLE sys_role_permission (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_id         BIGINT NOT NULL,
    permission_id   BIGINT NOT NULL,
    UNIQUE KEY uk_role_perm (role_id, permission_id),
    INDEX idx_permission (permission_id)
);

注①:将 data_scope 放在角色表上是最简方案 —— 用一个枚举值定义数据可见范围,适合"全部/本部门/仅本人"这类固定维度的场景。当数据权限规则变得复杂(如按区域、按客户标签、按时间范围过滤)时,应将数据权限策略独立为单独的配置表或 ABAC 策略,避免与角色强耦合。这一点在后文"常见陷阱"中有进一步讨论。

扩展表

在基础五表之上,支撑完整模型还需要以下扩展表:

-- 6. 组织/部门表
CREATE TABLE sys_dept (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    parent_id   BIGINT       DEFAULT NULL,
    name        VARCHAR(128) NOT NULL,
    path        VARCHAR(512),                   -- 物化路径 "/1/3/7/"
    sort_order  INT          NOT NULL DEFAULT 0,
    INDEX idx_parent (parent_id),
    INDEX idx_path (path)
);

-- 7. 用户组表
CREATE TABLE sys_user_group (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(64)  NOT NULL UNIQUE,
    name        VARCHAR(128) NOT NULL
);

-- 8. 用户-用户组关联表
CREATE TABLE sys_user_group_member (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id    BIGINT NOT NULL,
    user_id     BIGINT NOT NULL,
    UNIQUE KEY uk_group_user (group_id, user_id)
);

-- 9. 用户组-角色关联表
CREATE TABLE sys_user_group_role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id    BIGINT NOT NULL,
    role_id     BIGINT NOT NULL,
    UNIQUE KEY uk_group_role (group_id, role_id)
);

-- 10. 组织-角色关联表(组织绑定角色,成员自动继承)
CREATE TABLE sys_dept_role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    dept_id     BIGINT NOT NULL,
    role_id     BIGINT NOT NULL,
    UNIQUE KEY uk_dept_role (dept_id, role_id)
);

-- 11. 职位表
CREATE TABLE sys_position (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    code        VARCHAR(64)  NOT NULL UNIQUE,   -- 职位编码,如 "finance_director"
    name        VARCHAR(128) NOT NULL,          -- 职位名称,如 "财务总监"
    dept_id     BIGINT,                         -- 所属部门(可选)
    sort_order  INT          NOT NULL DEFAULT 0
);

-- 12. 职位-角色关联表
CREATE TABLE sys_position_role (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    position_id BIGINT NOT NULL,
    role_id     BIGINT NOT NULL,
    UNIQUE KEY uk_position_role (position_id, role_id)
);

-- 13. 用户-职位关联表(通常一对一,但预留多对多扩展性)
CREATE TABLE sys_user_position (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id     BIGINT NOT NULL,
    position_id BIGINT NOT NULL,
    UNIQUE KEY uk_user_position (user_id, position_id)
);

-- 14. 角色互斥表(SSD 约束)
CREATE TABLE sys_role_exclusion (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_id_a   BIGINT NOT NULL,
    role_id_b   BIGINT NOT NULL,
    UNIQUE KEY uk_exclusion (role_id_a, role_id_b)
);

权限树的存储方案

权限资源是树形结构,存储方案的选择直接影响查询性能:

方案 原理 查询子树 查询祖先 写入性能 适用场景
邻接表(parent_id) 每个节点存储父节点 ID 递归查询 递归查询 O(1) 层级浅(3-5 层)、写多读少
物化路径(path) 存储从根到当前节点的路径 LIKE 前缀匹配 字符串截取 O(depth) 层级中等、读多写少
闭包表(closure table) 存储所有祖先-后代对 单次 JOIN 单次 JOIN O(depth) 层级深、频繁子树查询

权限树一般不超过 4-5 层(菜单→子菜单→页面→按钮),且写入频率远低于读取频率,物化路径是性价比最高的选择。上面的建表语句中 sys_dept 预留了 path 字段,sys_permission 预留了 tree_path 字段,都是为物化路径存储做准备。

一句话总结:基础五表(用户、角色、权限、两张关联表)是 RBAC 的最小数据模型,随着业务复杂度增长逐步引入组织表、用户组表、职位表和约束表,权限树推荐使用物化路径存储。

权限校验的技术实现

权限模型和数据库设计完成后,最后一步是在应用层实现高效的权限校验。

三种校验方式

方式一:过滤器/拦截器(全局统一)

在请求进入 Controller 之前,通过 Filter 或 Interceptor 统一校验。适合 URL 粒度的权限控制。

/**
 * Spring MVC 拦截器实现 URL 级别权限校验
 */
public class PermissionInterceptor implements HandlerInterceptor {

    private final PermissionService permissionService;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    // URL 模式 → 所需权限编码 的映射(启动时从数据库加载)
    // key 示例: "POST:/api/orders/*/refund"  value 示例: "order:refund:approve"
    private final Map<String, String> urlPermissionMap;

    @Override
    public boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) {
        String uri = request.getRequestURI();
        String method = request.getMethod();
        String lookupPath = method + ":" + uri;

        // 1. 匹配 URL 对应的权限编码
        String requiredPermission = urlPermissionMap.entrySet().stream()
            .filter(e -> pathMatcher.match(e.getKey(), lookupPath))
            .map(Map.Entry::getValue)
            .findFirst().orElse(null);
        if (requiredPermission == null) {
            return true; // 未配置权限的接口,放行(或可改为默认拒绝)
        }

        // 2. 获取当前用户的权限集合(从缓存读取)
        Long userId = SecurityContext.getCurrentUserId();
        Set<String> userPermissions = permissionService.getUserPermissions(userId);

        // 3. 校验(支持通配符匹配)
        if (!matchesAny(userPermissions, requiredPermission)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return false;
        }
        return true;
    }

    /**
     * 支持通配符的权限匹配
     * "order:*" 可以匹配 "order:refund:approve"
     */
    private boolean matchesAny(Set<String> owned, String required) {
        for (String perm : owned) {
            if (perm.equals(required)) return true;
            if (perm.endsWith(":*")) {
                String prefix = perm.substring(0, perm.length() - 1);
                if (required.startsWith(prefix)) return true;
            }
        }
        return false;
    }
}

方式二:注解(方法级声明式)

在 Controller 方法上通过注解声明所需权限,由 AOP 切面统一处理。语义更清晰,适合操作级粒度。

/**
 * 自定义权限注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    /** 所需权限编码,支持多个 */
    String[] value();
    /** 多个权限的逻辑关系:AND(全部满足)或 OR(满足其一) */
    Logical logical() default Logical.AND;
}

/**
 * 权限注解的 AOP 切面
 */
@Aspect
@Component
public class PermissionAspect {

    private final PermissionService permissionService;

    @Around("@annotation(requiresPermission)")
    public Object checkPermission(ProceedingJoinPoint pjp,
                                   RequiresPermission requiresPermission) throws Throwable {
        String[] required = requiresPermission.value();
        Logical logical = requiresPermission.logical();
        Long userId = SecurityContext.getCurrentUserId();
        Set<String> owned = permissionService.getUserPermissions(userId);

        boolean pass = (logical == Logical.AND)
            ? Arrays.stream(required).allMatch(r -> permissionService.matchesAny(owned, r))
            : Arrays.stream(required).anyMatch(r -> permissionService.matchesAny(owned, r));

        if (!pass) {
            throw new ForbiddenException(
                "权限不足: " + String.join(", ", required));
        }
        return pjp.proceed();
    }
}

// 使用示例
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @PostMapping("/{id}/refund")
    @RequiresPermission({"order:refund:approve"})
    public Result approveRefund(@PathVariable Long id) {
        // 业务逻辑
    }
}

方式三:编程式(灵活动态)

在业务代码中显式调用权限检查,适合权限逻辑与业务逻辑耦合度高的场景。

public class OrderService {

    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);

        // 动态权限判断:只有订单创建者或管理员可以取消
        Long currentUserId = SecurityContext.getCurrentUserId();
        if (!order.getCreatorId().equals(currentUserId)
            && !permissionService.hasRole(currentUserId, "admin")) {
            throw new ForbiddenException("只有订单创建者或管理员可以取消订单");
        }
        // 取消逻辑...
    }
}

权限缓存策略

权限校验发生在每个请求的关键路径上,必须做到毫秒级响应。将用户权限数据缓存到 Redis 是标准做法:

/**
 * 权限缓存服务
 * 用户登录时加载权限到 Redis,权限变更时主动失效
 */
@Service
public class PermissionCacheService {

    private static final String CACHE_KEY_PREFIX = "perm:user:";
    private static final String LOCK_KEY_PREFIX = "perm:lock:";
    private static final Duration CACHE_TTL = Duration.ofMinutes(30);

    private final StringRedisTemplate redis;

    /**
     * 获取用户权限(缓存优先)
     * 使用 Redis SETNX 作为分布式锁,防止缓存击穿时多线程并发加载数据库
     */
    public Set<String> getUserPermissions(Long userId) {
        String key = CACHE_KEY_PREFIX + userId;
        Set<String> cached = redis.opsForSet().members(key);
        if (cached != null && !cached.isEmpty()) {
            return cached;
        }
        // 缓存未命中:用分布式锁防止缓存击穿
        String lockKey = LOCK_KEY_PREFIX + userId;
        Boolean locked = redis.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
        if (Boolean.TRUE.equals(locked)) {
            try {
                // Double-check:获取锁后再检查一次缓存(可能被其他线程刚写入)
                cached = redis.opsForSet().members(key);
                if (cached != null && !cached.isEmpty()) {
                    return cached;
                }
                Set<String> permissions = loadFromDb(userId);
                redis.opsForSet().add(key, permissions.toArray(new String[0]));
                redis.expire(key, CACHE_TTL);
                return permissions;
            } finally {
                redis.delete(lockKey);
            }
        }
        // 未获取到锁,短暂等待后重试读缓存
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        cached = redis.opsForSet().members(key);
        return (cached != null && !cached.isEmpty()) ? cached : loadFromDb(userId);
    }

    /**
     * 权限变更时主动失效缓存
     * 当角色权限变更时,需要批量失效该角色关联的所有用户
     */
    public void invalidateByRole(Long roleId) {
        List<Long> userIds = userRoleRepository.findUserIdsByRoleId(roleId);
        List<String> keys = userIds.stream()
            .map(id -> CACHE_KEY_PREFIX + id)
            .toList();
        redis.delete(keys);
    }
}

性能优化:位运算权限码

当权限数量可控(通常 < 64 个核心操作权限)时,可以用位运算大幅提升校验性能:

/**
 * 位运算权限码
 * 将权限映射为 long 的每一位,校验只需一次 AND 操作
 */
public class BitPermission {
    // 权限定义(每个权限占一个 bit 位)
    public static final long READ       = 1L;       // 0001
    public static final long WRITE      = 1L << 1;  // 0010
    public static final long DELETE     = 1L << 2;  // 0100
    public static final long APPROVE    = 1L << 3;  // 1000

    /**
     * 校验是否拥有指定权限
     * 单次 AND 运算,O(1) 时间复杂度
     */
    public static boolean hasPermission(long userPermBits, long requiredBits) {
        return (userPermBits & requiredBits) == requiredBits;
    }

    /**
     * 授予权限(OR 运算)
     */
    public static long grant(long current, long newPerm) {
        return current | newPerm;
    }

    /**
     * 撤销权限(AND NOT 运算)
     */
    public static long revoke(long current, long permToRevoke) {
        return current & ~permToRevoke;
    }
}

// 使用示例
long userPerms = BitPermission.grant(0L, BitPermission.READ | BitPermission.WRITE);
boolean canDelete = BitPermission.hasPermission(userPerms, BitPermission.DELETE); // false
boolean canRead = BitPermission.hasPermission(userPerms, BitPermission.READ);     // true

位运算适合权限数量固定、校验频率极高的核心路径。对于动态增减的菜单级权限,仍然使用 Set 匹配方式。

一句话总结:权限校验可以通过拦截器(URL 级)、注解(方法级)、编程式(业务逻辑级)三种方式实现,配合 Redis 缓存和位运算优化可以做到每次校验微秒级完成。

微服务架构下的权限设计

单体应用的权限校验在进程内完成,逻辑相对简单。进入微服务架构后,权限系统面临三个新挑战:身份如何跨服务传播、鉴权逻辑放在哪里、服务间调用的权限如何管控。

网关统一鉴权 vs 服务自主鉴权

两种主流方案各有适用场景:

维度 网关统一鉴权 服务自主鉴权
核心思路 网关做完所有权限校验,服务信任网关转发的请求 网关只做认证(验 Token),授权由各服务自行处理
优点 鉴权逻辑集中,服务无感知 服务自治,可根据领域模型定制鉴权逻辑
缺点 网关变重,成为性能瓶颈;细粒度权限难以在网关层表达 鉴权逻辑分散,维护成本高
适用场景 权限粒度在 URL/API 级别 权限粒度涉及业务规则和数据权限

实际项目中最常见的是分层鉴权:网关负责粗粒度鉴权(认证 + URL 级权限),服务负责细粒度鉴权(操作权限 + 数据权限)。

OAuth 2.0 + OIDC 集成

在微服务架构中,OAuth 2.0 + OpenID Connect 是认证授权的事实标准协议栈:

协议 职责 核心概念
OAuth 2.0 授权框架 Access Token、Refresh Token、Scope、Grant Type
OpenID Connect 认证层(基于 OAuth 2.0) ID Token、UserInfo Endpoint、Claims

OAuth 2.0 定义了获取 Access Token 的标准流程(Authorization Code、Client Credentials 等),Access Token 中的 scope 字段表达了粗粒度的授权范围。

OIDC 在 OAuth 2.0 之上增加了 ID Token(JWT 格式),包含用户身份信息(sub、name、email 等),解决了 OAuth 2.0 只有授权没有认证的问题。

在权限系统中,推荐的集成方式是:

  1. 认证服务(如 Keycloak、Casdoor)负责签发 JWT Token,Token 的 Claims 中携带用户 ID、角色列表等身份信息
  2. API 网关 验证 Token 签名和有效期,解析 Claims 并作为 Header 透传给下游服务
  3. 业务服务 从 Header 中读取身份上下文,执行业务级权限校验

JWT Token 与权限上下文透传

JWT Token 中应该携带多少权限信息?这是一个权衡:

策略 Token 内容 优点 缺点
轻量 Token 只含 userId、roles Token 体积小,签发快 每次请求需查询权限详情
重量 Token 含完整权限列表 服务端无需查库 Token 体积大,权限变更不实时
折中方案 含 roles + 权限版本号 兼顾体积和实时性 需要实现版本比对逻辑

推荐折中方案:Token 中携带角色列表和权限版本号(permission_version),服务端缓存权限数据并通过版本号判断是否需要刷新。

/**
 * 权限上下文透传
 * 网关解析 JWT 后将身份信息通过 Header 传递给下游
 */
public class AuthGatewayFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = extractToken(exchange.getRequest());
        Claims claims = jwtService.parseAndVerify(token);

        // 将身份信息注入 Header,透传给下游服务
        ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
            .header("X-User-Id", claims.getSubject())
            .header("X-User-Roles", String.join(",", claims.get("roles", List.class)))
            .header("X-User-Dept-Id", claims.get("dept_id", String.class))
            .header("X-Perm-Version", claims.get("perm_version", String.class))
            .build();

        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
}

服务间调用的权限管控

微服务之间的内部调用也需要权限管控,防止"A 服务调用 B 服务绕过权限检查"的问题。

方案一:透传用户上下文 — 内部调用时将原始用户的身份信息透传(通过 Header 或 gRPC Metadata),被调用服务依然按用户身份做权限校验。适合用户操作触发的链式调用。

方案二:服务身份认证 — 使用 mTLS 或 Service Account Token 标识调用方服务的身份,被调用服务基于"调用方是谁"来决定是否允许。适合服务间的后台任务和数据同步。Kubernetes 的 ServiceAccount + RBAC 就是这种模式。

方案三:混合模式 — 有用户上下文时用方案一,无用户上下文(如定时任务、消息消费)时用方案二。

一句话总结:微服务架构下推荐网关粗粒度鉴权 + 服务细粒度鉴权的分层模式,通过 OAuth 2.0/OIDC 签发 JWT Token,网关解析后将身份上下文透传给下游服务。

常见陷阱与最佳实践

角色爆炸

随着业务增长,角色数量失控增长到数百甚至上千个,每个细微的权限差异都创建一个新角色。根本原因是将本应由 ABAC 策略表达的细粒度条件硬编码为 RBAC 角色。

解决方案是引入 ABAC 层处理动态条件,RBAC 只保留与组织职能对应的核心角色。定期审计角色使用率,合并低频角色。

超级管理员绕过检查

很多系统对超级管理员角色直接 return true 跳过所有权限检查。这带来两个风险:超管账号被盗后无任何防线;审计日志中超管的操作无法追溯权限依据。

最佳实践是超管也走正常的权限校验流程,只是拥有所有权限。同时对超管的敏感操作(如修改权限配置、删除数据)增加二次确认或双人审批。

前端权限 ≠ 安全

前端通过隐藏菜单和按钮实现的"权限控制"只是用户体验优化,不是安全机制。用户可以通过浏览器开发工具、直接调用 API 绕过前端控制。后端必须独立校验每一个接口的权限,前后端的权限配置需要保持同步但独立实现。

权限变更的实时性

用户权限被修改后(如移除某个角色),如果使用了 JWT Token 且 Token 未过期,用户仍然持有旧权限。解决方案:

策略 实时性 复杂度
缩短 Token 有效期(如 5 分钟) 低(但增加 Token 刷新频率)
权限版本号比对 中(需要实现版本机制)
Token 黑名单 即时 高(需要维护分布式黑名单)
强制重新登录 即时 低(但用户体验差)

推荐权限版本号方案:权限变更时递增版本号,服务端校验时比对 Token 中的版本号与最新版本号,不一致则重新加载权限。

数据权限与功能权限耦合

常见的错误是在同一张配置表里混合功能权限和数据权限,导致管理混乱。例如用一个角色同时定义"能访问订单页面"和"只能看华东区的订单"。

正确做法是分层解耦:功能权限通过 RBAC 角色管理,数据权限通过独立的数据范围策略(DataScope)管理,两者在运行时组合生效。

审计:AAA 的最后一环

第一章提到了 AAA 模型中的 Accounting(审计),但很多权限系统在实现时只做了认证和授权,忽视了审计。审计日志是事后追溯、合规检查和异常检测的基础,缺失审计的权限系统是不完整的。

权限审计至少应记录以下信息:(用户 ID + 身份信息)、什么时候(时间戳)、做了什么(操作类型 + 目标资源)、结果是什么(成功/拒绝 + 拒绝原因)、在哪里(IP、设备、Session ID)。审计日志应写入独立存储(如 Elasticsearch),与业务库分离,并且审计记录本身不可被修改或删除。

对于敏感操作(权限配置变更、数据导出、批量删除),除了审计日志外还应触发实时告警,而不只是事后查看。

最小权限原则的落地

最小权限原则(Principle of Least Privilege)是权限设计的第一原则:只授予完成工作所必需的最小权限集合。落地建议:

  • 新用户默认无权限,而不是默认拥有基础角色
  • 定期审计权限使用情况,回收 90 天内未使用的权限
  • 敏感权限设置有效期,到期自动回收
  • 临时权限通过审批流程授予,自动过期

一句话总结:权限系统的常见陷阱多源于"图省事" —— 跳过校验、前端代替后端、角色代替策略,遵循最小权限原则和分层解耦是避免这些陷阱的根本方法。

总结

权限系统的设计可以归纳为三个层面的决策:

模型层面:选择合适的访问控制模型。大多数企业系统以 RBAC 为主体,在需要细粒度动态控制的场景(数据权限、条件审批)引入 ABAC 策略。不要为了"先进"而选择 ABAC —— 策略的编写和维护成本远高于 RBAC 的角色配置。

架构层面:在单体应用中,权限校验在过滤器或 AOP 层完成;在微服务架构中,采用网关粗粒度鉴权 + 服务细粒度鉴权的分层模式,通过 JWT Token 透传身份上下文。

工程层面:功能权限和数据权限分层实现,权限数据缓存到 Redis 并通过版本号保证一致性,核心路径可以用位运算优化到微秒级。

贯穿始终的设计原则:最小权限(只给必要的权限)、职责分离(互斥角色不能共存)、纵深防御(前端、网关、服务端多层校验,任何一层都不能独立保证安全)、可审计(所有访问行为可追溯,形成 AAA 闭环)。

权限系统不是一次性工程,而是随着业务演进持续迭代的基础设施。从 RBAC0 起步,在实际需求驱动下逐步引入角色继承、约束、ABAC 策略,这种渐进式演进远比一开始就搭建"理想模型"更务实。

加载导航中...

评论