209|权限系统设计:RBAC/ABAC、组织模型、数据权限和审计

架构设计与系统设计约 13 分钟阅读7,421 字符
知识库文档 210 / 251

阅读时间:约 40–50 分钟 前置知识:了解 Web 应用基本架构、数据库基本操作、认证(Authentication)与授权(Authorization)的区别


一、为什么权限系统是架构设计的"地基"

如果把一个业务系统比作一栋大楼,认证(Authentication)是大楼的"门禁"——你是谁,能否进门;而授权(Authorization)是每一间办公室的"门锁"——你进门之后,能去哪些房间,能做什么操作。权限系统就是这套门锁体系的完整实现。

很多工程师在职业生涯早期会把权限系统简单理解为"用户-角色-权限"三张表,觉得"不就是 CRUD 吗"。但真正进入企业级系统设计后会发现,权限系统是跨越业务、跨越服务、跨越数据层的横向切面,其设计质量直接决定了系统的安全边界、扩展能力和合规成本。

一个失败的权限系统长什么样?

  • 每个业务服务自己写一套权限判断逻辑,代码散落各处,规则不一致
  • 运营同学说"给这个用户开个只读权限",开发说"要改代码,下周上线"
  • 审计时发现半年前的权限变更"查不到记录",合规亮红灯
  • 数据权限是"在 SQL 里拼 WHERE 条件",换个接口就漏了
  • 多租户场景下,租户 A 的管理员不小心看到了租户 B 的数据

这些问题,根源都在于权限系统没有在设计之初就认真对待。

本文将从 RBAC 与 ABAC 两大模型开始,深入到组织模型、数据权限和审计体系,最后给出工程落地的最佳实践和面试视角的提问。读完你会对权限系统设计有一个完整的认知框架。


二、RBAC:基于角色的访问控制

2.1 RBAC 的四个层次

RBAC(Role-Based Access Control)由 NIST 在 1992 年提出,2004 年成为 ANSI 标准。它通过"角色"这一中间层解耦了用户和权限,是目前企业软件中使用最广泛的权限模型。RBAC 分为四个层次:

RBAC0 —— 基础模型

核心关系:User → Role → Permission。一个用户可以有多个角色,一个角色可以拥有多个权限。权限到角色的映射由管理员维护,用户到角色的映射由账号管理员维护。

以最简单的 CMS 系统为例:

code
用户"张三" → 角色"编辑" → 权限["文章.创建", "文章.编辑", "文章.删除(仅自己)"]
用户"李四" → 角色"审核员" → 权限["文章.发布", "文章.驳回", "文章.查看"]

权限粒度的设计是这里的关键决策。通常有三层:

  • 菜单/页面权限:控制左侧导航栏、页面入口可见性(最粗粒度,前端控制)
  • 操作/按钮权限:控制页面内具体按钮是否可点击(中粒度,前后端双重校验)
  • 接口/API 权限:控制后端接口调用权限(最细粒度,后端强制校验)

注意:前端隐藏按钮只是 UX 优化,真正的权限校验必须发生在后端。前端的一切都可以被绕过。

RBAC1 —— 角色继承

角色之间可以有父子关系,子角色自动继承父角色的所有权限。典型的场景是组织架构映射:

code
"CEO" 继承 "部门总监" 的所有权限
"部门总监" 继承 "团队主管" 的所有权限
"团队主管" 继承 "普通员工" 的所有权限

继承可以是树状(单继承)或图状(多继承)。多继承更灵活但容易产生权限冲突,需要定义冲突解决策略(如"拒绝优先"或"允许优先")。

RBAC角色继承树:CEO→部门总监→团队主管→普通员工,展示角色层级和多继承关系
RBAC角色继承树:CEO→部门总监→团队主管→普通员工,展示角色层级和多继承关系

RBAC2 —— 角色约束

对角色分配施加限制,防止权限滥用。核心约束类型:

约束类型 说明 示例
互斥角色(SoD) 同一用户不能同时拥有两个互斥角色 不能同时是"采购申请"和"采购审批"
基数约束 限制某个角色可分配的最大用户数 系统管理员最多 3 人
先决条件约束 获取某角色必须先拥有另一个角色 要成为"高级审核员"必须先有"审核员"
时间约束 角色只在特定时间段生效 临时管理员权限有效期为 24 小时

