概述
PostgreSQL 的权限管理体系设计精细且层次分明。与某些数据库系统相对简化的权限模型不同,PostgreSQL 要求用户从实例连接层开始,逐级通过数据库、Schema、表、列乃至行级安全(Row Level Security)的层层校验,才能最终访问到目标数据。这种层次化的设计虽然在初次接触时略显复杂,但为数据安全提供了极为细粒度的控制能力。
本文将从权限层级体系出发,深入解析 PostgreSQL 中 Role 与 User 的本质区别、预定义角色的使用场景、角色删除的标准流程,以及权限审计与巡检的最佳实践。
权限层级体系
PostgreSQL 的权限控制遵循一个清晰的层次结构。用户要访问最终数据,必须依次获得以下各层的授权:
#mermaid-svg-0kOKdIpCk952wA7s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0kOKdIpCk952wA7s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0kOKdIpCk952wA7s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0kOKdIpCk952wA7s .error-icon{fill:#552222;}#mermaid-svg-0kOKdIpCk952wA7s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0kOKdIpCk952wA7s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0kOKdIpCk952wA7s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0kOKdIpCk952wA7s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0kOKdIpCk952wA7s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0kOKdIpCk952wA7s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0kOKdIpCk952wA7s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0kOKdIpCk952wA7s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0kOKdIpCk952wA7s .marker.cross{stroke:#333333;}#mermaid-svg-0kOKdIpCk952wA7s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0kOKdIpCk952wA7s p{margin:0;}#mermaid-svg-0kOKdIpCk952wA7s .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0kOKdIpCk952wA7s .cluster-label text{fill:#333;}#mermaid-svg-0kOKdIpCk952wA7s .cluster-label span{color:#333;}#mermaid-svg-0kOKdIpCk952wA7s .cluster-label span p{background-color:transparent;}#mermaid-svg-0kOKdIpCk952wA7s .label text,#mermaid-svg-0kOKdIpCk952wA7s span{fill:#333;color:#333;}#mermaid-svg-0kOKdIpCk952wA7s .node rect,#mermaid-svg-0kOKdIpCk952wA7s .node circle,#mermaid-svg-0kOKdIpCk952wA7s .node ellipse,#mermaid-svg-0kOKdIpCk952wA7s .node polygon,#mermaid-svg-0kOKdIpCk952wA7s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0kOKdIpCk952wA7s .rough-node .label text,#mermaid-svg-0kOKdIpCk952wA7s .node .label text,#mermaid-svg-0kOKdIpCk952wA7s .image-shape .label,#mermaid-svg-0kOKdIpCk952wA7s .icon-shape .label{text-anchor:middle;}#mermaid-svg-0kOKdIpCk952wA7s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0kOKdIpCk952wA7s .rough-node .label,#mermaid-svg-0kOKdIpCk952wA7s .node .label,#mermaid-svg-0kOKdIpCk952wA7s .image-shape .label,#mermaid-svg-0kOKdIpCk952wA7s .icon-shape .label{text-align:center;}#mermaid-svg-0kOKdIpCk952wA7s .node.clickable{cursor:pointer;}#mermaid-svg-0kOKdIpCk952wA7s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0kOKdIpCk952wA7s .arrowheadPath{fill:#333333;}#mermaid-svg-0kOKdIpCk952wA7s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0kOKdIpCk952wA7s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0kOKdIpCk952wA7s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0kOKdIpCk952wA7s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0kOKdIpCk952wA7s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0kOKdIpCk952wA7s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0kOKdIpCk952wA7s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0kOKdIpCk952wA7s .cluster text{fill:#333;}#mermaid-svg-0kOKdIpCk952wA7s .cluster span{color:#333;}#mermaid-svg-0kOKdIpCk952wA7s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0kOKdIpCk952wA7s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0kOKdIpCk952wA7s rect.text{fill:none;stroke-width:0;}#mermaid-svg-0kOKdIpCk952wA7s .icon-shape,#mermaid-svg-0kOKdIpCk952wA7s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0kOKdIpCk952wA7s .icon-shape p,#mermaid-svg-0kOKdIpCk952wA7s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0kOKdIpCk952wA7s .icon-shape .label rect,#mermaid-svg-0kOKdIpCk952wA7s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0kOKdIpCk952wA7s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0kOKdIpCk952wA7s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0kOKdIpCk952wA7s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端连接
pg_hba.conf 认证
实例级别
连接权限
数据库级别
CONNECT 权限
Schema 级别
USAGE 权限
表/视图级别
SELECT/INSERT/UPDATE/DELETE 等
列级别
列级权限
行级别
Row Level Security 策略
访问最终数据
各层级对应的核心权限类型如下:
| 层级 | 对象 | 核心权限 | 说明 |
|---|---|---|---|
| 实例级 | 数据库集群 | pg_hba.conf | 白名单控制,决定能否建立网络连接 |
| 数据库级 | DATABASE | CONNECT, CREATE, TEMP | 控制能否连接到特定数据库 |
| Schema 级 | SCHEMA | USAGE, CREATE | 控制能否使用或在该 Schema 下创建对象 |
| 表级 | TABLE | SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER | 控制对表数据的操作 |
| 列级 | COLUMN | SELECT(column), UPDATE(column) | 控制对特定列的访问 |
| 行级 | ROW | RLS 策略 | 通过行级安全策略控制行级访问 |
值得注意的是,表级权限不会从 Schema 级别的授权中自动继承 。即使用户拥有某个 Schema 的 USAGE 权限,若未被显式授予该 Schema 下某张表的 SELECT 权限,仍然无法查询该表的数据。
以下示例演示了一个完整的权限授予流程:
sql
-- 1. 创建数据库
CREATE DATABASE mydb;
-- 2. 创建 Schema
CREATE SCHEMA app_schema;
-- 3. 创建角色(无登录权限,作为权限模板)
CREATE ROLE app_readonly;
-- 4. 授予 Schema 使用权
GRANT USAGE ON SCHEMA app_schema TO app_readonly;
-- 5. 授予表查询权限
GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly;
-- 6. 创建用户(具有登录权限)
CREATE USER app_user WITH PASSWORD 'secure_password';
-- 7. 将角色授予用户
GRANT app_readonly TO app_user;
-- 8. 授予数据库连接权限
GRANT CONNECT ON DATABASE mydb TO app_user;
若在上述任意一层缺少相应权限,访问将被拒绝。例如,仅回收数据库的连接权限即可阻止用户访问:
sql
-- 回收数据库连接权限
REVOKE CONNECT ON DATABASE mydb FROM app_user;
-- 注意:由于 PUBLIC 是特殊角色,代表所有用户,可能还需要额外回收
REVOKE CONNECT ON DATABASE mydb FROM PUBLIC;
Role 与 User 的本质区别
在 PostgreSQL 中,Role(角色) 和 User(用户) 在本质上并无区别------二者都是 Role。唯一的差异在于默认属性:
CREATE ROLE创建的角色默认 不具有LOGIN属性(即NOLOGIN)CREATE USER创建的角色默认 具有LOGIN属性
因此,可以这样理解:
CREATE USER name 等价于 CREATE ROLE name LOGIN。
Role 的核心设计理念是作为权限的集合(权限模板)。在实际生产环境中,推荐的最佳实践是:
- 根据业务需求创建多个 不带 LOGIN 属性的 Role,每个 Role 代表一组特定的权限集合(如只读、读写、管理员等)
- 将相应的 Role 授予给实际的 User (带
LOGIN属性的 Role) - 当权限需求发生变化时,只需修改 Role 的权限,所有授予了该 Role 的 User 将自动继承变更
这种设计模式类似于班级中的"课代表""劳动委员"等角色------角色定义了职责和权力范围,具体同学被授予某个角色后即获得相应的权力。
sql
-- 创建组角色(无登录权限)
CREATE ROLE read_only_role;
CREATE ROLE read_write_role;
-- 授予组角色权限
GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO read_write_role;
-- 创建用户并授予组角色
CREATE USER alice WITH PASSWORD 'xxx';
CREATE USER bob WITH PASSWORD 'xxx';
GRANT read_only_role TO alice; -- alice 只有只读权限
GRANT read_write_role TO bob; -- bob 拥有读写权限
权限继承机制
PostgreSQL 支持 Role 之间的继承关系。当一个 Role 被授予给另一个 Role 时,成员 Role 会自动获得父 Role 的所有权限。
sql
-- 创建基础角色
CREATE ROLE viewer;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO viewer;
-- 创建高级角色,继承 viewer
CREATE ROLE editor;
GRANT viewer TO editor; -- editor 自动拥有 viewer 的所有权限
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO editor;
-- 创建用户并授予 editor
CREATE USER charlie WITH PASSWORD 'xxx';
GRANT editor TO charlie; -- charlie 拥有 viewer + editor 的全部权限
继承关系是传递性的:若 A 继承 B,B 继承 C,则 A 自动拥有 B 和 C 的全部权限。但需要注意,继承链会在遇到 NOINHERIT 属性的 Role 时终止。
对于拥有高权限的角色(如 SUPERUSER、CREATEDB、CREATEROLE 等),建议设置 NOINHERIT 属性以减少权限提升的风险。
预定义角色
PostgreSQL 提供了一组预定义角色(Predefined Roles),用于简化常见权限管理场景。这些角色在 PostgreSQL 14 及更高版本中得到了显著增强。
以下是常用预定义角色的汇总:
| 预定义角色 | 权限说明 | 引入版本 |
|---|---|---|
pg_read_all_data |
读取所有表、视图、序列的数据,如同拥有 SELECT 权限;对所有 Schema 拥有 USAGE 权限 |
PostgreSQL 14 |
pg_write_all_data |
写入所有表、视图、序列的数据,如同拥有 INSERT、UPDATE、DELETE 权限 |
PostgreSQL 14 |
pg_monitor |
读取/执行各种监控视图和函数。是 pg_read_all_settings、pg_read_all_stats、pg_stat_scan_tables 的成员角色 |
PostgreSQL 10 |
pg_signal_backend |
向其他后端进程发送信号,以取消查询或终止会话 | PostgreSQL 9.6 |
pg_read_all_settings |
读取所有配置变量,包括通常仅对超级用户可见的变量 | PostgreSQL 10 |
pg_read_all_stats |
读取所有 pg_stat_* 视图及统计相关信息 |
PostgreSQL 10 |
pg_stat_scan_tables |
执行可能对表持有 ACCESS SHARE 锁的监控函数 |
PostgreSQL 10 |
pg_database_owner |
隐式成员关系,成员为当前数据库的 Owner | PostgreSQL 14 |
pg_read_server_files |
通过 COPY 及其他文件访问函数读取服务器文件 |
PostgreSQL 10 |
pg_write_server_files |
通过 COPY 及其他文件访问函数写入服务器文件 |
PostgreSQL 10 |
pg_execute_server_program |
以数据库运行用户身份执行服务器端程序 | PostgreSQL 10 |
使用示例:
sql
-- 授予只读权限(PostgreSQL 14+)
GRANT pg_read_all_data TO app_readonly_user;
-- 授予读写权限(PostgreSQL 14+)
GRANT pg_write_all_data TO app_readwrite_user;
-- 授予监控权限
GRANT pg_monitor TO monitoring_user;
-- 授予终止会话权限
GRANT pg_signal_backend TO admin_user;
需要注意的是,pg_read_all_data 和 pg_write_all_data 不包含 BYPASSRLS 属性。如果数据库中启用了行级安全(RLS),管理员可能需要为被授予这些角色的用户额外设置 BYPASSRLS。
在 PostgreSQL 14 之前,要实现只读用户需要逐表授予 SELECT 权限并回收默认权限,操作较为繁琐。PostgreSQL 14 引入的 pg_read_all_data 和 pg_write_all_data 极大地简化了这一流程。
PUBLIC 伪角色
PUBLIC 是一个特殊的伪角色,代表 所有角色 (包括当前已存在和将来可能创建的角色)。每个新创建的用户都会自动成为 PUBLIC 的成员。
关键注意事项:
- 某些对象类型在创建时会默认授予
PUBLIC一定的权限 - 例如,
publicSchema 默认授予PUBLIC的CREATE权限 - 回收单个用户的权限时,必须同时从
PUBLIC回收 ,否则该用户仍可能通过PUBLIC保留权限
sql
-- 创建测试用户
CREATE USER test_user WITH PASSWORD 'xxx';
-- 仅回收 test_user 的权限是不够的
REVOKE CONNECT ON DATABASE mydb FROM test_user;
-- 还必须从 PUBLIC 回收
REVOKE CONNECT ON DATABASE mydb FROM PUBLIC;
-- 更安全的做法:在 template1 中预先回收,使所有新建数据库默认安全
\c template1
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
删除角色
删除一个曾拥有对象或权限的角色,并非简单的 DROP ROLE 操作。PostgreSQL 要求先处理该角色拥有的所有对象及其被授予的权限。
标准删除流程:
#mermaid-svg-67fcwwq5pEhreiLd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-67fcwwq5pEhreiLd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-67fcwwq5pEhreiLd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-67fcwwq5pEhreiLd .error-icon{fill:#552222;}#mermaid-svg-67fcwwq5pEhreiLd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-67fcwwq5pEhreiLd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-67fcwwq5pEhreiLd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-67fcwwq5pEhreiLd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-67fcwwq5pEhreiLd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-67fcwwq5pEhreiLd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-67fcwwq5pEhreiLd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-67fcwwq5pEhreiLd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-67fcwwq5pEhreiLd .marker.cross{stroke:#333333;}#mermaid-svg-67fcwwq5pEhreiLd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-67fcwwq5pEhreiLd p{margin:0;}#mermaid-svg-67fcwwq5pEhreiLd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-67fcwwq5pEhreiLd .cluster-label text{fill:#333;}#mermaid-svg-67fcwwq5pEhreiLd .cluster-label span{color:#333;}#mermaid-svg-67fcwwq5pEhreiLd .cluster-label span p{background-color:transparent;}#mermaid-svg-67fcwwq5pEhreiLd .label text,#mermaid-svg-67fcwwq5pEhreiLd span{fill:#333;color:#333;}#mermaid-svg-67fcwwq5pEhreiLd .node rect,#mermaid-svg-67fcwwq5pEhreiLd .node circle,#mermaid-svg-67fcwwq5pEhreiLd .node ellipse,#mermaid-svg-67fcwwq5pEhreiLd .node polygon,#mermaid-svg-67fcwwq5pEhreiLd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-67fcwwq5pEhreiLd .rough-node .label text,#mermaid-svg-67fcwwq5pEhreiLd .node .label text,#mermaid-svg-67fcwwq5pEhreiLd .image-shape .label,#mermaid-svg-67fcwwq5pEhreiLd .icon-shape .label{text-anchor:middle;}#mermaid-svg-67fcwwq5pEhreiLd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-67fcwwq5pEhreiLd .rough-node .label,#mermaid-svg-67fcwwq5pEhreiLd .node .label,#mermaid-svg-67fcwwq5pEhreiLd .image-shape .label,#mermaid-svg-67fcwwq5pEhreiLd .icon-shape .label{text-align:center;}#mermaid-svg-67fcwwq5pEhreiLd .node.clickable{cursor:pointer;}#mermaid-svg-67fcwwq5pEhreiLd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-67fcwwq5pEhreiLd .arrowheadPath{fill:#333333;}#mermaid-svg-67fcwwq5pEhreiLd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-67fcwwq5pEhreiLd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-67fcwwq5pEhreiLd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67fcwwq5pEhreiLd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-67fcwwq5pEhreiLd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67fcwwq5pEhreiLd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-67fcwwq5pEhreiLd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-67fcwwq5pEhreiLd .cluster text{fill:#333;}#mermaid-svg-67fcwwq5pEhreiLd .cluster span{color:#333;}#mermaid-svg-67fcwwq5pEhreiLd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-67fcwwq5pEhreiLd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-67fcwwq5pEhreiLd rect.text{fill:none;stroke-width:0;}#mermaid-svg-67fcwwq5pEhreiLd .icon-shape,#mermaid-svg-67fcwwq5pEhreiLd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67fcwwq5pEhreiLd .icon-shape p,#mermaid-svg-67fcwwq5pEhreiLd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-67fcwwq5pEhreiLd .icon-shape .label rect,#mermaid-svg-67fcwwq5pEhreiLd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67fcwwq5pEhreiLd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-67fcwwq5pEhreiLd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-67fcwwq5pEhreiLd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} REASSIGN OWNED
DROP OWNED
重复执行
每个数据库
DROP ROLE
步骤说明:
REASSIGN OWNED BY old_role TO new_role:将old_role拥有的所有对象的所有权 转移给new_roleDROP OWNED BY old_role:删除old_role拥有的、尚未转移所有权的对象,并移除授予old_role的所有权限- 在每个包含
old_role所拥有对象的数据库中重复执行上述两步 DROP ROLE old_role:最后删除角色本身
重要提示 :
REASSIGN OWNED无法处理 跨数据库的对象,且不会移除 授予给目标角色的权限(这些权限不属于该角色拥有)。因此,必须先执行REASSIGN OWNED,再执行DROP OWNED,顺序不可颠倒。
错误示例(直接 DROP ROLE):
sql
-- 直接删除会报错
DROP ROLE u1;
-- ERROR: role "u1" cannot be dropped because some objects depend on it
-- DETAIL: privileges for table t1
-- privileges for table t2
-- ...
正确示例:
sql
-- 1. 在每个相关数据库中转移所有权
\c db1
REASSIGN OWNED BY u1 TO u2;
\c db2
REASSIGN OWNED BY u1 TO u2;
-- 2. 在每个相关数据库中移除权限和残留对象
\c db1
DROP OWNED BY u1;
\c db2
DROP OWNED BY u1;
-- 3. 最后删除角色
DROP ROLE u1;
DROP OWNED 会删除目标角色拥有的对象,因此执行前请确保所有需要保留的对象已通过 REASSIGN OWNED 转移给其他角色。
权限审计与巡检
PostgreSQL 本身不提供内置的权限差异对比功能。但可以通过社区扩展或自定义查询来实现权限审计。
pg_permissions 扩展
pg_permissions 是一个第三方扩展,用于审查和对比数据库对象权限。
安装与使用:
sql
-- 安装扩展
CREATE EXTENSION pg_permissions SCHEMA public;
-- 定义期望的权限配置
INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name)
VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'TABLE', 'appschema');
-- 查看权限差异(期望权限与实际权限的对比)
SELECT * FROM public.permission_diffs();
-- 返回缺失的权限和多余的权限
-- 查看实际权限
SELECT * FROM schema_permissions
WHERE role_name = 'appuser' AND schema_name = 'appschema';
-- 返回该角色在指定 Schema 上的实际权限
该扩展提供的视图包括:
database_permissions:数据库级权限schema_permissions:Schema 级权限table_permissions:表级权限view_permissions:视图级权限column_permissions:列级权限function_permissions:函数权限sequence_permissions:序列权限all_permissions:所有对象的权限合集
自定义权限查询
若不安装扩展,也可以通过查询系统目录来审查权限:
sql
-- 查看角色的所有权限(表级)
SELECT
grantor,
grantee,
table_schema,
table_name,
privilege_type
FROM information_schema.table_privileges
WHERE grantee = 'appuser';
-- 查看角色的角色成员关系
SELECT
r.rolname AS role_name,
m.rolname AS member_name,
r.rolinherit
FROM pg_roles r
JOIN pg_auth_members am ON am.roleid = r.oid
JOIN pg_roles m ON m.oid = am.member;
-- 查看角色的登录权限等属性
SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin
FROM pg_roles
WHERE rolcanlogin = true;
API 速览
CREATE ROLE
创建新的数据库角色。
语法:
sql
CREATE ROLE name [ [ WITH ] option [ ... ] ]
常用选项:
| 选项 | 说明 |
|---|---|
LOGIN / NOLOGIN |
是否允许登录。CREATE USER 默认 LOGIN,CREATE ROLE 默认 NOLOGIN |
SUPERUSER / NOSUPERUSER |
是否拥有超级用户权限 |
CREATEDB / NOCREATEDB |
是否允许创建数据库 |
CREATEROLE / NOCREATEROLE |
是否允许创建其他角色 |
INHERIT / NOINHERIT |
是否继承所属角色的权限 |
BYPASSRLS / NOBYPASSRLS |
是否绕过行级安全策略 |
CONNECTION LIMIT |
并发连接数限制 |
PASSWORD |
设置密码 |
示例:
sql
-- 创建无登录权限的组角色
CREATE ROLE app_readonly NOLOGIN;
-- 创建可登录的用户(等效于 CREATE USER)
CREATE ROLE app_user LOGIN PASSWORD 'secure_password';
-- 创建具有创建数据库权限的角色
CREATE ROLE db_admin CREATEDB;
CREATE USER
CREATE USER 是 CREATE ROLE 的别名,区别在于默认包含 LOGIN 属性。
语法:
sql
CREATE USER name [ [ WITH ] option [ ... ] ]
示例:
sql
-- 等效于 CREATE ROLE app_user LOGIN
CREATE USER app_user WITH PASSWORD 'secure_password';
GRANT
授予权限或角色成员关系。
授予对象权限:
sql
GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
[, ...] | ALL [ PRIVILEGES ] }
ON { [ TABLE ] table_name [, ...] | ALL TABLES IN SCHEMA schema_name [, ...] }
TO role_specification [, ...] [ WITH GRANT OPTION ]
授予角色成员关系:
sql
GRANT role_name [, ...] TO role_specification [, ...] [ WITH ADMIN OPTION ]
示例:
sql
-- 授予表查询权限
GRANT SELECT ON TABLE users TO app_readonly;
-- 授予 Schema 使用权
GRANT USAGE ON SCHEMA public TO app_readonly;
-- 授予所有表的查询权限
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
-- 将角色授予用户
GRANT app_readonly TO app_user;
-- 授予预定义角色
GRANT pg_monitor TO monitoring_user;
REVOKE
回收权限或角色成员关系。
语法:
sql
REVOKE [ GRANT OPTION FOR ]
{ { SELECT | INSERT | UPDATE | DELETE | ... } [, ...] | ALL [ PRIVILEGES ] }
ON [ TABLE ] table_name [, ...]
FROM { [ GROUP ] role_name | PUBLIC } [, ...]
[ CASCADE | RESTRICT ]
示例:
sql
-- 回收表查询权限
REVOKE SELECT ON TABLE users FROM app_readonly;
-- 回收 PUBLIC 的默认权限
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- 回收数据库连接权限
REVOKE CONNECT ON DATABASE mydb FROM PUBLIC;
ALTER DEFAULT PRIVILEGES
设置将来创建对象的默认权限。
语法:
sql
ALTER DEFAULT PRIVILEGES
[ FOR { ROLE | USER } target_role [, ...] ]
[ IN SCHEMA schema_name [, ...] ]
abbreviated_grant_or_revoke
示例:
sql
-- 设置默认权限:未来在 public Schema 中创建的所有表都对 app_readonly 可读
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app_readonly;
-- 设置默认权限:未来创建的函数对 PUBLIC 回收 EXECUTE 权限
ALTER DEFAULT PRIVILEGES
REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
REASSIGN OWNED
将目标角色拥有的所有对象的所有权转移给另一个角色。
语法:
sql
REASSIGN OWNED BY old_role [, ...] TO new_role
示例:
sql
-- 将 u1 拥有的所有对象的所有权转移给 u2
REASSIGN OWNED BY u1 TO u2;
DROP OWNED
删除目标角色拥有的对象,并移除授予该角色的所有权限。
语法:
sql
DROP OWNED BY role_name [, ...] [ CASCADE | RESTRICT ]
示例:
sql
-- 删除 u1 拥有的对象并移除其权限
DROP OWNED BY u1;
DROP ROLE
删除数据库角色。
语法:
sql
DROP ROLE [ IF EXISTS ] name [, ...]
示例:
sql
-- 删除角色(需先执行 REASSIGN OWNED 和 DROP OWNED)
DROP ROLE u1;
Demo 简单示例
以下是一个完整的 Node.js 示例,演示了 PostgreSQL 权限管理的核心操作。
运行说明
环境要求:
- Node.js 16+
- PostgreSQL 14+
pgnpm 包
安装依赖:
bash
npm init -y
npm install pg
启动 PostgreSQL:
bash
# 确保 PostgreSQL 服务已启动
pg_ctl -D /usr/local/var/postgres start
运行脚本:
bash
node index.js
代码实现
js
const { Client } = require('pg');
// 数据库连接配置
const config = {
host: 'localhost',
port: 5432,
database: 'postgres',
user: 'postgres',
password: 'postgres'
};
// 辅助函数:执行 SQL 并打印结果
async function executeSQL(client, sql, label) {
console.log(`\n--- ${label} ---`);
console.log(`SQL: ${sql}`);
try {
const res = await client.query(sql);
if (res.rows && res.rows.length > 0) {
console.log('结果:', JSON.stringify(res.rows, null, 2));
} else {
console.log('执行成功 (无返回数据)');
}
return res;
} catch (err) {
console.error(`错误: ${err.message}`);
throw err;
}
}
async function main() {
const client = new Client(config);
await client.connect();
console.log('已连接到 PostgreSQL');
try {
// ============================================================
// 1. 创建测试数据库和 Schema
// ============================================================
await executeSQL(client, 'CREATE DATABASE demo_db', '创建数据库');
await executeSQL(client, '\\c demo_db', '切换到 demo_db');
// 注意: \c 是 psql 元命令,在 node-pg 中需使用单独连接
// 重新连接到 demo_db
const demoClient = new Client({ ...config, database: 'demo_db' });
await demoClient.connect();
console.log('已连接到 demo_db');
await executeSQL(demoClient, 'CREATE SCHEMA app_schema', '创建 Schema');
// ============================================================
// 2. 创建角色(无登录权限)和用户(有登录权限)
// ============================================================
await executeSQL(demoClient,
'CREATE ROLE app_readonly_role NOLOGIN',
'创建只读角色 (NOLOGIN)'
);
await executeSQL(demoClient,
'CREATE USER app_user WITH PASSWORD \'test123\'',
'创建用户 (默认 LOGIN)'
);
// ============================================================
// 3. 创建测试表
// ============================================================
await executeSQL(demoClient,
`CREATE TABLE app_schema.users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
)`,
'创建测试表'
);
await executeSQL(demoClient,
`INSERT INTO app_schema.users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com')`,
'插入测试数据'
);
// ============================================================
// 4. 授予权限
// ============================================================
// 4.1 Schema 使用权
await executeSQL(demoClient,
'GRANT USAGE ON SCHEMA app_schema TO app_readonly_role',
'授予 Schema USAGE 权限'
);
// 4.2 表查询权限
await executeSQL(demoClient,
'GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly_role',
'授予表 SELECT 权限'
);
// 4.3 数据库连接权限
await executeSQL(demoClient,
'GRANT CONNECT ON DATABASE demo_db TO app_user',
'授予数据库 CONNECT 权限'
);
// 4.4 将角色授予用户
await executeSQL(demoClient,
'GRANT app_readonly_role TO app_user',
'将只读角色授予用户'
);
// ============================================================
// 5. 验证权限
// ============================================================
// 5.1 查看角色的权限
await executeSQL(demoClient,
`SELECT
r.rolname,
r.rolcanlogin,
r.rolinherit
FROM pg_roles r
WHERE r.rolname IN ('app_readonly_role', 'app_user')`,
'查看角色属性'
);
// 5.2 查看表级权限
await executeSQL(demoClient,
`SELECT
grantee,
table_schema,
table_name,
privilege_type
FROM information_schema.table_privileges
WHERE grantee = 'app_readonly_role'`,
'查看表级权限'
);
// ============================================================
// 6. 测试只读用户连接
// ============================================================
console.log('\n--- 测试只读用户连接 ---');
const readOnlyClient = new Client({
...config,
database: 'demo_db',
user: 'app_user',
password: 'test123'
});
try {
await readOnlyClient.connect();
console.log('只读用户连接成功');
// 查询数据(应该成功)
const queryRes = await readOnlyClient.query(
'SELECT * FROM app_schema.users'
);
console.log('查询结果:', queryRes.rows);
// 尝试写入数据(应该失败)
try {
await readOnlyClient.query(
'INSERT INTO app_schema.users (name, email) VALUES ($1, $2)',
['Charlie', 'charlie@example.com']
);
console.log('⚠️ 警告: 只读用户居然可以写入数据!');
} catch (writeErr) {
console.log('✅ 写入被拒绝 (符合预期):', writeErr.message);
}
await readOnlyClient.end();
} catch (connErr) {
console.error('只读用户连接失败:', connErr.message);
}
// ============================================================
// 7. 清理资源 (演示 REASSIGN OWNED + DROP OWNED + DROP ROLE)
// ============================================================
console.log('\n--- 清理资源 ---');
// 7.1 将 app_user 拥有的对象转移给 postgres
await executeSQL(demoClient,
'REASSIGN OWNED BY app_user TO postgres',
'转移所有权 (REASSIGN OWNED)'
);
// 7.2 删除 app_user 的权限和残留对象
await executeSQL(demoClient,
'DROP OWNED BY app_user',
'删除权限和残留对象 (DROP OWNED)'
);
// 7.3 删除角色和用户
await executeSQL(demoClient,
'DROP ROLE app_readonly_role',
'删除只读角色'
);
await executeSQL(demoClient,
'DROP USER app_user',
'删除用户'
);
// 7.4 删除测试表
await executeSQL(demoClient,
'DROP TABLE app_schema.users CASCADE',
'删除测试表'
);
await executeSQL(demoClient,
'DROP SCHEMA app_schema CASCADE',
'删除 Schema'
);
await demoClient.end();
console.log('\n✅ 所有测试完成,资源已清理');
} catch (err) {
console.error('❌ 执行失败:', err);
} finally {
await client.end();
}
}
main().catch(console.error);
PostgreSQL 原生指令对照
上述 Node.js 代码对应的 PostgreSQL 原生 SQL 指令如下:
sql
-- ============================================================
-- 1. 创建数据库和 Schema
-- ============================================================
CREATE DATABASE demo_db;
\c demo_db
CREATE SCHEMA app_schema;
-- ============================================================
-- 2. 创建角色和用户
-- ============================================================
CREATE ROLE app_readonly_role NOLOGIN;
CREATE USER app_user WITH PASSWORD 'test123';
-- ============================================================
-- 3. 创建测试表
-- ============================================================
CREATE TABLE app_schema.users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
);
INSERT INTO app_schema.users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');
-- ============================================================
-- 4. 授予权限
-- ============================================================
GRANT USAGE ON SCHEMA app_schema TO app_readonly_role;
GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly_role;
GRANT CONNECT ON DATABASE demo_db TO app_user;
GRANT app_readonly_role TO app_user;
-- ============================================================
-- 5. 验证权限
-- ============================================================
SELECT rolname, rolcanlogin, rolinherit FROM pg_roles
WHERE rolname IN ('app_readonly_role', 'app_user');
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.table_privileges
WHERE grantee = 'app_readonly_role';
-- ============================================================
-- 6. 测试只读用户
-- ============================================================
-- 以 app_user 连接后执行:
SELECT * FROM app_schema.users; -- 成功
INSERT INTO app_schema.users (name, email) VALUES
('Charlie', 'charlie@example.com'); -- 失败 (无权限)
-- ============================================================
-- 7. 清理资源
-- ============================================================
REASSIGN OWNED BY app_user TO postgres;
DROP OWNED BY app_user;
DROP ROLE app_readonly_role;
DROP USER app_user;
DROP TABLE app_schema.users CASCADE;
DROP SCHEMA app_schema CASCADE;
技术点总结
| 技术点 | 说明 |
|---|---|
| Role vs User | CREATE ROLE 默认 NOLOGIN,CREATE USER 默认 LOGIN |
| 权限层级 | 数据库连接 → Schema → 表 → 列/行,逐层授权 |
| 角色继承 | 通过 GRANT role TO member 实现权限继承 |
| 预定义角色 | pg_read_all_data、pg_write_all_data(PG 14+)、pg_monitor 等 |
| PUBLIC 伪角色 | 代表所有用户,回收权限时需同时从 PUBLIC 回收 |
| 删除角色 | 必须按 REASSIGN OWNED → DROP OWNED → DROP ROLE 顺序执行 |
多语言示例
以下分别使用 Go、Python 和 Java 实现了与 Node.js 示例完全相同的 PostgreSQL 权限管理演示逻辑:创建数据库、Schema、角色、用户、表,授予权限,验证只读用户行为,最后清理资源。
Go 示例
运行说明
环境要求:
- Go 1.18+
- PostgreSQL 14+
- 依赖库:
github.com/lib/pq(PostgreSQL 驱动)
安装依赖:
bash
go mod init demo
go get github.com/lib/pq
启动 PostgreSQL:
bash
pg_ctl -D /usr/local/var/postgres start
运行脚本:
bash
go run main.go
代码实现
go
package main
import (
"database/sql"
"fmt"
"log"
"strings"
_ "github.com/lib/pq"
)
// 数据库连接配置
const (
host = "localhost"
port = 5432
user = "postgres"
password = "postgres"
dbname = "postgres"
)
// 辅助函数:执行 SQL 并打印结果
func execSQL(db *sql.DB, sqlStr string, label string) error {
fmt.Printf("\n--- %s ---\n", label)
fmt.Printf("SQL: %s\n", sqlStr)
_, err := db.Exec(sqlStr)
if err != nil {
log.Printf("错误: %v", err)
return err
}
fmt.Println("执行成功")
return nil
}
// 查询并打印结果
func querySQL(db *sql.DB, sqlStr string, label string) error {
fmt.Printf("\n--- %s ---\n", label)
fmt.Printf("SQL: %s\n", sqlStr)
rows, err := db.Query(sqlStr)
if err != nil {
log.Printf("错误: %v", err)
return err
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
vals := make([]interface{}, len(cols))
for i := range vals {
vals[i] = new(interface{})
}
rows.Scan(vals...)
fmt.Printf("结果: %v\n", vals)
}
return nil
}
func main() {
// 连接到 postgres 默认库
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
client, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer client.Close()
if err := client.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("已连接到 PostgreSQL")
// 1. 创建测试数据库
if err := execSQL(client, "CREATE DATABASE demo_db", "创建数据库"); err != nil {
log.Fatal(err)
}
// 重新连接到 demo_db
demoConnStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=demo_db sslmode=disable",
host, port, user, password)
demoDB, err := sql.Open("postgres", demoConnStr)
if err != nil {
log.Fatal(err)
}
defer demoDB.Close()
if err := demoDB.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("已连接到 demo_db")
// 2. 创建 Schema
if err := execSQL(demoDB, "CREATE SCHEMA app_schema", "创建 Schema"); err != nil {
log.Fatal(err)
}
// 3. 创建角色(无登录)和用户(有登录)
if err := execSQL(demoDB, "CREATE ROLE app_readonly_role NOLOGIN", "创建只读角色 (NOLOGIN)"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "CREATE USER app_user WITH PASSWORD 'test123'", "创建用户 (默认 LOGIN)"); err != nil {
log.Fatal(err)
}
// 4. 创建测试表
createTableSQL := `
CREATE TABLE app_schema.users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
)
`
if err := execSQL(demoDB, createTableSQL, "创建测试表"); err != nil {
log.Fatal(err)
}
insertSQL := `INSERT INTO app_schema.users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com')`
if err := execSQL(demoDB, insertSQL, "插入测试数据"); err != nil {
log.Fatal(err)
}
// 5. 授予权限
if err := execSQL(demoDB, "GRANT USAGE ON SCHEMA app_schema TO app_readonly_role", "授予 Schema USAGE 权限"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly_role", "授予表 SELECT 权限"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "GRANT CONNECT ON DATABASE demo_db TO app_user", "授予数据库 CONNECT 权限"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "GRANT app_readonly_role TO app_user", "将只读角色授予用户"); err != nil {
log.Fatal(err)
}
// 6. 验证权限
if err := querySQL(demoDB, `SELECT rolname, rolcanlogin, rolinherit FROM pg_roles
WHERE rolname IN ('app_readonly_role', 'app_user')`, "查看角色属性"); err != nil {
log.Fatal(err)
}
if err := querySQL(demoDB, `SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.table_privileges WHERE grantee = 'app_readonly_role'`, "查看表级权限"); err != nil {
log.Fatal(err)
}
// 7. 测试只读用户连接
fmt.Println("\n--- 测试只读用户连接 ---")
readOnlyConnStr := fmt.Sprintf("host=%s port=%d user=app_user password=test123 dbname=demo_db sslmode=disable",
host, port)
roClient, err := sql.Open("postgres", readOnlyConnStr)
if err != nil {
log.Fatal(err)
}
defer roClient.Close()
if err := roClient.Ping(); err != nil {
log.Printf("只读用户连接失败: %v", err)
} else {
fmt.Println("只读用户连接成功")
// 查询
rows, err := roClient.Query("SELECT * FROM app_schema.users")
if err != nil {
log.Printf("查询失败: %v", err)
} else {
for rows.Next() {
var id int
var name, email string
rows.Scan(&id, &name, &email)
fmt.Printf("查询结果: id=%d, name=%s, email=%s\n", id, name, email)
}
rows.Close()
}
// 尝试写入(应失败)
_, err = roClient.Exec("INSERT INTO app_schema.users (name, email) VALUES ('Charlie', 'charlie@example.com')")
if err != nil {
fmt.Println("✅ 写入被拒绝 (符合预期):", err)
} else {
fmt.Println("⚠️ 警告: 只读用户居然可以写入数据!")
}
}
// 8. 清理资源
fmt.Println("\n--- 清理资源 ---")
if err := execSQL(demoDB, "REASSIGN OWNED BY app_user TO postgres", "转移所有权 (REASSIGN OWNED)"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "DROP OWNED BY app_user", "删除权限和残留对象 (DROP OWNED)"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "DROP ROLE app_readonly_role", "删除只读角色"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "DROP USER app_user", "删除用户"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "DROP TABLE app_schema.users CASCADE", "删除测试表"); err != nil {
log.Fatal(err)
}
if err := execSQL(demoDB, "DROP SCHEMA app_schema CASCADE", "删除 Schema"); err != nil {
log.Fatal(err)
}
fmt.Println("\n✅ 所有测试完成,资源已清理")
}
代码说明
- 驱动 :使用
github.com/lib/pq,这是 Go 最常用的 PostgreSQL 驱动,支持database/sql标准接口。 - 连接管理 :先连接
postgres默认库创建demo_db,再切换连接至新库执行后续操作。 - 权限操作 :
execSQL封装了Exec执行 DDL/DML;querySQL使用Query查询并打印结果。 - 错误处理:每个步骤检查错误并立即退出,便于调试。
- 资源清理 :按标准流程
REASSIGN OWNED→DROP OWNED→DROP ROLE/USER→ 删除表/Schema。
技术点总结
| 技术点 | 说明 |
|---|---|
| 数据库驱动 | lib/pq 实现 database/sql 驱动,支持连接池和上下文 |
| 连接字符串 | 使用 key=value 格式,指定 sslmode=disable 简化测试 |
| 事务处理 | 未显式使用事务,但所有操作均为原子 SQL 语句 |
| 错误处理 | 显式检查 error,符合 Go 惯用法 |
| 资源释放 | 使用 defer 确保连接和 rows 关闭 |
Python 示例
运行说明
环境要求:
- Python 3.8+
- PostgreSQL 14+
- 依赖库:
psycopg2-binary(推荐)或psycopg2
安装依赖:
bash
pip install psycopg2-binary
启动 PostgreSQL:
bash
pg_ctl -D /usr/local/var/postgres start
运行脚本:
bash
python main.py
代码实现
python
import psycopg2
import psycopg2.extras
import sys
# 数据库连接配置
config = {
'host': 'localhost',
'port': 5432,
'user': 'postgres',
'password': 'postgres',
'database': 'postgres'
}
def execute_sql(conn, sql, label):
"""执行 SQL 并打印结果"""
print(f"\n--- {label} ---")
print(f"SQL: {sql}")
cur = conn.cursor()
try:
cur.execute(sql)
conn.commit()
print("执行成功")
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
conn.rollback()
raise
finally:
cur.close()
def query_sql(conn, sql, label):
"""查询并打印结果"""
print(f"\n--- {label} ---")
print(f"SQL: {sql}")
cur = conn.cursor()
try:
cur.execute(sql)
rows = cur.fetchall()
if rows:
for row in rows:
print(f"结果: {row}")
else:
print("无数据")
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
raise
finally:
cur.close()
def main():
# 连接到 postgres 默认库
try:
conn = psycopg2.connect(**config)
conn.autocommit = False
except Exception as e:
print(f"连接失败: {e}", file=sys.stderr)
sys.exit(1)
print("已连接到 PostgreSQL")
try:
# 1. 创建测试数据库
execute_sql(conn, "CREATE DATABASE demo_db", "创建数据库")
# 重新连接到 demo_db
demo_config = config.copy()
demo_config['database'] = 'demo_db'
demo_conn = psycopg2.connect(**demo_config)
demo_conn.autocommit = False
print("已连接到 demo_db")
# 2. 创建 Schema
execute_sql(demo_conn, "CREATE SCHEMA app_schema", "创建 Schema")
# 3. 创建角色和用户
execute_sql(demo_conn, "CREATE ROLE app_readonly_role NOLOGIN", "创建只读角色 (NOLOGIN)")
execute_sql(demo_conn, "CREATE USER app_user WITH PASSWORD 'test123'", "创建用户 (默认 LOGIN)")
# 4. 创建测试表
create_table = """
CREATE TABLE app_schema.users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
)
"""
execute_sql(demo_conn, create_table, "创建测试表")
insert_sql = "INSERT INTO app_schema.users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com')"
execute_sql(demo_conn, insert_sql, "插入测试数据")
# 5. 授予权限
execute_sql(demo_conn, "GRANT USAGE ON SCHEMA app_schema TO app_readonly_role", "授予 Schema USAGE 权限")
execute_sql(demo_conn, "GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly_role", "授予表 SELECT 权限")
execute_sql(demo_conn, "GRANT CONNECT ON DATABASE demo_db TO app_user", "授予数据库 CONNECT 权限")
execute_sql(demo_conn, "GRANT app_readonly_role TO app_user", "将只读角色授予用户")
# 6. 验证权限
query_sql(demo_conn, "SELECT rolname, rolcanlogin, rolinherit FROM pg_roles WHERE rolname IN ('app_readonly_role', 'app_user')", "查看角色属性")
query_sql(demo_conn, "SELECT grantee, table_schema, table_name, privilege_type FROM information_schema.table_privileges WHERE grantee = 'app_readonly_role'", "查看表级权限")
# 7. 测试只读用户连接
print("\n--- 测试只读用户连接 ---")
ro_config = {
'host': 'localhost',
'port': 5432,
'user': 'app_user',
'password': 'test123',
'database': 'demo_db'
}
try:
ro_conn = psycopg2.connect(**ro_config)
print("只读用户连接成功")
cur = ro_conn.cursor()
cur.execute("SELECT * FROM app_schema.users")
rows = cur.fetchall()
for row in rows:
print(f"查询结果: {row}")
cur.close()
# 尝试写入
try:
cur = ro_conn.cursor()
cur.execute("INSERT INTO app_schema.users (name, email) VALUES ('Charlie', 'charlie@example.com')")
ro_conn.commit()
print("⚠️ 警告: 只读用户居然可以写入数据!")
except Exception as write_err:
print(f"✅ 写入被拒绝 (符合预期): {write_err}")
finally:
cur.close()
ro_conn.close()
except Exception as conn_err:
print(f"只读用户连接失败: {conn_err}")
# 8. 清理资源
print("\n--- 清理资源 ---")
execute_sql(demo_conn, "REASSIGN OWNED BY app_user TO postgres", "转移所有权 (REASSIGN OWNED)")
execute_sql(demo_conn, "DROP OWNED BY app_user", "删除权限和残留对象 (DROP OWNED)")
execute_sql(demo_conn, "DROP ROLE app_readonly_role", "删除只读角色")
execute_sql(demo_conn, "DROP USER app_user", "删除用户")
execute_sql(demo_conn, "DROP TABLE app_schema.users CASCADE", "删除测试表")
execute_sql(demo_conn, "DROP SCHEMA app_schema CASCADE", "删除 Schema")
demo_conn.close()
print("\n✅ 所有测试完成,资源已清理")
except Exception as e:
print(f"执行失败: {e}", file=sys.stderr)
sys.exit(1)
finally:
conn.close()
if __name__ == "__main__":
main()
代码说明
- 驱动 :使用
psycopg2(二进制版),这是 Python 最成熟稳定的 PostgreSQL 适配器。 - 连接管理 :通过字典传递连接参数,支持
autocommit模式(此处显式关闭,手动提交)。 - 游标使用:每个操作创建新游标,执行后关闭,避免资源泄漏。
- 异常处理:捕获异常并回滚事务,确保数据一致性。
- 日志输出 :使用
print输出过程信息,便于观察。
技术点总结
| 技术点 | 说明 |
|---|---|
| 数据库驱动 | psycopg2 支持 Python DB-API 2.0,提供连接池和异步选项 |
| 事务管理 | 默认自动提交关闭,需显式 commit() 或 rollback() |
| 参数化查询 | 示例中使用字符串拼接(仅演示),生产环境需使用 %s 占位符 |
| 游标迭代 | fetchall() 获取所有结果,适合小数据集 |
| 资源释放 | 使用 close() 释放游标和连接,或使用上下文管理器 |
Java 示例
运行说明
环境要求:
- JDK 11+
- PostgreSQL 14+
- 依赖库:PostgreSQL JDBC 驱动
org.postgresql:postgresql:42.7.2
构建与运行(使用 Maven):
创建 pom.xml:
xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>pg-demo</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
</dependency>
</dependencies>
</project>
编译运行:
bash
mvn clean compile
mvn exec:java -Dexec.mainClass="com.example.Main"
或直接使用 javac 和 java(需将 JAR 加入 classpath)。
启动 PostgreSQL:
bash
pg_ctl -D /usr/local/var/postgres start
代码实现
java
package com.example;
import java.sql.*;
import java.util.Properties;
public class Main {
private static final String HOST = "localhost";
private static final int PORT = 5432;
private static final String USER = "postgres";
private static final String PASSWORD = "postgres";
private static final String DBNAME = "postgres";
// 辅助:执行 SQL(无结果集)
private static void execSQL(Connection conn, String sql, String label) throws SQLException {
System.out.println("\n--- " + label + " ---");
System.out.println("SQL: " + sql);
try (Statement stmt = conn.createStatement()) {
stmt.execute(sql);
System.out.println("执行成功");
} catch (SQLException e) {
System.err.println("错误: " + e.getMessage());
throw e;
}
}
// 辅助:执行查询并打印结果
private static void querySQL(Connection conn, String sql, String label) throws SQLException {
System.out.println("\n--- " + label + " ---");
System.out.println("SQL: " + sql);
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
while (rs.next()) {
StringBuilder sb = new StringBuilder("结果: ");
for (int i = 1; i <= colCount; i++) {
if (i > 1) sb.append(", ");
sb.append(meta.getColumnName(i)).append("=").append(rs.getString(i));
}
System.out.println(sb.toString());
}
} catch (SQLException e) {
System.err.println("错误: " + e.getMessage());
throw e;
}
}
public static void main(String[] args) {
Properties props = new Properties();
props.setProperty("user", USER);
props.setProperty("password", PASSWORD);
props.setProperty("ssl", "false");
String url = String.format("jdbc:postgresql://%s:%d/%s", HOST, PORT, DBNAME);
try (Connection conn = DriverManager.getConnection(url, props)) {
conn.setAutoCommit(false);
System.out.println("已连接到 PostgreSQL");
// 1. 创建数据库
execSQL(conn, "CREATE DATABASE demo_db", "创建数据库");
conn.commit();
// 切换到 demo_db
String demoUrl = String.format("jdbc:postgresql://%s:%d/demo_db", HOST, PORT);
try (Connection demoConn = DriverManager.getConnection(demoUrl, props)) {
demoConn.setAutoCommit(false);
System.out.println("已连接到 demo_db");
// 2. 创建 Schema
execSQL(demoConn, "CREATE SCHEMA app_schema", "创建 Schema");
// 3. 创建角色和用户
execSQL(demoConn, "CREATE ROLE app_readonly_role NOLOGIN", "创建只读角色 (NOLOGIN)");
execSQL(demoConn, "CREATE USER app_user WITH PASSWORD 'test123'", "创建用户 (默认 LOGIN)");
// 4. 创建表
String createTable = "CREATE TABLE app_schema.users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)";
execSQL(demoConn, createTable, "创建测试表");
String insert = "INSERT INTO app_schema.users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com')";
execSQL(demoConn, insert, "插入测试数据");
// 5. 授予权限
execSQL(demoConn, "GRANT USAGE ON SCHEMA app_schema TO app_readonly_role", "授予 Schema USAGE 权限");
execSQL(demoConn, "GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO app_readonly_role", "授予表 SELECT 权限");
execSQL(demoConn, "GRANT CONNECT ON DATABASE demo_db TO app_user", "授予数据库 CONNECT 权限");
execSQL(demoConn, "GRANT app_readonly_role TO app_user", "将只读角色授予用户");
demoConn.commit();
// 6. 验证权限
querySQL(demoConn, "SELECT rolname, rolcanlogin, rolinherit FROM pg_roles WHERE rolname IN ('app_readonly_role', 'app_user')", "查看角色属性");
querySQL(demoConn, "SELECT grantee, table_schema, table_name, privilege_type FROM information_schema.table_privileges WHERE grantee = 'app_readonly_role'", "查看表级权限");
// 7. 测试只读用户
System.out.println("\n--- 测试只读用户连接 ---");
Properties roProps = new Properties();
roProps.setProperty("user", "app_user");
roProps.setProperty("password", "test123");
roProps.setProperty("ssl", "false");
String roUrl = String.format("jdbc:postgresql://%s:%d/demo_db", HOST, PORT);
try (Connection roConn = DriverManager.getConnection(roUrl, roProps)) {
System.out.println("只读用户连接成功");
try (Statement st = roConn.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM app_schema.users")) {
while (rs.next()) {
System.out.printf("查询结果: id=%d, name=%s, email=%s%n",
rs.getInt("id"), rs.getString("name"), rs.getString("email"));
}
}
// 尝试写入
try (Statement st = roConn.createStatement()) {
st.executeUpdate("INSERT INTO app_schema.users (name, email) VALUES ('Charlie', 'charlie@example.com')");
System.out.println("⚠️ 警告: 只读用户居然可以写入数据!");
} catch (SQLException writeErr) {
System.out.println("✅ 写入被拒绝 (符合预期): " + writeErr.getMessage());
}
} catch (SQLException connErr) {
System.err.println("只读用户连接失败: " + connErr.getMessage());
}
// 8. 清理资源
System.out.println("\n--- 清理资源 ---");
execSQL(demoConn, "REASSIGN OWNED BY app_user TO postgres", "转移所有权 (REASSIGN OWNED)");
execSQL(demoConn, "DROP OWNED BY app_user", "删除权限和残留对象 (DROP OWNED)");
execSQL(demoConn, "DROP ROLE app_readonly_role", "删除只读角色");
execSQL(demoConn, "DROP USER app_user", "删除用户");
execSQL(demoConn, "DROP TABLE app_schema.users CASCADE", "删除测试表");
execSQL(demoConn, "DROP SCHEMA app_schema CASCADE", "删除 Schema");
demoConn.commit();
System.out.println("\n✅ 所有测试完成,资源已清理");
} catch (SQLException e) {
System.err.println("在 demo_db 中出错: " + e.getMessage());
e.printStackTrace();
}
} catch (SQLException e) {
System.err.println("主连接错误: " + e.getMessage());
e.printStackTrace();
}
}
}
代码说明
- 驱动 :使用 PostgreSQL JDBC 驱动(
org.postgresql.Driver),通过DriverManager获取连接。 - 连接参数 :通过
Properties设置用户名、密码和 SSL 选项。 - 事务控制 :显式设置
setAutoCommit(false),手动commit()确保操作原子性。 - 资源管理 :使用 try-with-resources 自动关闭
Statement、ResultSet和Connection。 - 异常处理 :捕获
SQLException并打印,确保流程清晰。
技术点总结
| 技术点 | 说明 |
|---|---|
| JDBC 驱动 | 标准 Java 数据库连接,支持所有 SQL 操作 |
| 连接字符串 | jdbc:postgresql://host:port/db 格式 |
| 事务 | 关闭自动提交,手动控制事务边界 |
| 资源释放 | try-with-resources 确保资源自动关闭 |
| 元数据 | 使用 ResultSetMetaData 动态获取列信息 |
多语言对比
| 对比维度 | Node.js (参考) | Go | Python | Java |
|---|---|---|---|---|
| 驱动/库 | pg (npm) |
github.com/lib/pq |
psycopg2-binary |
org.postgresql:postgresql |
| 连接方式 | new Client(config) |
sql.Open("postgres", connStr) |
psycopg2.connect(**config) |
DriverManager.getConnection(url, props) |
| 事务控制 | 默认自动提交,可手动 BEGIN/COMMIT |
默认自动提交,可 Begin() |
默认自动提交,可设置 autocommit=False |
默认自动提交,可 setAutoCommit(false) |
| 错误处理 | 回调或 try/catch |
显式 if err != nil |
try/except |
try/catch (SQLException) |
| 资源释放 | client.end() |
defer rows.Close() |
cur.close() / conn.close() |
try-with-resources 自动关闭 |
| SQL 执行 | client.query() / client.query() |
db.Exec() / db.Query() |
cur.execute() / cur.fetchall() |
stmt.execute() / stmt.executeQuery() |
| 连接池 | 内置 | 通过 sql.DB 内置连接池 |
需额外库(如 psycopg2.pool) |
通过 HikariCP 等第三方库 |
| 参数化查询 | $1, $2 占位符 |
$1, $2 占位符 |
%s 或 %(name)s |
? 或 $1(驱动支持) |
| 类型映射 | 自动映射 JavaScript 类型 | 映射 Go 基本类型 | 映射 Python 类型 | 映射 Java 类型(getString, getInt 等) |
| 适用场景 | 全栈 JavaScript 生态,快速开发 | 高并发、云原生微服务 | 数据分析、快速原型、脚本 | 企业级应用、Spring 生态 |
各语言实现均遵循相同的 PostgreSQL 权限管理逻辑,开发者可根据项目技术栈选择合适的语言。所有示例均完整可运行,并包含资源清理环节,确保测试环境无残留。
项目难点与解决方案
核心难点
PostgreSQL 权限体系的层次化设计 和角色继承机制构成了主要的理解门槛。新手 DBA 常常遇到以下问题:
- 权限排查困难 :用户拥有数据库连接权限,但查询表时报错
permission denied for schema,难以定位是哪一层权限缺失 - PUBLIC 隐式授权 :回收了特定用户的权限后,用户仍能访问,因为
PUBLIC的默认授权未被回收 - 角色删除失败 :直接
DROP ROLE报错cannot be dropped because some objects depend on it,不清楚需要先处理哪些依赖
解决方案
- 分层排查法 :按照 实例 → 数据库 → Schema → 表 → 列/行 的顺序逐层检查权限,使用
information_schema.table_privileges和pg_roles系统表辅助排查 - PUBLIC 权限回收 :在创建数据库之初,通过修改
template1回收PUBLIC的默认权限,从源头规避问题 - 标准化删除流程 :严格遵循
REASSIGN OWNED→DROP OWNED→DROP ROLE的三步流程,并在每个相关数据库中执行
广度
本文覆盖了 PostgreSQL 权限管理的完整知识体系,包括:
- 权限层级架构(实例 → 数据库 → Schema → 表 → 列 → 行)
- 角色与用户的区别与联系
- 角色继承机制
- 预定义角色(PostgreSQL 10~17)
- PUBLIC 伪角色的特殊性
- 角色删除的标准流程
- 权限审计与巡检(
pg_permissions扩展)
深度
深入剖析了以下关键技术细节:
CREATE ROLE与CREATE USER的本质差异(仅LOGIN属性不同)REASSIGN OWNED与DROP OWNED的协同工作机制及执行顺序pg_read_all_data与pg_write_all_data的引入版本(PostgreSQL 14)及BYPASSRLS注意事项- 权限继承链在
NOINHERIT处的终止行为
复杂度
PostgreSQL 权限管理的复杂度主要体现在:
- 多层次嵌套:权限检查涉及 6 个层级,每层都有独立的授权模型
- 隐式行为 :
PUBLIC的默认授权、角色的自动继承等隐式行为增加了理解成本 - 删除操作的依赖链:角色删除需要处理所有权转移和权限回收,涉及多个数据库的跨库操作
官方文档
- PostgreSQL 官方文档: 数据库角色
- PostgreSQL 官方文档: 角色属性
- PostgreSQL 官方文档: 角色成员关系
- PostgreSQL 官方文档: 预定义角色
- PostgreSQL 官方文档: 删除角色
- PostgreSQL 官方文档: GRANT
- PostgreSQL 官方文档: REVOKE
- PostgreSQL 官方文档: REASSIGN OWNED
- PostgreSQL 官方文档: DROP OWNED
- PostgreSQL 官方文档: ALTER DEFAULT PRIVILEGES
参考链接
总结
PostgreSQL 的权限管理体系以 Role 为核心概念,通过层次化的权限检查机制(实例认证 → 数据库连接 → Schema 使用 → 表操作 → 列/行级控制)实现了精细的访问控制。Role 与 User 的本质区别仅在于 LOGIN 属性的默认值,这一设计使得 Role 天然适合作为权限模板进行批量授权管理。
在生产环境中,建议遵循以下最佳实践:
- 使用不带
LOGIN的 Role 作为权限集合,再授予给实际的 User - 充分利用预定义角色 (尤其是 PostgreSQL 14+ 的
pg_read_all_data和pg_write_all_data)简化只读/读写用户的配置 - 注意
PUBLIC伪角色的隐式授权 ,回收权限时需同时从PUBLIC回收 - 删除角色时严格遵循
REASSIGN OWNED→DROP OWNED→DROP ROLE的标准流程 - 使用
pg_permissions等工具进行定期的权限审计与巡检
理解并掌握这套权限体系,是保障 PostgreSQL 数据库安全稳定运行的基石。