PostgreSQL笔记60: 权限与角色管理——从层级体系到最佳实践

概述

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 的核心设计理念是作为权限的集合(权限模板)。在实际生产环境中,推荐的最佳实践是:

  1. 根据业务需求创建多个 不带 LOGIN 属性的 Role,每个 Role 代表一组特定的权限集合(如只读、读写、管理员等)
  2. 将相应的 Role 授予给实际的 User (带 LOGIN 属性的 Role)
  3. 当权限需求发生变化时,只需修改 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 时终止。

对于拥有高权限的角色(如 SUPERUSERCREATEDBCREATEROLE 等),建议设置 NOINHERIT 属性以减少权限提升的风险。

预定义角色

PostgreSQL 提供了一组预定义角色(Predefined Roles),用于简化常见权限管理场景。这些角色在 PostgreSQL 14 及更高版本中得到了显著增强。

以下是常用预定义角色的汇总:

预定义角色 权限说明 引入版本
pg_read_all_data 读取所有表、视图、序列的数据,如同拥有 SELECT 权限;对所有 Schema 拥有 USAGE 权限 PostgreSQL 14
pg_write_all_data 写入所有表、视图、序列的数据,如同拥有 INSERTUPDATEDELETE 权限 PostgreSQL 14
pg_monitor 读取/执行各种监控视图和函数。是 pg_read_all_settingspg_read_all_statspg_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_datapg_write_all_data 不包含 BYPASSRLS 属性。如果数据库中启用了行级安全(RLS),管理员可能需要为被授予这些角色的用户额外设置 BYPASSRLS

在 PostgreSQL 14 之前,要实现只读用户需要逐表授予 SELECT 权限并回收默认权限,操作较为繁琐。PostgreSQL 14 引入的 pg_read_all_datapg_write_all_data 极大地简化了这一流程。

PUBLIC 伪角色

PUBLIC 是一个特殊的伪角色,代表 所有角色 (包括当前已存在和将来可能创建的角色)。每个新创建的用户都会自动成为 PUBLIC 的成员。

关键注意事项:

  • 某些对象类型在创建时会默认授予 PUBLIC 一定的权限
  • 例如,public Schema 默认授予 PUBLICCREATE 权限
  • 回收单个用户的权限时,必须同时从 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

步骤说明:

  1. REASSIGN OWNED BY old_role TO new_role :将 old_role 拥有的所有对象的所有权 转移给 new_role
  2. DROP OWNED BY old_role :删除 old_role 拥有的、尚未转移所有权的对象,并移除授予 old_role 的所有权限
  3. 在每个包含 old_role 所拥有对象的数据库中重复执行上述两步
  4. 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 默认 LOGINCREATE 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 USERCREATE 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+
  • pg npm 包

安装依赖:

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 默认 NOLOGINCREATE USER 默认 LOGIN
权限层级 数据库连接 → Schema → 表 → 列/行,逐层授权
角色继承 通过 GRANT role TO member 实现权限继承
预定义角色 pg_read_all_datapg_write_all_data(PG 14+)、pg_monitor
PUBLIC 伪角色 代表所有用户,回收权限时需同时从 PUBLIC 回收
删除角色 必须按 REASSIGN OWNEDDROP OWNEDDROP 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 OWNEDDROP OWNEDDROP 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"