互斥角色(Separation of Duties, SoD)是权限安全的核心手段。在金融系统中,SoD 是合规的硬性要求——发起交易的人不能同时是审批交易的人。

RBAC3 —— 完整模型

RBAC3 = RBAC1 + RBAC2,即同时支持角色继承和角色约束。这是企业级 RBAC 的最终形态。

2.2 RBAC 的数据库设计

RBAC 的核心表结构:

sql
-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(64) NOT NULL UNIQUE,
    status TINYINT NOT NULL DEFAULT 1
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY,
    role_code VARCHAR(64) NOT NULL UNIQUE,
    role_name VARCHAR(128) NOT NULL,
    parent_id BIGINT DEFAULT NULL,
    status TINYINT NOT NULL DEFAULT 1
);

-- 权限表(资源+操作)
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY,
    perm_code VARCHAR(128) NOT NULL UNIQUE,
    perm_name VARCHAR(128) NOT NULL,
    resource_type VARCHAR(32) NOT NULL,
    parent_id BIGINT DEFAULT NULL
);

-- 用户-角色关联
CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id)
);

-- 角色-权限关联
CREATE TABLE sys_role_permission (
    role_id BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id)
);

在生产环境中,实际表结构会远不止这些。还需要考虑:

  • 用户组表:批量授权场景,用户组可以直接关联角色
  • 角色互斥表:sys_role_mutex(role_id_a, role_id_b) 记录互斥关系
  • 权限变更日志表:记录每次权限的授予和撤销
  • 临时权限表:有时效性的权限授予

2.3 RBAC 的工程实践要点

权限标识的设计

权限标识(perm_code)的命名规范非常重要。推荐使用 资源:操作 的格式:

code
user:create          # 创建用户
user:update          # 修改用户
user:delete          # 删除用户
user:view            # 查看用户
order:create         # 创建订单
order:approve        # 审批订单
report:export_all    # 导出全部报表
report:export_dept   # 仅导出本部门报表

对于 RESTful API,权限标识可以映射到 HTTP 方法:

code
GET    /api/users       → user:list
POST   /api/users       → user:create
PUT    /api/users/{id}  → user:update
DELETE /api/users/{id}  → user:delete

权限鉴权的性能优化

在 RBAC 模型中,每次请求都需要查询用户的所有角色及其权限。如果每次都查数据库,高并发下会成为瓶颈。工程上的典型做法是:

  1. 用户登录时:将用户的权限列表加载到 Redis(或本地缓存),格式为 Set<perm_code>
  2. 权限变更时:主动刷新缓存,或使用版本号机制让缓存自然失效
  3. 鉴权时:直接从缓存中 O(1) 判断 userPerms.contains("order:create")
java
// 权限缓存结构示例
// Redis Key: "user:perms:12345"
// Value: {"order:create","order:view","user:view","report:export_dept"}

public boolean hasPermission(Long userId, String permCode) {
    Set<String> perms = redisTemplate.opsForSet()
        .members("user:perms:" + userId);
    return perms != null && perms.contains(permCode);
}

RBAC 的局限性

RBAC 在"静态角色"场景下工作得很好,但面对动态条件时会力不从心:

  • "只能查看自己部门的订单" → 角色无法表达"自己部门"这个动态条件
  • "只能在工作时间访问" → 角色无法表达时间约束
  • "只能修改金额小于 10000 元的订单" → 角色无法表达数值条件

这些场景,正是 ABAC 的用武之地。


三、ABAC:基于属性的访问控制

3.1 ABAC 的核心概念

ABAC(Attribute-Based Access Control)不依赖预设的角色,而是通过属性动态计算访问决策。一次访问请求包含四类属性:

属性类别 说明 示例
Subject(主体) 谁在访问 用户 ID、部门、职级、IP 地址、设备类型
Resource(资源) 访问什么 文档 ID、文档密级、所属部门、创建时间
Action(动作) 做什么操作 读、写、删除、审批、导出
Environment(环境) 在什么条件下 时间、地理位置、网络区域、安全等级

ABAC 的策略引擎采用 PEP/PDP/PIP/PAP 架构:

  • PEP(Policy Enforcement Point,策略执行点):拦截每一次访问请求,向 PDP 询问是否允许
  • PDP(Policy Decision Point,策略决策点):根据策略规则和属性做出"允许/拒绝"决策
  • PIP(Policy Information Point,策略信息点):提供属性数据(如从数据库查询用户部门)
  • PAP(Policy Administration Point,策略管理点):管理策略规则的生命周期

