如果管理过任何一套正经的 PostgreSQL,你大概率遇到过两种经典场面:一是新同事在测试库上死活查不着一张表,你在工位上一看就知道是权限没给到位;二是上线前夜,有人来问某个账号为啥能碰生产库的数据。两个问题指向同一个根源——账号与权限体系没有理顺。PostgreSQL 的用户与权限管理在圈内以"灵活"著称,但灵活的背面就是复杂。它不像 MySQL 那样有 user@host 这种相对直观的授权模型,而是把用户、角色、成员关系、对象权限、默认权限、行级安全叠在一起,新手第一次接触很容易绕晕。
这篇是 PostgreSQL 16 系列里专门讲用户与权限管理的一篇,内容包括基础概念、语法拆解、实战案例,以及我从 PG15 升到 PG16 之后在权限方面踩过的坑。适合刚接触 PostgreSQL 的同学建立完整认知,也适合有一定经验但平时主要写业务 SQL、很少系统梳理权限体系的开发同学查漏补缺。我会尽量把每个知识点都写清楚"为什么这样做",而不只是给一段能跑的命令。
1. 权限体系概览:先搞清角色、用户和授权的关系
很多人第一次被 PostgreSQL 绕住,就是因为它没有一个独立的"用户"概念。在 MySQL 里,用户就是登录账号,和库、表是分开的;在 PostgreSQL 里,用户和角色本质上是一个东西——用户就是带有 LOGIN 属性的角色。这个设计在早期看起来反直觉,但真正理解之后会发现它非常强大,因为你可以把一个角色当作"权限集合"来用,再把这个集合交给别的角色,实现权限的批量管理。
1.1 "用户"和"角色"在 PostgreSQL 里是同一个实体
先记住一句话:PostgreSQL 中的 ROLE 既可以是一个人(登录账号),也可以是一个组(权限集合)。系统表 pg_authid 里存的就是所有角色,你在 psql 里执行\du看到的列表就是它们。
创建的时候,CREATE ROLE和CREATE USER几乎等价,唯一的区别是CREATE USER会默认带上 LOGIN 属性,而CREATE ROLE默认不带。也就是说:
CREATE USER alice; -- 等价于 CREATE ROLE alice WITH LOGIN;所以后面讲角色属性时,你不用把"用户"和"角色"当成两个东西,只需要区分:某个角色是不是允许登录(LOGIN),以及它被授予了哪些权限。
1.2 授权的层级:集群、数据库、Schema、对象
PostgreSQL 的权限不是一条 GRANT 走天下,而是分层的。从大到小大致是:
- 集群级:能否连接、能否创建数据库、是否超级用户等,由角色属性控制。
- 数据库级:CONNECT、CREATE、TEMPORARY。
- Schema 级:USAGE(能不能用这个 schema 里的对象)、CREATE(能不能在里面建对象)。
- 对象级:表、视图、序列、函数等各自有各自的权限集合。
这个层级最容易踩的坑是:很多人只给用户授了表的 SELECT,却忘记授 Schema 的 USAGE,结果查询时报permission denied for schema public。权限检查是从外层往内层走的,外层的门没开,里层的钥匙再多也没用。
1.3 拆解一条 GRANT 语句
先看一条最简单的授权:
GRANT SELECT ON TABLE public.orders TO app_rw;这条语句拆开来看,包含四个要素:GRANT 关键字、权限类型(SELECT)、对象(public.orders)、被授权者(app_rw)。如果再加上 WITH GRANT OPTION,就意味着"这个用户拿到权限后,还可以把这个权限转授给别人"。别小看这个选项,很多权限失控就出在它上面——被授权者可以把自己拥有的权限继续扩散,而回收时往往需要级联处理。
到这里,权限体系的大框架就清楚了。接下来我们从账号管理的生命周期开始,把每一步操作都过一遍。
2. 账号管理实操:从创建角色到安全删除
用户和权限管理的第一课,永远是"账号怎么建、怎么改、怎么删"。
2.1 创建角色:CREATE ROLE 的完整姿势
创建一个用于业务系统的登录账号,最少要指定密码和登录权限:
CREATE ROLE app_rw WITH LOGIN PASSWORD 'strong_password_here';如果你不想让这个角色能登录,就建一个"组角色"用来收拢权限:
CREATE ROLE app_readonly_role;组角色不设 LOGIN,不接业务,只作为权限的载体。然后你把具体用户挂到这个组下面:
GRANT app_readonly_role TO alice;这样 alice 就自动继承了 app_readonly_role 拥有的所有权限。以后想调整一批账号的权限,只需要改组角色的授权关系,不需要逐个账号 GRANT。
2.2 角色属性逐个说清楚
除了 LOGIN 和 PASSWORD,角色还有很多属性,我列一个常用表:
| 属性 | 含义 | 使用建议 |
|---|---|---|
| SUPERUSER | 超级用户,绕过一切权限检查 | 生产环境不要给业务账号 |
| CREATEDB | 允许创建数据库 | 只给 DBA 类角色 |
| CREATEROLE | 允许创建和管理角色 | 只给 DBA 类角色 |
| REPLICATION | 复制账号专用 | 只在搭建流复制时使用 |
| BYPASSRLS | 绕过行级安全策略 | 谨慎授予 |
| CONNECTION LIMIT | 限制并发连接数 | 业务账号常用,可防连接风暴 |
| VALID UNTIL | 密码有效期 | 临时账号必备 |
| INHERIT / NOINHERIT | 是否继承成员角色的权限 | 默认 INHERIT,一般不用改 |
举个例子,创建一个有效期到今年年底、并发连接上限 50 的临时分析账号:
CREATE ROLE analyst_bob WITH LOGIN PASSWORD 'xxx' CONNECTION LIMIT 50 VALID UNTIL '2025-12-31';这里有两个细节值得注意。第一,VALID UNTIL到期后账号不是被删除,而是无法登录,数据都还在,适合做短期外包或临时合作场景。第二,CONNECTION LIMIT限制的是这个角色同时建立的连接数,不是所有用户加起来的总连接数,理解这一点就不会把参数设得太激进而影响正常业务。
2.3 修改角色:ALTER ROLE 的典型场景
账号建好之后免不了要调整。最常见的几个操作:
-- 修改密码 ALTER ROLE app_rw WITH PASSWORD 'new_password'; -- 临时禁用登录(保留账号和数据) ALTER ROLE app_rw WITH NOLOGIN; -- 重新启用登录 ALTER ROLE app_rw WITH LOGIN; -- 修改连接数上限 ALTER ROLE app_rw WITH CONNECTION LIMIT 100;我的经验是,遇到"某个账号被怀疑泄露或需要暂停使用"的情况,先不要急着 DROP,改为NOLOGIN是更稳妥的第一步。原因有两点:DRO P 之后如果发现误判,重建账号还要重新配权限;而NOLOGIN撤销走 GRANT 即可,影响面小得多。
还有一类需求是修改角色在某个数据库里的默认配置,比如要限制某个角色只在指定库里生效:
ALTER ROLE app_rw IN DATABASE appdb SET statement_timeout = '30s';这样 app_rw 在其他数据库里不受影响,只在 appdb 里执行超时 30 秒。这对写业务代码的同学特别有用,因为可以在数据库层面做一道防线,防止慢查询拖垮实例。
2.4 删除角色:依赖关系怎么处理
DROP ROLE 报错是我见过的新手高频问题。最常见报错是:
ERROR: role "app_rw" cannot be dropped because some objects depend on it这个报错翻译成人话是:这个账号名下还持有表、视图、序列等对象,或者在某些对象的 owner 字段里。直接删是删不掉的,得先把这些对象转给别人:
-- 把旧账号拥有的所有对象重新指派给另一个角色 REASSIGN OWNED BY app_rw TO app_owner; -- 删除旧账号拥有的、且不属于任何人的残留对象 DROP OWNED BY app_rw; -- 最后删除角色 DROP ROLE app_rw;REASSIGN OWNED BY会自动处理该角色在数据库中拥有的所有对象,包括表、索引、视图、序列、函数、schema 等等。DROP OWNED BY会删除该角色拥有但尚未转移的对象。这两条命令执行时一定要确认目标角色选对了,尤其DROP OWNED BY是删除对象而不是转移,误操作后果很严重。
我在实际运维里见过最危险的误操作,就是有人为了删一个账号,先把账号提升成超级用户,然后强行 DROP。这种操作一旦出问题,牵连的权限关系非常难恢复。正确的顺序永远是:先转移所有权,再清理,最后删角色。
3. 对象级授权语法详解:GRANT / REVOKE 全参数解析
账号建好之后,真正的重头戏是对象级授权。这一层最灵活,也最容易出错。
3.1 对象权限类型一览
PostgreSQL 里不同对象类型有不同的权限集合,我用表格整理一下:
| 对象类型 | 可授权权限 |
|---|---|
| 数据库 | CREATE, CONNECT, TEMPORARY |
| Schema | CREATE, USAGE |
| 表 / 视图 / 物化视图 | SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, MAINTAIN |
| 序列 | USAGE, SELECT, UPDATE |
| 函数 / 过程 | EXECUTE |
| 类型 | USAGE |
| 表空间 | CREATE |
这里要解释一个容易混淆的点:TRUNCATE和DELETE是两回事。DELETE 只删行数据,TRUNCATE 是清空表并重置存储。如果一个账号只需要能删数据,不给TRUNCATE是更安全的默认选择。
再看REFERENCES权限,它允许用户创建外键引用该表。平时业务账号可能不需要,但如果有报表团队要把不同表关联起来做分析,没有 REFERENCES 权限,建外键或者 JOIN 某些场景会受限。这个权限经常被遗漏,等别人来问"为什么我建不了外键"时才想起来。
至于MAINTAIN,这是 PostgreSQL 16 新增的权限,专门用来解决"非表 owner 想执行 VACUUM / ANALYZE 却能不开超级用户"的问题。后面专门有一节单独讲,这里先记住它属于对象级权限。
3.2 GRANT 授权语法与常见场景
授权语法可以总结为:
GRANT 权限列表 ON 对象类型 对象名 TO 被授权者 [WITH GRANT OPTION];一个典型只读账号授权,需要同时处理数据库、Schema、表三层:
-- 允许连接数据库 GRANT CONNECT ON DATABASE appdb TO app_ro; -- 允许使用 schema GRANT USAGE ON SCHEMA public TO app_ro; -- 允许读取所有表数据 GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro;注意第二行USAGE ON SCHEMA,它不等于"能读表",只表示"允许进入这个 schema 并使用其中的对象"。没有这一步,后面 SELECT 授权做得再全也会报权限错误。这个分层设计不是多余的,它允许你创建一个"看得见门但进不去屋"的账号,对安全隔离很有用。
如果你希望一次性把某个 schema 里所有表的所有权限都交给一个账号:
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_rw;也可以针对特定表单独授权:
GRANT SELECT, INSERT, UPDATE, DELETE ON public.orders TO app_rw;还有一个容易被忽略但其实很重要的对象是序列。业务表经常有自增主键,如果只给表授予INSERT权限,却没有给序列授予USAGE权限,插入时会报 "permission denied for sequence"。完整的读写账号授权必须把序列也覆盖进去:
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_rw;3.3 WITH GRANT OPTION 和 REVOKE 的回收行为
WITH GRANT OPTION表示被授权者可以把权限转授给其他人。这个选项的核心风险在于回收时需要级联,因为权限链条已经往外延伸了。
先看一下 REVOKE 的两种写法:
-- 普通回收:只回收 alice 自己的权限 REVOKE SELECT ON public.orders FROM alice; -- 回收转授权能力:alice 不能再给别人授权,但它自己已有的权限还在 REVOKE GRANT OPTION FOR SELECT ON public.orders FROM alice; -- 级联回收:回收权限,同时把 alice 转授权产生的所有下游权限一并回收 REVOKE SELECT ON public.orders FROM alice CASCADE;我在生产环境处理过一次权限事故,就是某位同事给一个外包账号加上了WITH GRANT OPTION,外包账号又把它复制给了好几个服务账号。最后排查时不得不一路CASCADE回收,虽然解决问题了,但整个过程胆战心惊,因为中间有一层关系没梳理清楚,就可能误伤正常业务。所以我的建议很简单:默认不要用 WITH GRANT OPTION,需要"让某个人帮忙管理权限"的场景,优先选择把他加进一个管理组角色,而不是把转授权交给他个人。
3.4 ALTER DEFAULT PRIVILEGES:一次性解决"新表没权限"问题
手动 GRANT 最大的痛点在于:只对当前已存在的对象有效。今天授权了 100 张表,明天开发新建一张表,新账号还是没有权限。如果每次建表都手工补 GRANT,权限管理就会变成一场永无休止的追赶游戏。
ALTER DEFAULT PRIVILEGES就是为这个问题设计的。它的作用是定义"未来由某个角色创建的对象,默认授权给谁":
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw; ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_rw;这两行命令的意思是:当app_owner这个角色在 public schema 里新建表时,这些新表自动授予 app_rw 读写权限;新建序列时自动授予 app_rw 使用权限。注意FOR ROLE app_owner是关键,它限定了"谁的建表行为会触发默认授权",如果建表的人不是 app_owner,这个默认权限不生效。
在实际项目里,我会要求应用相关的表统一由某个建表角色创建(比如app_owner),这样配合ALTER DEFAULT PRIVILEGES,整个对象生命周期内的权限都能自动覆盖。已经存在的表再用GRANT ON ALL TABLES批量补齐一次,双管齐下。
这里还有个容易踩的细节:默认权限只对新建对象有效,不会回头修改已有对象。所以新上线一个项目时,正确的顺序是先手动批量授权已有对象,再设置默认权限,并让后续对象都使用指定角色创建。
4. 更高阶的权限控制手段:预定义角色、成员关系与行级安全
如果只是单人小项目,前面三节的内容已经够用了。但生产环境往往有多角色分权的问题,这就要用到 PostgreSQL 提供的"组角色"和预定义角色体系。
4.1 预定义角色:官方帮你审计好的常用权限包
PostgreSQL 内置了一批预定义角色,本质上是官方定义好的权限组,直接 GRANT 给某个账号即可:
| 预定义角色 | 用途 |
|---|---|
| pg_read_all_data | 读取所有数据库对象,相当于全局只读 |
| pg_write_all_data | 写入所有数据库对象,相当于全局读写 |
| pg_maintain | 对所有对象执行 VACUUM、ANALYZE、CLUSTER、REINDEX 等维护操作 |
| pg_monitor | 读取监控相关视图和函数 |
| pg_signal_backend | 取消或终止其他用户的后端进程 |
| pg_checkpoint | 执行 CHECKPOINT |
| pg_use_reserved_connections | 使用预留连接槽位 |
| pg_create_subscription | 创建逻辑复制订阅 |
| pg_execute_server_program | 执行服务端程序,用于 COPY 等场景 |
举例来说,需要一个全局只读的监控账号:
CREATE ROLE monitor_user WITH LOGIN PASSWORD 'monitor_pass'; GRANT pg_monitor TO monitor_user; GRANT pg_read_all_data TO monitor_user;这样 monitor_user 既能看监控视图,也能读所有业务表,比手动给几百张表逐个授权要省太多事。我建议不要自行模拟这些权限,直接使用官方预定义角色,因为 PG 后续版本会跟随系统目录变化维护这些角色的定义。
4.2 成员关系与 INHERIT:组角色的行为边界
当你执行GRANT group_role TO user_role时,user_role 就成为了 group_role 的成员。成员继承权限的行为受 INHERIT 属性控制。
默认情况下,角色 INHERIT,成员角色自动获得组角色的对象权限,不需要显式切换。比如:
CREATE ROLE app_ro_group; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro_group; CREATE ROLE alice LOGIN PASSWORD 'xxx'; GRANT app_ro_group TO alice;默认情况下 alice 登录后直接可以查询 public schema 的所有表,因为在权限检查时,PostgreSQL 会沿着角色成员链一路向上找权限。
但有一个例外要特别记住:INHERIT 只对对象权限生效,对角色属性(LOGIN、SUPERUSER、CREATEDB 等)不生效。也就是说,即使某个角色是超级用户的成员,它也不会因此自动成为超级用户,需要切换身份或者显式设置 SET ROLE 才能使用超级用户属性。这个设计其实是为了防止权限链无意间放大,理解之后就不会觉得它"反直觉"了。
4.3 SET ROLE:临时切换身份
有时候你需要以某个组角色的身份执行操作,而不是自己的身份。比如一个 DBA 平时用低权限账号登录,做维护时临时切换成维护角色:
SET ROLE pg_maintain; -- 执行 ANALYZE 等操作 ANALYZE public.orders; RESET ROLE;SET ROLE和SET SESSION AUTHORIZATION的区别需要理清:前者切换的是"当前会话中的角色身份",后者切换的是"会话所属的登录用户身份"。日常运维中基本不用后者,前者足够。
有一个小坑:RESET ROLE 之后会回到登录角色的默认权限,但如果你开了多个嵌套的 SET ROLE,需要一层层 RESET。生产环境里我发现很多同学在脚本里写了 SET ROLE,但忘写 RESET ROLE,于是脚本后续所有操作都带着高权限身份执行,这个隐患在审计时会被拎出来重点查。
4.4 行级安全(RLS):让同一张表对不同用户显示不同行
对象级权限控制的是"能不能操作这张表",行级安全(Row-Level Security)控制的是"能操作这张表的哪些行"。两者互补,在实际的多租户场景里非常有价值。
启用 RLS 的步骤是:
-- 1. 启用表的行级安全 ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY; -- 2. 创建一条策略,比如只允许用户看到属于自己组织的数据 CREATE POLICY tenant_isolation ON public.orders USING (org_id = current_setting('app.org_id')::int);这条策略的含义是:当用户查询 orders 表时,PostgreSQL 会自动追加一个条件org_id = current_setting('app.org_id')。如果你在同一个表里存了 A 和 B 两个租户的数据,A 租户的连接即使执行SELECT * FROM orders,也只能看到自己的行。
启用 RLS 后默认行为是"拒绝",也就是说如果没有匹配的策略,普通用户看不到任何行。表 owner 和超级用户默认不受 RLS 限制,除非你显式执行FORCE ROW LEVEL SECURITY。业务账号如果不想被 RLS 影响,可以给角色加BYPASSRLS属性,但这等于放弃了行级安全防线,需要严格评估。
RLS 是把双刃剑,它极大地增强了多租户隔离能力,但也会让 SQL 执行计划变得更复杂,而且策略本身如果写得不对,可能连 owner 都查不到数据。我建议在开发阶段就启用并做好充分测试,不要等生产数据进去之后再“补”策略。
5. 实战:从开发库到生产库,一套完整授权方案设计
理论讲了那么多,还是落到一个实际场景里看看完整做法。假设现在要为一个中型 Web 应用设计数据库权限,业务库叫 appdb,schema 是 app,表结构由团队维护。
5.1 场景与角色拆分
我们需要三类角色:
| 角色 | 用途 | 权限范围 |
|---|---|---|
| app_owner | 建表、改表结构,DDL 操作 | schema 和对象的 owner |
| app_rw | 应用服务运行账号,DML 操作 | 表的增删改查、序列使用 |
| app_ro | 报表、临时查询、数据导出 | 表只读 |
再加上一个管理员账号 admin_dba,用于日常 DBA 操作,拥有超级用户权限。这样设计的核心思想是:应用连接时用 app_rw 或 app_ro,不要用拥有对象的账号直接跑 DML,避免应用账号误操作 DDL。
5.2 落地步骤
第一步,创建数据库和 schema,并指定 owner:
CREATE DATABASE appdb OWNER app_owner; \connect appdb CREATE SCHEMA app AUTHORIZATION app_owner;第二步,创建应用账号:
CREATE ROLE app_rw WITH LOGIN PASSWORD 'rw_pass'; CREATE ROLE app_ro WITH LOGIN PASSWORD 'ro_pass' CONNECTION LIMIT 20;第三步,授予数据库连接和 schema 使用权限:
GRANT CONNECT ON DATABASE appdb TO app_rw, app_ro; GRANT USAGE ON SCHEMA app TO app_rw, app_ro;第四步,配置默认权限,让 app_owner 未来新建的任何表自动授权:
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw; ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT USAGE, SELECT ON SEQUENCES TO app_rw; ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT SELECT ON TABLES TO app_ro; ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app GRANT SELECT ON SEQUENCES TO app_ro;第五步,如果 schema 里已经有一些表(比如从旧库迁过来),需要手动批量补一次权限:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_rw; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_rw; GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_ro; GRANT SELECT ON ALL SEQUENCES IN SCHEMA app TO app_ro;到这里,app_rw 登录后可以直接读写应用表,app_ro 登录后可以查询、导出数据,两者都没有 DDL 权限,也不会误删表结构。
5.3 权限验收与检查
授权完成后,不能只看"好像能用了",要做一次系统性的检查。我常用的检查 SQL 是以 grantee 维度查看表权限:
SELECT grantee, privilege_type, table_name FROM information_schema.role_table_grants WHERE table_schema = 'app' ORDER BY grantee, table_name, privilege_type;另一个方式是从系统目录看 ACL,能看到每个对象的授权全貌:
SELECT n.nspname AS schema, c.relname AS table, pg_get_userbyid(c.relowner) AS owner, acl.privileges FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN LATERAL aclexplode(c.relacl) AS acl ON TRUE WHERE c.relkind = 'r' AND n.nspname = 'app' ORDER BY n.nspname, c.relname;aclexplode是理解 PostgreSQL ACL 的利器,它可以把表上的 ACL 数组展开成一行行权限记录,方便导出审计。建议把这两条 SQL 保存成常用查询脚本,每次给新环境配置完权限后跑一遍。
5.4 方案延伸:环境隔离
开发库、测试库、生产库的权限应该分开设计,不要在三个环境共用一套密码。我见过不少团队开发环境直接用超级用户跑业务,等到了生产环境才想起来收敛权限,结果应用上线第一天就因为权限不足报错,手忙脚乱补授权。正确的节奏是:开发环境可以宽松,但生产库从第一张表开始就执行 app_rw / app_ro 的最小权限原则。权限这场仗应该在生产环境之前就打赢,而不是在生产环境边救火边总结。
6. 升级到 PostgreSQL 16,权限这块要注意什么
PG16 在权限管理上做了几个比较重要的调整,从旧版本升级后如果不了解这些变化,很容易踩雷。
6.1 新增 MAINTAIN 权限
PG16 之前,要对一张不是自己拥有的表执行VACUUM或ANALYZE,通常需要超级用户。这在分权管理的环境里很麻烦:业务表的 owner 可能是一个建表账号,但日常维护人员并不需要拥有表的所有权限,只想做统计分析和优化。
PG16 新增了MAINTAIN权限,专门解决这类需求。拥有某张表的 MAINTAIN 权限后,就可以对该表执行 VACUUM、ANALYZE、CLUSTER、REINDEX 等维护操作,不需要成为表 owner,也不需要超级用户。
授权方式和其他对象权限完全一样:
GRANT MAINTAIN ON TABLE public.orders TO maintainer_role;也可以批量授权:
GRANT MAINTAIN ON ALL TABLES IN SCHEMA app TO maintainer_role;这个权限非常适合运维团队和数据分析团队。他们需要做定期的ANALYZE来生成统计信息,但不需要也不应该有 DDL 权限。过去很多团队只能给这类账号开超级用户,风险很大,有了 MAINTAIN 之后授权粒度精细了很多。
6.2 新增预定义角色 pg_maintain
与 MAINTAIN 权限配套,PG16 还新增了pg_maintain预定义角色。授予这个角色的账号可以对数据库内的所有对象执行维护操作,而无需拥有这些对象:
GRANT pg_maintain TO maintainer_role;这里要区分的场景是:MAINTAIN是对象级权限,适合精确指定某几张表;pg_maintain是全局角色,适合"这个人要做全区维护"的场景。两者可以叠加使用,但实际设计权限方案时,我会优先考虑 MAINTIAIN 精确授权,只有维护人员数量多、且维护范围覆盖全库时才考虑直接给 pg_maintain。
6.3 从 PG15 / PG14 升级时容易踩的权限问题
- public schema 的默认 CREATE 权限变化。PG15 之后,public schema 不再默认对 public 角色授予 CREATE 权限。这意味着从 PG14 升级到 PG15 或 PG16 后,普通用户在同一数据库的 public schema 里建表的操作会失败。如果你的应用确实需要在 public schema 里动态建表,需要显式授予:
GRANT CREATE ON SCHEMA public TO app_rw;但更建议的做法是:使用独立 schema,而不是继续在 public 里建业务表。public schema 的定位应该是默认放系统信息的,业务表放独立 schema 会更安全。
预定义角色的成员关系不会自动迁移。升级后检查一遍
\du,确认 pg_read_all_data、pg_monitor 等角色的授权是否还在。有些安全加固类工具在旧版本里直接改系统表添加角色成员关系,升级后可能失效。密码认证方式的兼容。PG16 已经默认使用 SCRAM-SHA-256 密码加密,如果旧库里有使用 md5 认证的账号,升级后连接可能失败,需要在 pg_hba.conf 和密码格式上做好同步。这一点虽然不是纯权限问题,但影响登录行为,排查时常常容易往权限方向误判。
6.4 升级后的权限审计清单
作为运维习惯,升级完成后我会按这个清单快速过一遍:
- 用
\du查看所有角色及属性,确认超级用户列表没有异常膨胀。 - 用
\l查看数据库 owner 和权限,确认没有 plan 以外的默认 public 权限开放。 - 用
\dn+查看 schema 权限,尤其确认 schema 的 CREATE 权限是否符合预期。 - 抽查几张核心表,确认业务账号的 SELECT / INSERT / UPDATE / DELETE 权限正确。
- 确认 pg_maintain、MAINTAIN 等新权限没有被错误授予给不必要的账号。
这套检查我一般会在升级演练环境的测试阶段做一次,生产切换后立刻再做一次,前后对比能快速发现权限配置漂移。
7. 权限问题排查思路与运维心得
最后一部分,聊一聊真正遇到权限问题时应该怎么排查。这部分内容很实战,建议保存下来遇到问题时候再翻。
7.1 常见权限报错与含义
| 报错信息 | 常见原因 |
|---|---|
| permission denied for schema public | Schema 层 USAGE 权限缺失 |
| permission denied for table orders | 表级 SELECT / INSERT 等权限缺失 |
| permission denied for sequence orders_id_seq | 序列 USAGE 权限缺失 |
| permission denied for database appdb | 数据库 CONNECT 权限缺失 |
| must be owner of table orders | 当前角色不是 owner,且没有 MAINTAIN 权限 |
| role does not exist | 角色名写错了,或者还没有创建 |
排查思路可以总结成一条链:先确认能不能连上库,再看能不能进 schema,再看能不能操作表,一层一层往里走。很多人习惯直接去查表权限,查了半天没发现问题,最后发现是数据库连接权限没给,白白折腾。
7.2 快速定位一次权限问题的复盘路径
假设你收到反馈:"账号 bob 查不了 public.orders 表"。我会按这个顺序快速定位:
第一步,确认 bob 是否存在及是否能登录:
SELECT rolname, rolcanlogin, rolsuper FROM pg_roles WHERE rolname = 'bob';第二步,确认 bob 对 appdb 的连接权限:
SELECT has_database_privilege('bob', 'appdb', 'CONNECT');第三步,确认 bob 对 public schema 的权限:
SELECT has_schema_privilege('bob', 'public', 'USAGE');第四步,确认 bob 对 orders 表的权限:
SELECT has_table_privilege('bob', 'public.orders', 'SELECT'); SELECT has_table_privilege('bob', 'public.orders', 'INSERT');每一步返回true或false,很快就能定位是哪一层权限缺失。这比翻\dp输出直观得多,尤其是权限关系复杂、有多层组角色嵌套时,直接查询某个角色最终是否拥有某项权限,才是真正有效的办法。
7.3 长期权限运维的几个习惯
第一,不要随手给业务账号开超级用户。超级用户会绕过所有权限检查,会掩盖掉所有权限设计上的漏洞。业务代码里有不当操作,只有把它们暴露在最小权限环境下,才能尽早暴露问题。
第二,角色的命名和用途要有记录。我见过一个库里有 20 多个角色,没人说得清哪个角色是干什么用的。建议在备注里写明用途,比如创建角色时加上 COMMENT:
COMMENT ON ROLE app_rw IS 'appdb 应用读写账号,仅用于Web服务,禁止用于日常运维';第三,定期做权限审计。至少每季度导出一次所有角色和各 schema 的 ACL,交给负责人确认。权限这个东西,时间一长就会因为各种临时授权逐步“膨胀”,等出事的时候再去追溯,成本远高于日常定期检查。
第四,利用ALTER DEFAULT PRIVILEGES作为权限管理的主路径,而不是事后补 GRANT。每次新项目上线,我都会先设计好默认权限,再让业务账号接触数据库。这能避免绝大多数"新表没权限"的凌晨告警。
第五,不要把权限都放在一个角色里。哪怕项目规模不大,也尽量保持"owner 角色 + 读写角色 + 只读角色"的结构。这个成本很低,但对后续的权限收敛和安全审计帮助极大。
我在实际维护中最大的体会是,权限管理做得好不好,往往不是看某一条授权语句写得有多完美,而是看整个体系是否可预期。所谓可预期,就是你不用翻半天日志,也能说出某个角色在某些条件下能做什么、不能做什么。PostgreSQL 提供的能力足够支撑这个目标,关键是把语法用对、把边界想清楚,然后让规则长期稳定地执行下去。希望这篇把概念和实战串起来的内容,能让你在搭权限体系的时候少走一些弯路。