孙亮亮
返回 小企业信息化系统开发

小企业信息化系统开发

小企业自建系统的权限设计实战:从“谁都能改”到 RBAC 与数据范围隔离

2026年8月23日

五张表落地 RBAC,讲清功能权限、数据范围与字段脱敏。

小企业自建系统的权限设计实战:从"谁都能改"到 RBAC 与数据范围隔离

小企业自研的业务系统几乎都有同一段黑历史:上线时人少图省事,所有人共用一个账号,或者用 is_admin 一个布尔字段区分管理员。跑上一两年,销售能看到全公司客户、仓管能改已审核的订单、离职员工账号还留着,出问题查不出是谁改的。

这篇文章讲的就是补救过程:不推翻重做,在已有系统上把权限一层层补齐。表设计基于几套真实的进销存和工单系统改造经验,可直接照搬。

先分清三种权限,别混着做

功能权限回答"能不能进这个页面、点这个按钮"。数据权限回答"同一功能里能看到哪些行",比如销售 A 只能看自己的客户,区域经理看本区域全部。字段权限回答"同一条记录里哪些列可见可改",比如普通员工看不到成本价。

三者难度递增,按顺序推进:功能权限先上,数据权限紧跟,字段权限只对少数敏感列做。一次性全做工期会失控。

表设计:五张表撑起 RBAC

RBAC 的本质是在用户和权限之间插一层角色,避免权限直接挂人。最小可用结构就五张表:

-- 权限点:细到"操作"级别
CREATE TABLE perm (
  code   VARCHAR(64) PRIMARY KEY,   -- 如 order:view / order:audit
  name   VARCHAR(64) NOT NULL,
  module VARCHAR(32) NOT NULL
);

-- 角色
CREATE TABLE role (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(32) NOT NULL UNIQUE,
  data_scope TINYINT NOT NULL DEFAULT 1, -- 1本人 2本部门 3本部门及下级 4全部
  remark     VARCHAR(128)
);

CREATE TABLE role_perm (
  role_id   INT NOT NULL,
  perm_code VARCHAR(64) NOT NULL,
  PRIMARY KEY (role_id, perm_code)
);

CREATE TABLE user_role (
  user_id INT NOT NULL,
  role_id INT NOT NULL,
  PRIMARY KEY (user_id, role_id)
);

-- 用户表补两个字段
ALTER TABLE user ADD dept_id INT NULL,
                 ADD status TINYINT NOT NULL DEFAULT 1; -- 1在职 0禁用

几个设计取舍值得说明。

权限点命名用 模块:操作 的形式,比数字 ID 可读性高得多,排查问题时看日志就懂。data_scope 直接挂在角色上而不是单独建表,是因为小企业的数据范围规则简单,四个档位覆盖九成场景,真有特例再加用户级覆盖字段。用户禁用要用 status 而不是删记录,历史单据上的经办人还要能查到名字。

校验放在哪一层:只信服务端

前端隐藏按钮只是体验优化,不是安全措施。有人把权限判断写在页面里,接口裸奔,稍懂浏览器的人改一下请求就能越权。

正确做法是接口层统一拦截。以常见的中间件写法为例:

def require(perm_code):
    def deco(fn):
        def wrapper(req, *a, **kw):
            if perm_code not in current_perms(req.user_id):
                return json_error(403, "无权限")
            return fn(req, *a, **kw)
        return wrapper
    return deco

@require("order:audit")
def audit_order(req):
    ...

用户权限集合每次查库会拖慢接口,缓存到会话或 Redis,有效期十分钟,配合"改完角色主动清缓存"。这里有个必须注意的点:改了权限要能立刻生效,否则调整完员工权限还要等半小时,运维会被投诉。

数据范围隔离:在 SQL 上动手

数据权限的落地方式是给查询自动追加条件,而不是查出来再在代码里过滤。后者在数据量上来后会直接拖垮性能,还容易在分页时算错总数。