或直接使用 javacjava(需将 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 自动关闭 StatementResultSetConnection
  • 异常处理 :捕获 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 常常遇到以下问题:

  1. 权限排查困难 :用户拥有数据库连接权限,但查询表时报错 permission denied for schema,难以定位是哪一层权限缺失
  2. PUBLIC 隐式授权 :回收了特定用户的权限后,用户仍能访问,因为 PUBLIC 的默认授权未被回收
  3. 角色删除失败 :直接 DROP ROLE 报错 cannot be dropped because some objects depend on it,不清楚需要先处理哪些依赖

解决方案

  1. 分层排查法 :按照 实例 → 数据库 → Schema → 表 → 列/行 的顺序逐层检查权限,使用 information_schema.table_privilegespg_roles 系统表辅助排查
  2. PUBLIC 权限回收 :在创建数据库之初,通过修改 template1 回收 PUBLIC 的默认权限,从源头规避问题
  3. 标准化删除流程 :严格遵循 REASSIGN OWNEDDROP OWNEDDROP ROLE 的三步流程,并在每个相关数据库中执行

广度

本文覆盖了 PostgreSQL 权限管理的完整知识体系,包括:

  • 权限层级架构(实例 → 数据库 → Schema → 表 → 列 → 行)
  • 角色与用户的区别与联系
  • 角色继承机制
  • 预定义角色(PostgreSQL 10~17)
  • PUBLIC 伪角色的特殊性
  • 角色删除的标准流程
  • 权限审计与巡检(pg_permissions 扩展)

深度

深入剖析了以下关键技术细节:

  • CREATE ROLECREATE USER 的本质差异(仅 LOGIN 属性不同)
  • REASSIGN OWNEDDROP OWNED 的协同工作机制及执行顺序
  • pg_read_all_datapg_write_all_data 的引入版本(PostgreSQL 14)及 BYPASSRLS 注意事项
  • 权限继承链在 NOINHERIT 处的终止行为

复杂度

PostgreSQL 权限管理的复杂度主要体现在:

  • 多层次嵌套:权限检查涉及 6 个层级,每层都有独立的授权模型
  • 隐式行为PUBLIC 的默认授权、角色的自动继承等隐式行为增加了理解成本
  • 删除操作的依赖链:角色删除需要处理所有权转移和权限回收,涉及多个数据库的跨库操作

官方文档

参考链接

总结

PostgreSQL 的权限管理体系以 Role 为核心概念,通过层次化的权限检查机制(实例认证 → 数据库连接 → Schema 使用 → 表操作 → 列/行级控制)实现了精细的访问控制。Role 与 User 的本质区别仅在于 LOGIN 属性的默认值,这一设计使得 Role 天然适合作为权限模板进行批量授权管理。

在生产环境中,建议遵循以下最佳实践:

  1. 使用不带 LOGIN 的 Role 作为权限集合,再授予给实际的 User
  2. 充分利用预定义角色 (尤其是 PostgreSQL 14+ 的 pg_read_all_datapg_write_all_data)简化只读/读写用户的配置
  3. 注意 PUBLIC 伪角色的隐式授权 ,回收权限时需同时从 PUBLIC 回收
  4. 删除角色时严格遵循 REASSIGN OWNEDDROP OWNEDDROP ROLE 的标准流程
  5. 使用 pg_permissions 等工具进行定期的权限审计与巡检

理解并掌握这套权限体系,是保障 PostgreSQL 数据库安全稳定运行的基石。

相关推荐
whcyhhh2 小时前
头歌实践教学平台:大数据存储2023(六)
大数据·数据库·python
Elastic 中国社区官方博客3 小时前
让大模型思考,让小模型执行:在 Elastic Workflows 中拆分 LLM 成本
大数据·运维·数据库·人工智能·elasticsearch·ai
Faith_xzc3 小时前
一条 SQL 顶一条 Flink 链路?Doris Streaming Job 持续导入全景解析
大数据·数据库·sql·flink
IZero074 小时前
Elasticsearch 学习笔记筑基-文档操作篇
笔记·学习·elasticsearch
公爵爱学习5 小时前
安装Ubuntu20.04遇到的问题 简要简述笔记
笔记
这个DBA有点耶6 小时前
多模数据库深度解读:从“多库拼装”到“一库多能”的架构演进
数据库·mysql·dba
程序员夏洛7 小时前
MySQL 默认的事务隔离级别是什么?为什么选择这个级别?
数据库·mysql
Wang's Blog7 小时前
PostgreSQL笔记28: PostgreSQL ACID 特性与实现机制深度解析
大数据·笔记·postgresql
艺术留白7 小时前
MokaTest使用篇-SQL接口
数据库·sql