ABAC策略执行时序图:PEP/PDP/PIP三方交互,展示属性查询和策略决策的完整流程
ABAC策略执行时序图:PEP/PDP/PIP三方交互,展示属性查询和策略决策的完整流程

3.2 主流 ABAC 策略引擎

OPA(Open Policy Agent)

CNCF 毕业项目,使用 Rego 语言编写策略。策略与应用程序解耦,OPA 以 sidecar 或独立服务方式部署。

rego
# OPA 策略示例:只有本部门员工且工作时间才能查看内部文档
package document.access

default allow = false

allow {
    input.subject.department == input.resource.department
    input.resource.classification != "confidential"
    input.environment.current_hour >= 9
    input.environment.current_hour < 18
}

Casbin

支持多种访问控制模型的 Go 库,以"模型文件 + 策略文件"的方式定义权限:

ini
# model.conf - 支持 RBAC + 条件
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, cond

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) && eval(p.cond)

3.3 RBAC vs ABAC:如何选择

RBACvsABAC选型决策树:从权限规则是否依赖动态属性出发,引导选择合适模型
RBACvsABAC选型决策树:从权限规则是否依赖动态属性出发,引导选择合适模型

维度 RBAC ABAC
核心概念 用户 → 角色 → 权限 主体属性 + 资源属性 + 环境属性 → 策略
灵活性 低,角色是静态的 高,属性可动态组合
复杂度 低,易于理解和维护 高,策略编写和调试成本大
性能 高,鉴权 O(1) 较低,需实时计算属性
适用场景 权限边界清晰的管理系统 需要精细控制的动态场景
典型产品 企业内部管理系统、OA 云平台 IAM、数据平台

推荐策略:绝大多数系统采用 RBAC 为主、ABAC 为辅 的混合模型。RBAC 负责粗粒度的"能不能进这个页面/调这个接口",ABAC 负责细粒度的"在这个页面里能看到哪些数据、能做什么操作"。例如:

  • RBAC 控制:有"订单管理"角色的用户才能访问 /api/orders 接口
  • ABAC 控制:该用户只能看到 order.department == user.department 的订单,且只能在工作时间修改金额小于 10000 元的订单

四、组织模型设计

权限系统不是孤立存在的,它必须与组织架构深度绑定。组织模型的设计决定了权限分配的灵活性和管理成本。

4.1 用户与账号

用户是权限的最小主体。设计时需要区分几个概念:

  • 自然人(User):一个真实的人,有唯一的身份证/手机号
  • 账号(Account):一个自然人在系统中的身份,一人可以有多个账号(如管理员账号和普通账号)
  • 主体(Principal):权限判断的最终对象,通常是账号,但也可能是 API Key、服务账号、设备

服务账号(Service Account) 是容易被忽略但非常重要的设计点。微服务之间的调用需要权限控制,不能所有服务间调用都"裸奔"。服务账号通常:

  • 没有密码,使用 API Key 或证书认证
  • 不允许登录 Web 控制台
  • 权限范围被严格限制在最小必要原则

4.2 用户组与部门

当用户数量达到数千甚至数万时,逐一给用户分配角色是不现实的。用户组(User Group)解决了批量授权的问题。

用户组 vs 角色的区别(这是面试高频题):

维度 用户组(Group) 角色(Role)
本质 用户的集合 权限的集合
关系 用户 ∈ 用户组 角色拥有权限
典型场景 按组织架构分组 按职能职责分组
示例 "研发部-基础架构组" "K8s 集群管理员"

正确的做法:用户通过用户组间接获得角色。

code
用户"张三" → 用户组"研发部-基础架构组" → 角色"K8s 只读" + 角色"GitLab Developer"

而不是让用户直接关联角色。这样当张三转岗时,只需将他从"基础架构组"移到"业务开发组",他自动获得新角色的权限,同时失去旧角色的权限。

4.3 多租户组织模型

SaaS 系统的组织模型更为复杂,需要处理多层级的隔离:

多租户组织模型层级图:平台层→租户→一级部门→二级部门→用户,展示完整层级
多租户组织模型层级图:平台层→租户→一级部门→二级部门→用户,展示完整层级

