PostgreSQL触发器无法捕获登录行为,因登录不触发DML且pg_authid不可写;PG 16+可用event trigger监听client_login事件,旧版本需轮询pg_stat_activity。

触发器不能直接捕获 pg_authid 登录行为
PostgreSQL 的 SQL 触发器(CREATE TRIGGER)只能响应表上的 INSERT/UPDATE/DELETE,而用户登录不写入任何用户可访问的表——它发生在系统层,pg_authid 是只读系统目录,且登录过程本身不触发 DML。想靠普通触发器“监听登录”会直接失败。
常见错误现象:ERROR: cannot create trigger on system catalog pg_authid 或触发器完全不执行。
可行路径只有两条:一是用 log_statement = 'all' + 外部解析日志;二是改用 event triggers 捕获 client_login 事件(需 PostgreSQL 16+)。
PostgreSQL 16+:用 event trigger 捕获 client_login
从 16 开始,PostgreSQL 支持 client_login 事件类型,配合 event trigger 可在登录成功后立即执行自定义逻辑。它不依赖表操作,也不修改系统目录,是目前最干净的方案。
实操步骤:
- 先创建一张登录日志表:
CREATE TABLE login_history ( id SERIAL PRIMARY KEY, username TEXT NOT NULL, client_addr INET, backend_pid INTEGER, login_time TIMESTAMPTZ DEFAULT NOW() );
- 再创建函数,注意必须用
SECURITY DEFINER并由超级用户拥有:CREATE OR REPLACE FUNCTION log_client_login() RETURNS event_trigger AS $$ BEGIN INSERT INTO login_history (username, client_addr, backend_pid) SELECT current_user, inet_client_addr(), pg_backend_pid(); END; $$ LANGUAGE plpgsql SECURITY DEFINER;
- 最后绑定事件:
CREATE EVENT TRIGGER log_user_login ON client_login EXECUTE FUNCTION log_client_login();
注意:current_user 在 event trigger 中返回的是实际登录用户名(非会话切换后的角色),但 inet_client_addr() 在本地 Unix 域连接时返回 NULL,需额外判断。
兼容旧版本:用 pg_stat_activity + 定时轮询模拟登录记录
PostgreSQL 15 及更早版本不支持 client_login,只能退而求其次:定期查询 pg_stat_activity,把新出现的活跃会话当作“刚登录”,写入日志表。这不是实时方案,但成本低、兼容性好。
关键点:
- 用
backend_start字段识别“新连接”,而非state_change(后者随查询变化频繁) - 避免重复记录:需用
(pid, backend_start)联合去重,或加ON CONFLICT DO NOTHING - 轮询间隔建议 ≥5 秒,太密会增加系统负载;太长则漏记短连接
- 示例插入语句:
INSERT INTO login_history (username, client_addr, backend_pid, login_time) SELECT usename, client_addr, pid, backend_start FROM pg_stat_activity WHERE backend_start > NOW() - INTERVAL '5 seconds' AND state = 'active' AND usename IS NOT NULL ON CONFLICT (backend_pid, login_time) DO NOTHING;
这个方案无法区分“重连”和“首次登录”,也无法捕获已断开但未及时清理的连接(pg_stat_activity 中 state = 'idle' 的连接可能滞留数分钟)。
别忽略权限与日志留存策略
无论用哪种方式,login_history 表会持续增长,且包含敏感信息(IP、用户名)。容易被忽略的点有:
-
log_client_login函数必须由超级用户拥有并标记SECURITY DEFINER,否则在 event trigger 上下文中无权写入表 - 普通用户默认无法查
pg_stat_activity的全部字段(如client_addr),需显式授权:GRANT SELECT ON pg_stat_activity TO your_app_role; - 生产环境务必加索引:
CREATE INDEX ON login_history (login_time);,否则按时间范围查历史会变慢 - 没有自动归档机制——得自己配
pg_cron或外部脚本定期DELETE或PARTITION旧数据
真实场景里,IP 地址可能被代理覆盖,inet_client_addr() 拿到的是最后一跳地址;如果应用层做了连接池(如 pgbouncer),看到的永远是池子的 IP,不是终端用户。

















