1. 权限分配这件事,先搞懂 PostgreSQL 的角色体系
做数据库运维这些年,我发现一个特别有意思的现象:很多人对 PostgreSQL 的权限管理第一反应是“这不就是 grant 一下嘛”,可真到了线上环境,经常被各种“没权限”“权限太大”“不知道谁有权限”的问题折腾到怀疑人生。其实问题不在 grant 本身,而在对角色(role)体系的理解上。
PostgreSQL 从 8.1 版本开始用“角色”统一了原来“用户”和“组”的概念。你可以创建带 LOGIN 属性的角色当作登录账号,也可以创建不带 LOGIN 属性的纯角色当作权限容器,再把登录账号丢进这个容器里。这套设计思路和操作系统的用户组模型很像——你不是直接给某个人发一堆零散权限,而是把权限打包成几个“角色模板”,谁需要就把他加进去。好处非常明显:权限像积木一样可以复用,后期调整也方便,不用逐个人去改授权。
举个例子,我前阵子帮某公司梳理一套业务系统的数据库权限,发现他们的做法是:每个开发人员一个账号,直接在账号上 grant select、insert、update、delete,十来个账号挨个授权,看着很直接。但问题是,数据库对象一多、人员一变动,授权语句就得跟着改,漏掉一个表或者多给一个表的权限都毫无感知。后来我把他们的账号全部改成了“业务开发组”“业务只读组”“数据清洗组”这种角色模型,再让账号 inherit 这些角色,事情一下子清爽了。所以第一步一定不是急着写 grant,而是先想清楚你的角色体系怎么设计。
1.1 角色不是账号那么简单
在 PostgreSQL 里,角色(role)既可以是登录账号,也可以是权限组,甚至两者兼得。这个弹性设计是很多人没吃透的第一个点。看两个最基础的区别:
-- 创建一个可以登录数据库的角色(相当于“用户”) CREATE ROLE app_dev WITH LOGIN PASSWORD 'Dev@123456'; -- 创建一个不能登录但可以用来承载权限的角色(相当于“用户组”) CREATE ROLE app_readonly WITH NOLOGIN;这里的关键词是 LOGIN。带 LOGIN 的角色可以像账号一样用密码或证书连上数据库,不带 LOGIN 的角色则只能作为“集合”被其他角色继承。我见过不少新手直接给所有角色都加上 LOGIN,结果安全审计的时候根本分不清哪些是真人、哪些是权限模板——这是个很典型的坏习惯。
还要注意一个容易踩坑的地方:PostgreSQL 里没有“必须属于某个组”的概念,角色的成员关系也用角色来表示。比如把 app_dev 加进 app_readonly 这个组角色,用下面的语句:
GRANT app_readonly TO app_dev;这句的意思是“让 app_dev 成为 app_readonly 的成员”,于是 app_dev 默认就继承了 app_readonly 对象上的权限(前提是 INHERIT 属性没有被关掉)。这套逻辑初看有点绕,但想通之后你就会发现它远比 MySQL 那种“用户+权限表”的方式灵活。
1.2 LOGIN 属性和组角色是权限分配的地基
设计权限体系的时候,我习惯按“人”和“事”先做一次分离:带 LOGIN 的角色只对应具体的人或应用账号,负责“谁”;不带 LOGIN 的组角色只描述权限范围,负责“什么”。这样划分之后,绝大多数权限问题都能在“成员关系”和“对象授权”这两层里找到答案,排查思路清晰很多。
实际项目中一个比较推荐的最小化模型大概是这样的:
-- 1. 组角色:定义“能做什么” CREATE ROLE app_readonly WITH NOLOGIN; CREATE ROLE app_readwrite WITH NOLOGIN; CREATE ROLE app_ddl WITH NOLOGIN; -- 2. 登录角色:定义“谁来做” CREATE ROLE alice WITH LOGIN PASSWORD 'Alice@123'; CREATE ROLE bob WITH LOGIN PASSWORD 'Bob@456'; -- 3. 组角色之间也可以有层级 GRANT app_readonly TO app_readwrite; GRANT app_readwrite TO app_ddl; -- 4. 登录角色加入合适的组角色 GRANT app_readwrite TO alice; GRANT app_readonly TO bob;这样设计之后,alice 拥有读写权限,bob 只能查询。如果哪天 bob 也需要写了,直接GRANT app_readwrite TO bob或者把 bob 调到 app_readwrite 组里,根本不需要去数据库对象上重新 grant 任何东西。这个抽象层就是精细化权限分配的地基,能让后续所有授权行为都变得可预期、可追踪。
1.3 从零创建角色:最基础的语句
很多教程喜欢一上来就噼里啪啦抛一大段授权语句,但对初学者来说,反而最容易困惑的是“这个角色到底能不能登录”“密码是不是必须的”“为什么我创建了角色却查不了表”。这里我把自己日常建角色的几个关键属性整理出来:
| 属性 | 作用 | 说明 |
|---|---|---|
| LOGIN / NOLOGIN | 能否登录 | 真人账号必须 LOGIN,权限组保持 NOLOGIN |
| SUPERUSER | 超级用户 | 极少使用,业务账号一律禁止 |
| CREATEDB | 能否创建数据库 | 开发库可按需开启,生产库建议关闭 |
| CREATEROLE | 能否创建角色 | 同理,生产环境建议关闭 |
| INHERIT | 是否继承组角色权限 | 一般保持默认开启,特殊情况才关闭 |
| REPLICATION | 是否用于流复制 | 只给复制专用账号开 |
| CONNECTION LIMIT | 连接数限制 | 给应用账号设一个合理值,防止连接风暴 |
-- 一个相对标准的业务只读账号 CREATE ROLE read_only_user WITH LOGIN INHERIT NOLOGIN? -- 注意这里要同行写清楚,不要出现冲突 ;写 SQL 时要保持清晰,避免属性冲突。实际上一条典型语句更建议这样写:
CREATE ROLE read_only_user WITH LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS CONNECTION LIMIT 50;其实没有 BYPASSRLS(后面讲行级安全会用到)就是默认状态,所以可以不写。这一条语句创建出来的角色,就是一个除了能登录和继承权限之外什么管理能力都没有的“干净账号”。日常给应用、给同学、给分析师开账号,我都建议先按这个模板来,再按需放开额外属性。
2. 数据库、Schema、表级权限的精细管控
角色体系搭好之后,真正的重头戏是“授权”。PostgreSQL 的权限模型分了多个层级:集群级、数据库级、Schema 级、对象级(表、视图、序列、函数等),每一层都有独立的权限位,而且上下层之间不是天然的“通了就万事大吉”。比如你在数据库级别给了某个角色 CONNECT 权限,他确实能连上来,但连上来之后未必能在 public schema 里建表,更不一定能看见任何表的数据——因为表的权限是另一码事。
刚接触 PostgreSQL 的人经常被这个多级模型搞晕:为什么 grant all on database xxx to yyy 了,他还是啥也干不了?因为数据库级的 ALL 只是连接(CONNECT)、建临时表(TEMPORARY)和创建 Schema(CREATE)这些“容器级”权限,跟表数据一点关系没有。想让他能查业务表,你还得在 Schema 和表上分别动手。这个设计初看繁琐,可一旦你习惯它,反而会觉得可控性极强——你能精确到“某人只能查某几个表”“某账号只能在某个 Schema 里写数据”,这恰恰是精细化权限分配的核心价值。
2.1 权限层级关系,先画清脉络
我习惯用一句话概括 PostgreSQL 的授权逻辑:数据库管“你能不能进来”,Schema 管“你能不能在这个空间里折腾”,表和序列管“你能不能碰具体数据”,函数和触发器管“你能不能执行这段逻辑”。
所以把一个账号从“能登录”变成“能用业务数据”,至少要走四步:
-- 第一步:允许连接数据库 GRANT CONNECT ON DATABASE mydb TO app_readwrite; -- 第二步:允许使用 Schema(注意,不是 CREATE,而是 USAGE) GRANT USAGE ON SCHEMA public TO app_readwrite; -- 第三步:允许读写表中的数据 GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_readwrite; -- 第四步:允许使用序列(否则 insert 时自增主键会报错) GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_readwrite;少走任何一步,都会在运行时冒出莫名其妙的报错。尤其是序列权限,很多从 MySQL 迁移过来的朋友经常忽略:MySQL 的自增列不需要单独授权,但 PostgreSQL 里序列是一个独立对象,没有 USAGE 权限,INSERT 语句一旦触发 nextval(),直接报permission denied for sequence。这个问题出现频率极高,后面我还专门讲。
从 PostgreSQL 15 开始,public schema 默认权限收紧了不少(不再默认允许所有人建对象),所以很多时候你还要主动 CREATE SCHEMA 并授权。环境干净的话最好按业务建独立 schema,而不是扎堆用 public——这个习惯越早养成越好。
2.2 常用授权语句和场景库
我把自己日常用的授权语句整理成了一个“场景库”,遇到类似需求直接抄,再按实际情况改改对象名和角色名,效率很高:
| 场景 | 核心语句 |
|---|---|
| 只读查询某库所有现存表 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_role; |
| 只读查询未来新建的表 | ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO read_role; |
| 可增删改所有现存表 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO write_role; |
| 允许使用序列 | GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO write_role; |
| 允许执行函数 | GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO write_role; |
| 允许建表、改表结构 | GRANT CREATE ON SCHEMA public TO ddl_role; |
| 查看某角色具体权限 | \dp或查询information_schema.role_table_grants |
刚开始做权限分配的人,最容易把GRANT USAGE ON SCHEMA和GRANT CREATE ON SCHEMA搞混:USAGE 只是允许你在 Schema 里“使用”已有对象,CREATE 才是允许你“新建”对象。一个只读账号绝对不需要 CREATE,给多了就容易出现“能查数据但也能建表”的怪象。反过来,一个 DDL 账号如果连 USAGE 都没给,建表也会失败。这两个权限通常要配合使用,缺一不可。
2.3 DEFAULT PRIVILEGES,解决“新表权限自动继承”的痛点
只对现有表授权远远不够,因为数据库里每天都会产生新表、新视图、新序列。如果每次业务建张新表都要手动 grant 一次,运维就得累死。PostgreSQL 提供了默认权限机制,可以预先定义“谁在哪个 Schema 里新建对象时自动获得什么权限”。
-- 让 read_role 以后能自动读所有在 public schema 里新建的表 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO read_role; -- 让 write_role 以后能自动读写所有新建的表、序列 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO write_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO write_role;这里有个非常关键的注意点:ALTER DEFAULT PRIVILEGES只对“执行该语句的角色自己后续创建的对象”生效。什么意思呢?如果你是超级用户或其他普通角色执行这条语句,那么只有“以执行者身份创建的表”才会自动带出这些权限;如果是普通开发账号自己 connect 上去建表,规则根本不会触发。所以要真正实现“不管谁建表,读写角色都自动有权限”,通常需要用超级用户建一张基础表并设置好默认权限,或者让所有建表操作都通过某个固定账号完成。我在实际项目里更推荐用 migration 工具统一执行 DDL,配合超级用户设置全局默认权限,这样规则才稳定。
另外还要记得,ALTER DEFAULT PRIVILEGES不止能管 TABLES,还可以管 SEQUENCES、FUNCTIONS、TYPES、SCHEMAS。最常见的遗漏就是把表和序列都配置了,结果忘了函数,等报表那边调用函数时报permission denied for function,排查半天才发现是默认权限没覆盖。
3. 行级安全(RLS),权限细粒度到“行”这一层
数据库级别的权限再精细化,也只能控制到“某张表能不能读、能不能写”。但真实业务里经常有更细的要求:同一个表,不同人只能看到不同行。比如订单表,客服A只能看他负责的客户订单;再比如多租户场景,租户1只能读租户1的数据。这种需求用传统 grant 完全做不了,得靠 PostgreSQL 的行级安全(Row-Level Security,RLS)机制。
我第一次接触 RLS 是在一个多租户系统改造项目里。当时我们的做法是给每个租户建一套独立表,数据隔离效果是有了,但表数量爆炸、跨租户统计非常痛苦,连个“汇总所有租户”的报表都要写一堆 UNION。后来切换到单表+RLS,每个租户同一张表,通过当前用户绑定的租户 ID 自动过滤数据,逻辑干净太多,备份、扩容、统计也全部回归到常规操作。
3.1 什么时候需要 RLS,什么时候不需要
RLS 虽然好用,但不是万金油,不要动不动就开。我的判断标准是这样的:
- 多租户 SaaS 系统:必须开,特别是租户数量多、表结构相同的场景。RLS 是比“多个数据库实例”轻量得多、比“每租户独立 schema”可控得多的方案。
- 内部人员按部门或角色看不同数据:可以考虑开,但要先评估行过滤策略是否稳定、策略数量是否可控。
- 纯粹的“这张表只有某几个人能碰”:不需要 RLS,用传统 grant 就足够了。RLS 解决的是“同一个人能访问同一张表但只能看部分行”的问题,不是“谁能访问表”的问题。
换句话说,授权管的是“能不能进这扇门”,RLS 管的是“进这扇门之后你能看到屋里哪几个抽屉”。两者可以叠加使用,先有 table 级权限,再叠加行级策略,安全性会高很多。
3.2 开启 RLS 的具体步骤
以一个简单的多租户订单表为例,完整流程如下:
-- 1. 创建租户表 CREATE TABLE tenant_users ( id SERIAL PRIMARY KEY, tenant_id INT NOT NULL, username TEXT NOT NULL ); -- 2. 开启行级安全 ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY; -- 3. 创建策略:管理员可以看到所有行 CREATE POLICY tenant_users_admin_all ON tenant_users FOR ALL TO admin_role USING (true); -- 4. 创建策略:普通用户只能看到自己租户的数据 CREATE POLICY tenant_users_user_tenant ON tenant_users FOR SELECT TO app_user USING (tenant_id = current_setting('app.current_tenant_id')::int);current_setting('app.current_tenant_id')是我自己比较喜欢的一种做法:应用在建立数据库连接之后、正式执行 SQL 之前,先通过SET app.current_tenant_id = 233;声明当前登录用户的租户 ID,然后 RLS 策略自动按这个值过滤。这样连程序代码里都不需要硬编码 WHERE tenant_id = ?,而是把“当前租户是谁”交给数据库连接上下文来判断,过滤逻辑天然统一。
不过要注意,current_setting()是会话级别的,应用连接池复用时非常容易串。解决方式是建议在每次从连接池拿到连接后、提交业务 SQL 前,都强制重新 SET 一下;或者更稳妥一点,直接使用权限用户自带的SESSION_USER关联到租户映射表里判断。我见过不少线上事故是“用户A查到了用户B的数据”,排查下来就是连接池复用导致租户上下文串位了,这一点必须写在项目规范里。
RLS 还有一个隐藏脾气:它对超级用户默认不生效。原因很简单,超级用户拥有 BYPASSRLS 属性,可以直接绕过所有行级策略。这既是好事(管理员永远能救火),也是坏事(如果你依赖 RLS 做强制隔离,可千万别把应用账号搞成超级用户)。创建角色的时候一定要检查 BYPASSRLS 属性,默认是 NOBYPASSRLS,但CREATE ROLE xxx SUPERUSER这种操作一旦手滑,RLS 就白做了。
3.3 RLS 和视图方案怎么选
在没有 RLS 之前,很多项目用“视图 + WHERE 条件”来做数据隔离。比如建一个view_my_orders,底层WHERE owner_id = current_user_id(),让业务账号只能查这个视图。这种做法的问题是:代码里一旦有人直接查基表,隔离就失效了;而且视图方案很难覆盖 INSERT、UPDATE、DELETE 的自动过滤——你得为每种操作分别写触发器或规则,复杂度指数上升。
RLS 则把过滤逻辑下沉到表本身,无论应用通过什么 SQL 访问这张表,只要角色匹配了策略,都会强制带上行过滤条件。这相当于给表加了一层“物理隔离层”,比视图方案严密得多。我个人建议:新项目、改造项目优先考虑 RLS;老项目如果已经重度依赖视图,可以先保留视图,再把 RLS 叠加在基表上作为兜底,双保险。不过也别过度设计,如果只有一个角色访问某张表,开 RLS 纯属浪费精力。
4. 几个典型场景下的权限方案实操
理论说多了容易飘,我直接拿三类最常见的真实场景走一遍完整配置:开发库怎么放权、生产库怎么收权、多租户环境怎么隔离。这三个场景覆盖了“放开”“收紧”“隔离”三种诉求,基本就是权限分配的核心形态。
4.1 开发库:让开发人员放开手脚,但别失控
开发环境的核心矛盾是“效率优先”和“事后可控”。你不能让开发同学连建表都要找 DBA 审批,那开发节奏直接就废了;但也不能让他们拿着超级用户到处跑,不然有人手滑DROP DATABASE就是全体事故。我的习惯是给开发同学一个 NOLOGIN 的组角色dev_ddl_role,具备 Schema 的 CREATE、对象的 ALL PRIVILEGES,然后关闭数据库级 DROP 权限(其实非属主本来也删不掉别人的东西)。语句大概长这样:
CREATE ROLE dev_ddl_role WITH NOLOGIN; GRANT CONNECT ON DATABASE devdb TO dev_ddl_role; GRANT CREATE, USAGE ON SCHEMA public TO dev_ddl_role; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dev_ddl_role; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO dev_ddl_role; -- 把开发同学加入进来 GRANT dev_ddl_role TO zhangsan, lisi, wangwu;这里的关键点是:这些开发账号本身不要直接设为 SUPERUSER。有dev_ddl_role能建表改表就够了,万一哪次误操作把表删了,还能通过“非属主被拒”的机制保护一下。另外一个开发库特有的建议:给开发账号设置CONNECTION LIMIT,比如每个账号限制 10 个连接,防止本地调试连接泄漏把数据库连接池打爆。
4.2 生产库:只读账号的分配,要覆盖“现在”和“以后”
生产环境的只读账号需求非常常见,BI 报表、数据分析师、临时排查问题都会用到。一个标准的只读账号,至少包含以下配置:
-- 创建只读组角色 CREATE ROLE prod_readonly WITH NOLOGIN; -- 允许连接业务库 GRANT CONNECT ON DATABASE prod_db TO prod_readonly; -- 允许使用相关 Schema GRANT USAGE ON SCHEMA public TO prod_readonly; -- 现存表全部只读 GRANT SELECT ON ALL TABLES IN SCHEMA public TO prod_readonly; -- 未来新表也自动只读 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO prod_readonly; -- 序列只读(部分工具读取序列值需要) GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO prod_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO prod_readonly;有两点很多人容易漏:一是函数,数据分析师如果调用一些辅助函数跑统计,还要给GRANT EXECUTE;二是物化视图,PostgreSQL 里物化视图刷新需要 REFRESH 权限,如果你的只读账号跑报表时刷新物化视图,得单独给权限。安全起见,生产库我一般把 EXECUTE 默认权限也顺手给上,但要先确认函数里没有写操作。只读账号不等于“什么都能看”,如果某些表是敏感字段(比如用户手机号),建议在表级别把权限拿掉,或者用列级别的授权处理:
-- 只给查看非敏感列的权限 GRANT SELECT (order_id, created_at, amount) ON orders TO prod_readonly;列级授权是精细化权限里很有用但很少人用的功能。它的局限是:如果表里加了一列,默认别人是看不到的,需要重新授权。所以一般只用于个别敏感表,不要全部表都这样搞。
4.3 多租户环境的动态隔离:一套账号模板走天下
多租户系统的权限诉求,最理想是做到“同一套代码、同一份表结构、不同租户自动看到不同数据”。刚才讲 RLS 时已经给过基础配置,这里再补充一个基于租户标识的完整推荐配置:
-- 1. 新建租户账号(每个租户一个登录角色) CREATE ROLE tenant_233 WITH LOGIN PASSWORD 'Tenant233@Pass'; -- 2. 加到统一的应用角色里 GRANT app_readwrite TO tenant_233; -- 3. 关联租户上下文 COMMENT ON ROLE tenant_233 IS '租户ID: 233';应用层在建立连接后先执行SET app.tenant_id = 233,RLS 策略读取这个变量并过滤数据。这里有一个更稳妥的做法:把app.tenant_id的赋值逻辑封装成语义明确的函数,并且用SET LOCAL放在事务里,避免事务外残留上下文。直接SET是会话级的,事务回滚后仍然存在,容易带来脏上下文;SET LOCAL则随事务结束自动清掉,我强烈建议用SET LOCAL。
还要注意不同租户的密码策略、连接数限制可以差异化。比如大租户给 100 连接数上限,小租户给 20,防止某个租户的突发流量挤占整个数据库的连接池。这个放在运维规范里属于“配额管理”,配合角色体系落地非常方便。
5. 实战中常见的权限问题排查与避坑
权限问题有一个特点:报错信息往往只告诉你“没权限”,但没告诉你“缺哪一层权限”。这就是排查的难点。我把自己在实际工作中高频率遇到的几类问题整理出来,并给出诊断思路,希望你能少走弯路。
5.1 授权后还是没权限,先检查默认权限和属主
最经典的场景:你刚给某账号GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_role,对方兴冲冲跑过来查询,结果依然报permission denied for table orders。这种时候我建议按顺序排查三件事:
第一,目标表在不在 public schema 里?如果业务表在别的 schema,比如ods、dw,那上面的语句只覆盖 public,其他 schema 的表当然没权限。第二,目标表是不是视图或物化视图?GRANT SELECT ON ALL TABLES对视图同样生效,但物化视图有时需要单独处理。第三,最关键的一点——执行授权语句的角色是否真的是表属主?PostgreSQL 规定,只有表属主(或超级用户)才有权利对这张表授权。如果你用开发账号执行 grant,而那张表是业务系统通过另一个账号创建的,grant 命令本身就会报“必须是表属主”,或者静默失败。
这类问题最隐蔽的形态是:一个组角色通过ALTER DEFAULT PRIVILEGES设置了自动授权,但因为执行者是某个普通账号,结果只有他建的表自动授权成功,其他账号建的表全部不生效。排查方法很简单:\ddp查看默认权限的设置归属,或者直接查pg_default_acl视图。
5.2 序列权限问题,插入数据时的“隐形杀手”
PostgreSQL 中插入一条记录时,如果表的主键是SERIAL或IDENTITY,底层会调用序列的nextval()。如果角色没有序列的 USAGE 权限,INSERT 语句就会直接失败,而且报错信息往往让人一头雾水:
ERROR: permission denied for sequence orders_id_seq这个问题的坑点在于:授权语句里如果只写了GRANT ... ON ALL TABLES,没有写ON ALL SEQUENCES,那序列权限就永远缺失。序列是独立对象,不会跟着表权限自动走。我给新同学搭环境时,几乎每个新人都会在自增主键上踩一次这个坑。
解决方案是使用GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO role;同时配合ALTER DEFAULT PRIVILEGES ... ON SEQUENCES保证后续新增序列也能自动授权。如果是不小心遗漏的存量序列,可以用一条 DO 块批量处理,不需要一个个手动授权。
DO $$ DECLARE seq_name TEXT; BEGIN FOR seq_name IN SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'S' AND n.nspname = 'public' LOOP EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE public.%I TO app_readwrite', seq_name); END LOOP; END $$;5.3 权限给了但收不回来,用 REVOKE 的正确姿势
回收权限比授权更讲究。PostgreSQL 的权限模型继承机制会导致一个问题:如果某个账号继承了一个组角色,组角色的权限被 REVOKE,正常情况下账号也就失去权限了。但如果你在账号身上也单独授过同样的权限,那么 REVOKE 组权限并不会影响账号自己的权限。这个“双重路径”很容易让人产生“我怎么 revoke 不掉他”的困惑。
-- 先回收组角色本身的权限 REVOKE SELECT ON ALL TABLES IN SCHEMA public FROM app_readonly; -- 再把成员关系分离 REVOKE app_readonly FROM alice; -- 最后检查成员是否还存在别的授权路径我一般建议用information_schema.role_table_grants或者 psql 的\dp把某个角色的所有权限列出来,再决定从哪一层回收。还有一个细节:如果权限来自 PUBLIC 这个特殊角色,普通 REVOKE 语句可能不够,还要REVOKE ... FROM PUBLIC。PostgreSQL 里 PUBLIC 表示“任意角色”,有些默认权限(比如在 public schema 上建表的权限,PostgreSQL 15 之前)就是给 PUBLIC 的,很多人没意识到这一点,导致回收失败。
5.4 排查权限问诊表:一条命令看到底
如果你不想一条条去猜,我推荐直接查视图。下面这几个查询几乎覆盖了我 90% 的权限排查需求:
-- 查看某角色在哪些表上有哪些权限 SELECT grantee, table_schema, table_name, privilege_type FROM information_schema.role_table_grants WHERE grantee = 'app_readonly' ORDER BY table_schema, table_name; -- 查看某个角色的成员关系 SELECT r.rolname AS role_name, m.rolname AS member_name FROM pg_auth_members am JOIN pg_roles r ON r.oid = am.roleid JOIN pg_roles m ON m.oid = am.member; -- 查看默认权限配置 SELECT * FROM pg_default_acl;实际排障时,这三个查询组合起来,通常十分钟之内就能定位权限问题。重点是不要只盯着 grantee 那一层,还要看角色继承链。PostgreSQL 的角色继承可能有多层嵌套,A 继承 B,B 继承 C,那么 A 的权限其实是 B 和 C 的并集。如果权限“多出来”了,顺着继承链往上查一定会找到源头。
6. 几个安全建议,以及我个人的实操心得
前几章讲的是具体操作,最后这部分聊一些相对务虚但对长期维护非常重要的事:权限分配之后怎么办?怎么保证权限体系不腐化?这里分享几人我自己的心法和踩坑总结。
6.1 最小权限原则怎么落地才不折腾人
最小权限原则人人都知道,但落地的时候经常在两个方向上翻车:要么权限给得太宽,要么权限收得太紧导致无法干活。我的平衡策略是“按角色分层 + 动态调整周期”:先把人分成几个固定的角色模板(开发、只读、运维、报表),每个模板只包含完成本职工作的最小权限,然后每周或者每个迭代看一次角色成员列表,有新增人员按模板加,有离职人员立刻移除。千万别搞“临时授权”这种操作——没有记录的临时权限,三个月后就是安全漏洞。
具体到 PostgreSQL 上,我还有一个习惯:生产环境把CREATE权限从开发账号上彻底拿走,建表、加字段、改索引全部通过规范的变更流程执行。这样即使开发账号泄露了,攻击者也没法往里塞数据或篡改结构。当然这样会增加协作成本,所以一般大中型团队才这么干,小团队如果嫌麻烦,至少要把DROP权限严格控住。
6.2 怎么定期审查权限,避免权限越滚越大
权限体系的维护是一个永续过程,过半年不看,一定会有人权限比你预期的大。主要原因无外乎三种:老员工调岗但是权限没回收、某个需求临时开了大权限后来忘了收、数据表 Schema 发生变化导致默认权限规则意外扩大范围。
我个人的做法是每季度做一次权限审计,脚本化处理。核心检查项包括:
- 所有带 LOGIN 的角色清单,确认每个角色都有明确的负责人和用途。
- 超级用户(SUPERUSER)清单,必须极少,最好只有运维账号。
- 每个角色不直接关联对象权限,而是全部通过组角色间接继承(直接授权容易失控)。
- 默认权限(pg_default_acl)里每一行都解释得清用途,解释不清的立刻清理。
- 数据目录里有没有超过 90 天未登录的账号,有就考虑禁用。
还有一个细节:审计时不要只看\du的输出,要用pg_roles、pg_auth_members、pg_default_acl、information_schema.role_table_grants多表联查,才能看清权限的真实面貌。因为\du只显示角色属性,不显示对象权限。
6.3 我个人的实操总结:一开始就按“角色模板”设计,后期能救你无数次
最后说点实在的。我见过太多项目,上线第一年权限都靠 DBA 手工一条条 grant,表面上也跑得挺好;等第二年人员一多、数据库对象一多,各种权限问题开始集中爆发,领导层才开始追着“精细化权限管理”的指标改。每次做这种翻新,都要花比一开始设计模板多好几倍的精力。
所以我的核心建议很简单:第一周就把角色体系定下来,哪怕只建三个角色——只读、读写、DDL。先跑起来,后面再按需细化。不要把临时方案当成长期方案,不要因为“现在人少,直接给账号 grant 得了”而放弃角色抽象层。角色体系这东西,前期多点几个名字、多写十几条 grant,后期能帮你省下无数个排查权限问题的深夜。
另外,授权脚本一定要纳入版本管理。我自己的习惯是维护一个permissions.sql,每次权限变更都在里面写清楚变更时间和原因,然后用 migration 方式执行。权限脚本被记录在案之后,万一出问题,你能回溯到任何一个时间点的权限快照,这个价值在安全审计时尤为明显。
还有一个容易被忽略的经验:给“应用账号”和“人账号”分开设计角色。应用账号的权限应该非常稳定,基本不受人员流动影响;人账号按成员加组或移组就行。千万不要让应用账号和人账号共用同一个登录角色,否则人走了账号也要跟着改,或者应用把人的权限全继承下去,风险极大。这个点在刚开始设计时就要根植在团队规范里。
如果你现在正在做 PostgreSQL 权限规划,我的建议是从一个小项目开始试,把角色模板、默认权限、RLS 这三样东西一点点加上去,不要一开始就追求“完美方案”。跑上两三个迭代之后,你对这套体系的体会会比任何文档都深。后面如果碰到具体的报错或者拿不准授权范围,欢迎顺着性能监控的思路去查系统表,PostgreSQL 的权限问题几乎都有迹可循,不会像某些黑盒产品那样让你无从下手。