多租户权限设计的核心挑战:

  1. 租户内 RBAC 与平台 RBAC 的分离:每个租户有自己的角色体系,平台也有跨租户的平台角色(如"平台运营")
  2. 租户间数据隔离:租户 A 的角色不能看到租户 B 的数据,这需要在数据权限层做强制隔离
  3. 租户自定义角色:允许租户管理员在自己的租户空间内创建和分配角色
  4. 功能权限的租户级开关:不同套餐的租户看到不同的功能菜单

4.4 组织模型的数据库设计

sql
-- 租户表
CREATE TABLE sys_tenant (
    id BIGINT PRIMARY KEY,
    tenant_code VARCHAR(64) NOT NULL UNIQUE,
    tenant_name VARCHAR(128) NOT NULL
);

-- 部门表(树形结构)
CREATE TABLE sys_dept (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    parent_id BIGINT DEFAULT NULL,
    dept_name VARCHAR(128) NOT NULL,
    ancestors VARCHAR(512) DEFAULT '',
    sort_order INT DEFAULT 0
);

-- 用户表(关联租户和部门)
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    dept_id BIGINT DEFAULT NULL,
    username VARCHAR(64) NOT NULL,
    UNIQUE KEY uk_tenant_username (tenant_id, username)
);

-- 用户组表
CREATE TABLE sys_user_group (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    group_name VARCHAR(128) NOT NULL
);

-- 用户组-用户关联
CREATE TABLE sys_group_user (
    group_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    PRIMARY KEY (group_id, user_id)
);

-- 用户组-角色关联(批量授权)
CREATE TABLE sys_group_role (
    group_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (group_id, role_id)
);

五、数据权限设计

数据权限是权限系统中最复杂也最容易出错的部分。功能权限控制"能不能做",数据权限控制"能看到什么数据"。一个常见的血泪教训是:功能权限做得很好,但查询接口忘了加数据权限,导致用户看到了不该看的数据。

5.1 数据权限的维度

数据权限通常在以下维度上做控制:

维度 说明 示例
全部数据 不做任何过滤 超级管理员查看所有订单
本部门及子部门 查看所在部门树的所有数据 部门总监查看本部门所有员工的订单
仅本部门 只看本部门数据 部门助理查看本部门订单
仅本人 只看自己的数据 普通员工查看自己的订单
自定义 按业务规则定义 只看"状态=已审批"的订单

5.2 实现方式

方式一:SQL 拦截(MyBatis 拦截器)

最常用的方式,通过 MyBatis Plugin 在 SQL 执行前动态拼接 WHERE 条件。

java
@Intercepts({
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataScopeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        DataScope scope = SecurityContextHolder.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 dataScopeSql = originalSql + " AND " + buildScopeCondition(scope);
        // 反射替换 BoundSql 中的 SQL
        return invocation.proceed();
    }
}

方式二:应用层注解 + AOP

在 Mapper 方法上加注解,AOP 切面自动注入数据权限参数:

java
@Mapper
public interface OrderMapper {
    @DataScope(deptAlias = "o", userAlias = "o")
    List<Order> selectOrderList(OrderQuery query);
}

方式三:中间件层(ShardingSphere)

在 SQL 路由层做数据权限控制,与应用代码解耦。适合对数据隔离有严格要求的场景。

5.3 数据权限的完整执行流程

数据权限的鉴权链路包含了从功能权限判定到数据范围过滤的完整过程:

数据权限完整执行流程时序图:从客户端请求到数据权限拦截器拼接SQL、数据库返回过滤后结果
数据权限完整执行流程时序图:从客户端请求到数据权限拦截器拼接SQL、数据库返回过滤后结果

5.4 常见坑与应对

坑 1:关联查询漏过滤

sql
-- 错误:只过滤了主表,关联表没过滤
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.dept_id IN (100, 101)
-- u 表的数据可能包含其他部门用户的信息

解决:关联查询时明确哪些表需要过滤,制定统一的过滤规则。

坑 2:导出功能绕过数据权限

java
// 危险:导出接口直接查全表
@GetMapping("/export")
public void exportOrders(HttpServletResponse response) {
    List<Order> orders = orderMapper.selectAll();  // 没加数据权限!
}

解决:所有数据查询接口(包括导出、统计、报表)必须走统一的数据权限过滤。

坑 3:数据权限范围变更的缓存一致性