def scope_sql(user, alias="o"):
    if user.scope == 4:  return "1=1"
    if user.scope == 1:  return f"{alias}.owner_id = {user.id}"
    if user.scope == 2:  return f"{alias}.dept_id = {user.dept_id}"
    if user.scope == 3:
        ids = ",".join(map(str, dept_and_children(user.dept_id)))
        return f"{alias}.dept_id IN ({ids})"
    return "1=0"   # 兜底:未知范围一律查不到

sql = f"SELECT * FROM orders o WHERE {scope_sql(user)} AND o.status=1"

三个实践要点。业务表必须有 owner_iddept_id 两个冗余字段,否则范围过滤无从下手,老表补字段时要写脚本回填历史数据。兜底分支返回 1=0 而不是 1=1,配置出错时是"查不到"而非"全泄露",这个方向不能反。部门树查询要缓存,别每次请求都递归查数据库。

字段权限:低成本做法

不要为字段权限建复杂配置表。小企业的敏感字段通常就那么几个:成本价、进货价、员工薪资、客户手机号。做法是在返回数据前统一脱敏:

MASK = {"cost_price": "cost:view", "phone": "customer:phone"}

def mask(row, perms):
    for field, need in MASK.items():
        if field in row and need not in perms:
            row[field] = None if field.endswith("price") else mask_phone(row[field])
    return row

导出功能要单独做权限点。见过太多案例是列表页把成本价藏了,但"导出 Excel"接口忘了处理,数据整表流出。凡是导出、打印、批量接口,都要按同样规则再过一遍。

审计日志:权限的最后一道保险

权限管的是"不该做的做不了",审计管的是"做过的能查到"。小企业系统只需记录写操作即可,读操作量太大不划算。

CREATE TABLE audit_log (
  id        BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id   INT NOT NULL,
  action    VARCHAR(64) NOT NULL,   -- order:audit
  target_id VARCHAR(64),
  before_js JSON, after_js JSON,    -- 只存变化字段
  ip        VARCHAR(45),
  created_at DATETIME NOT NULL,
  INDEX idx_target (action, target_id),
  INDEX idx_user_time (user_id, created_at)
);

只存变化的字段而不是整条记录,否则表膨胀极快。保留期设六到十二个月,配合按月分表或定期归档。审计表要禁止业务代码删除,只允许追加。

迁移策略:老系统怎么平滑加权限

硬切一定会出事故。可行的四步节奏:先梳理,把现有页面和接口列一张表,标注每个操作应属哪个权限点,让业务负责人确认;再让校验跑"观察模式",不拦截只记日志,把"某用户触发了未授权的权限点"跑一周收集漏配;然后按岗位职责批量授权,通常三到五个角色够用;最后才开启真实拦截,同时留一个应急超管账号。

数据范围放在功能权限稳定后再开,两件事同时上线会分不清问题出在哪一层。

常见坑

  • 角色越建越多:出现"张三专用角色"就说明设计跑偏了,特例应该用用户级权限补充,而不是造新角色。
  • 只做增删改不管查:查询接口的数据范围最容易被漏掉,尤其是统计和报表接口。
  • 超管绕过一切校验:可以绕过权限,但不能绕过审计,超管的操作更要留痕。
  • 离职处理不完整:应禁用账号并解绑角色,历史单据保留姓名,不要物理删除用户。

小结

小企业系统的权限不需要企业级复杂度,但必须分层清晰:功能权限拦操作、数据范围过行、字段脱敏挡列、审计日志留痕。五张表加一个中间件,两三天就能补上,换来的是数据不再裸奔、责任可以追溯。越早做成本越低——等出了纠纷再回头查记录,发现什么都没记,那就真没办法了。

本模块其他文章

小企业自建系统的权限设计实战:从“谁都能改”到 RBAC 与数据范围隔离 · 孙亮亮