用户从"部门 A"调到"部门 B",如果数据权限范围被缓存,用户可能在一段时间内仍然看到旧部门的数据。需要设计缓存刷新机制。


六、权限审计

权限审计不是"锦上添花",而是企业级系统的合规刚需。ISO 27001、SOC 2、等保 2.0 等标准都要求对权限变更和访问行为有完整的审计记录。

6.1 审计日志设计

权限审计日志需要记录谁、在什么时间、对什么对象、做了什么操作、操作前后的值是什么:

sql
CREATE TABLE audit_permission_log (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    operator_id BIGINT NOT NULL,
    operator_name VARCHAR(64) NOT NULL,
    operator_ip VARCHAR(45),
    target_type VARCHAR(32) NOT NULL,
    target_id VARCHAR(128) NOT NULL,
    target_name VARCHAR(256),
    action VARCHAR(32) NOT NULL,
    before_value TEXT,
    after_value TEXT,
    result VARCHAR(16) NOT NULL,
    fail_reason VARCHAR(512),
    created_at DATETIME NOT NULL,
    INDEX idx_tenant_time (tenant_id, created_at),
    INDEX idx_target (target_type, target_id),
    INDEX idx_operator (operator_id, created_at)
);

6.2 审计体系架构

一个完整的权限审计体系包含三个层次:

权限审计体系架构图:日志采集层、处理分析层、输出层三层架构及数据流转
权限审计体系架构图:日志采集层、处理分析层、输出层三层架构及数据流转

6.3 关键审计场景

场景 1:权限变更追溯

当出现安全事故时,需要快速回答:"这个用户什么时候获得的这个权限?谁批准的?"

sql
SELECT operator_name, action, before_value, after_value, created_at
FROM audit_permission_log
WHERE target_id = '12345'
  AND target_type = 'USER'
  AND action IN ('GRANT', 'REVOKE')
ORDER BY created_at DESC;

场景 2:僵尸权限发现

"僵尸权限"是指用户已经不再需要但未及时回收的权限。通过分析"最近 90 天未被使用的权限"来发现:

sql
SELECT DISTINCT p.name
FROM sys_role_permission rp
JOIN sys_permission p ON rp.permission_id = p.id
WHERE rp.role_id IN (
    SELECT role_id FROM sys_user_role WHERE user_id = ?
)
AND p.id NOT IN (
    SELECT DISTINCT permission_id FROM audit_access_log
    WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 90 DAY)
);

场景 3:SoD 违规检测

sql
SELECT u.id, u.username, r1.role_name, r2.role_name
FROM sys_user_role ur1
JOIN sys_user_role ur2 ON ur1.user_id = ur2.user_id
JOIN sys_role r1 ON ur1.role_id = r1.id
JOIN sys_role r2 ON ur2.role_id = r2.id
JOIN sys_role_mutex m ON (
    (m.role_id_a = ur1.role_id AND m.role_id_b = ur2.role_id)
    OR (m.role_id_a = ur2.role_id AND m.role_id_b = ur1.role_id)
)
JOIN sys_user u ON ur1.user_id = u.id;

七、权限系统的工程落地

7.1 性能优化

权限校验是每次请求的必经之路,必须做到极低延迟。

优化策略:

  1. 多级缓存:本地缓存(Caffeine)→ 分布式缓存(Redis)→ 数据库
  2. 批量预加载:用户登录时一次性加载该用户的所有权限到 Redis Set
  3. 权限编码压缩:使用 BitMap 或 RoaringBitmap 压缩大量权限标识
  4. 异步鉴权:对于非关键路径(如审计日志),可以异步记录,不阻塞主流程
java
// 多级缓存示例
public Set<String> getUserPermissions(Long userId) {
    // L1: 本地缓存 (Caffeine, 5分钟过期)
    Set<String> perms = localCache.getIfPresent(userId);
    if (perms != null) return perms;

    // L2: Redis 缓存 (30分钟过期)
    perms = redisTemplate.opsForSet().members("user:perms:" + userId);
    if (perms != null && !perms.isEmpty()) {
        localCache.put(userId, perms);
        return perms;
    }

    // L3: 数据库
    perms = loadPermissionsFromDB(userId);
    redisTemplate.opsForSet().add("user:perms:" + userId,
        perms.toArray(new String[0]));
    redisTemplate.expire("user:perms:" + userId, 30, TimeUnit.MINUTES);
    localCache.put(userId, perms);
    return perms;
}

7.2 权限模型演进

权限系统不是一次性设计完就永远不变的,它需要随着业务发展而演进:

权限模型演进路线图:从硬编码→RBAC→RBAC\+数据权限→混合模型→统一权限中心
权限模型演进路线图:从硬编码→RBAC→RBAC\+数据权限→混合模型→统一权限中心

阶段一:初创期,用户少,权限直接用 if-else 判断。不要嘲笑这种写法——在业务验证期,过度设计比硬编码更可怕。

阶段二:用户增多,角色开始分化,引入 RBAC。这是 90% 的系统的"舒适区"。

阶段三:出现数据隔离需求(如"部门经理只能看本部门数据"),引入数据权限层。

阶段四:动态条件越来越多,角色数量膨胀,引入 ABAC 做细粒度控制。注意:不要一步到位全上 ABAC,复杂度爆炸。RBAC 做主框架,ABAC 做补充是最佳实践。

阶段五:多系统统一权限中心,策略即代码(Policy as Code),权限变更走 CI/CD 审批。

7.3 最佳实践十条

  1. 权限校验必须在后端:前端权限控制只是 UX,不能作为安全措施
  2. 默认拒绝(Deny by Default):没有明确授权的操作一律拒绝
  3. 最小权限原则(Least Privilege):只授予完成任务所需的最小权限
  4. 权限变更走审批:高危权限的授予必须经过审批流程
  5. 权限定期复核:每季度/每半年复核一次用户权限,清理僵尸权限
  6. SoD 互斥检查:关键操作(如资金审批)必须强制互斥角色
  7. 权限变更即审计:每次权限变更自动记录审计日志,不可删除
  8. 鉴权与业务解耦:权限逻辑独立于业务逻辑,通过 AOP/拦截器/网关统一处理
  9. 权限可观测:核心鉴权点要有 Metrics(QPS、延迟、拒绝率),接入监控告警
  10. 权限要有逃生通道:紧急情况下(如误删所有管理员权限),保留超级管理员的后门

权限系统整体架构图:展示认证层、鉴权层、数据权限层、审计层、缓存层的关系和交互
权限系统整体架构图:展示认证层、鉴权层、数据权限层、审计层、缓存层的关系和交互


八、面试官提问

以下问题从面试官视角出发,考察对权限系统设计的深度理解和工程判断力。建议在阅读完正文后,尝试在 30 秒内组织出每个问题的回答要点。

Q1:你设计了一个 RBAC 系统,产品经理要求"张三可以查看自己部门的订单,但只能在工作时间修改金额小于 10000 元的订单"。纯 RBAC 能实现吗?如果不能,你会怎么扩展?

Q2:用户组(Group)和角色(Role)有什么区别?如果用户组可以直接关联权限,会有什么问题?

Q3:你有一个多租户 SaaS 系统,每个租户可以自定义角色。租户 A 的管理员创建了一个名为"超级管理员"的角色并分配了所有权限——但他不应该能管理平台的计费信息。你怎么防止租户越权?

Q4:Redis 里缓存了用户的权限列表,HR 系统同步通知你"张三从部门 A 调到部门 B 了"。但此时张三刷新页面,因为缓存还没过期,他仍然能看到部门 A 的数据。你怎么设计缓存刷新机制?

Q5:你的权限系统上线半年后,发现有一个角色从未被任何用户使用过,另外有 30% 的用户拥有超过实际需要的权限。这是设计问题还是运营问题?你会怎么解决?

Q6:你在设计审计日志表时,before_value 和 after_value 字段应该存什么?如果权限数据量很大(比如一个角色有 500 个权限),直接存完整 JSON 会导致日志膨胀,你怎么权衡?

Q7:你说"权限校验必须在后端",但有一个场景:用户没有"删除按钮"的权限,前端隐藏了按钮,但用户通过 Postman 直接调了删除接口。后端拦截返回 403。这个交互体验怎么优化?是直接返回 403 还是给一个更友好的提示?

Q8:你要设计一个"权限中心"微服务,为公司的 20 个业务系统提供统一鉴权。每个业务系统的权限模型不完全一样(有的用 RBAC,有的有自己的权限表)。你会怎么设计这个统一权限中心的架构?


本文是"Java 技术专家/架构师面试 250 个高频考点"系列的第 209 篇。下一篇:210|社交 Feed 流系统设计。