导读:本期聚焦于阳光创作的《PostgreSQL怎么通过扩展实现密码连续失败后的账户锁定?》,敬请观看详情。放在公网上的PostgreSQL实例经常遭遇暴力破解,认证失败后账户默认还能继续尝试,时间一长安全风险越来越大。这篇文章围绕登录失败锁定账户的扩展实现展开,先说明PostgreSQL原生认证流程缺少自动锁定机制,再介绍利用客户端认证钩子记录失败次数的思路,给出核心C扩展代码和状态表设计。之后配置阈值、锁定时长和解锁方式,并演示从创建扩展到测试生效的完整过程。还会讨论认证阶段操作数据库的限制、共享内存的取舍,以及与fail2ban等外部工具的配合。读完可以按需部署一个轻量的防护模块,避免猜解攻击长时间持续。

PostgreSQL默认只负责校验密码是否正确,并不关心同一个账户已经连续输错多少次。也就是说,如果数据库暴露在公网并且只开启了密码认证,攻击者可以对目标账户持续发起猜测,而数据库不会在中途锁定该账户。这种设计虽然避免了误锁导致服务不可用,但也给暴力破解留下了空间。本文讨论的扩展正是为了解决这一问题:通过C语言编写的认证钩子,在认证阶段记录失败次数,达到阈值后直接拒绝后续尝试。

PostgreSQL怎么通过扩展实现密码连续失败后的账户锁定?

锁定机制的整体设计

实现登录失败锁定,需要先明确三件事:失败次数存储在哪里、何时判定锁定、锁定后如何解除。由于认证失败发生在用户进入数据库之前,此时还没有建立正常的会话事务,因此不能依赖简单的表触发器来实时记录。扩展通常采用共享内存保存计数,或者使用一个辅助进程持久化到磁盘。对于生产环境,如果实例因故障重启,内存中的失败计数会丢失,这是可以接受的折中;如果希望重启后仍保留锁定状态,则需要在锁定期内把关键信息写入文件或unlogged表。

另一个设计要点是锁定判定条件。可以在认证钩子中拿到用户名和客户端地址,如果某个用户连续失败次数超过阈值,就直接返回拒绝,而不再调用后续的密码校验逻辑。锁定状态可以用时间窗口来管理,例如连续失败5次后锁定10分钟,10分钟后自动恢复计数。也可以设置永不自动解锁,必须由管理员手动清除。不同场景对误锁的容忍度不同,建议把阈值和锁定时长都做成GUC参数,方便运维调整。

除了用户维度,还需要考虑客户端地址维度。攻击者可能使用同一个IP遍历多个用户名,也可能使用多个IP集中猜解一个账户。更完善的扩展会同时跟踪用户与IP的组合,或者至少提供按IP锁定的选项。考虑到实现复杂度和性能,第一版可以先只针对数据库用户名做锁定,后续再扩展。

用认证钩子记录失败次数

PostgreSQL提供了一个ClientAuthentication_hook类型,允许扩展在认证过程中插入自定义逻辑。该钩子函数在客户端认证成功或失败后都会触发。我们可以在钩子里检查认证结果,如果失败,则根据端口结构体中的用户名增加失败计数。端口结构体Port包含了database_name、user_name、raddr等信息,足够用于识别账户。

下面是一段简化后的C扩展代码,展示了如何注册钩子并在认证失败时累加计数。为了保持示例可读,这里只展示核心逻辑,完整扩展还需要处理共享内存的初始化和并发安全。

#include <postgres.h>
#include <fmgr.h>
#include <miscadmin.h>
#include <storage/lwlock.h>
#include <storage/shmem.h>
#include <utils/guc.h>
#include <libpq/libpq.h>
#include <utils/timestamp.h>

PG_MODULE_MAGIC;

void _PG_init(void);
static void lock_on_auth(Port *port, int status);
static int max_failures = 5;
static int lock_minutes = 10;

typedef struct AccountLockEntry
{
    int failures;
    TimestampTz last_fail_time;
    bool locked;
} AccountLockEntry;

static AccountLockEntry *account_array = NULL;

void
_PG_init(void)
{
    DefineCustomIntVariable("auth_lock.max_failures",
                            "Maximum failures before locking.",
                            NULL,
                            &max_failures,
                            5, 0, 1000,
                            PGC_SIGHUP, 0,
                            NULL, NULL, NULL);

    DefineCustomIntVariable("auth_lock.lock_minutes",
                            "Minutes to lock account after too many failures.",
                            NULL,
                            &lock_minutes,
                            10, 0, 1440,
                            PGC_SIGHUP, 0,
                            NULL, NULL, NULL);

    if (!process_shared_preload_libraries_in_progress)
        return;

    RequestAddinShmemSpace(sizeof(AccountLockEntry) * 1000);
    RequestNamedLWLockTranche("auth_lock_locks", 1);

    ClientAuthentication_hook = lock_on_auth;
}

static void
lock_on_auth(Port *port, int status)
{
    if (port == NULL || port->user_name == NULL)
        return;

    if (status == STATUS_OK)
        return;

    /* 根据用户名简单映射到数组下标,这里省略哈希实现 */
    int idx = 0;
    for (int i = 0; port->user_name[i] != '\0'; i++)
        idx = (idx + port->user_name[i]) % 1000;

    if (account_array[idx].locked)
    {
        TimestampTz now = GetCurrentTimestamp();
        long secs = (now - account_array[idx].last_fail_time) / 1000000;
        if (secs < lock_minutes * 60)
        {
            ereport(FATAL,
                (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
                 errmsg("account %s is locked due to repeated login failures",
                        port->user_name)));
        }
        else
        {
            account_array[idx].locked = false;
            account_array[idx].failures = 0;
        }
    }

    account_array[idx].failures++;
    account_array[idx].last_fail_time = GetCurrentTimestamp();

    if (account_array[idx].failures >= max_failures)
    {
        account_array[idx].locked = true;
        ereport(FATAL,
            (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
             errmsg("too many login failures, account %s is locked",
                    port->user_name)));
    }
}

代码中的FATAL错误会直接终止当前客户端连接,其他连接不会受到影响。需要注意的是,ClientAuthentication_hook运行在认证阶段,如果在这里调用需要数据库事务的函数会非常危险。示例中仅使用共享内存数组和轻量级锁,避免访问表。生产实现还要用LWLock保护计数更新,防止多个并发连接同时修改同一项导致计数丢失。

哈希映射部分为了演示而做了简化,实际使用中应当根据用户名建立真正的哈希表,否则不同用户可能因为下标碰撞而共享失败计数。例如用户alice和bob经过简单映射可能落到同一个数组位置,导致bob的失败次数被叠加到alice的项里。虽然这不会直接造成安全问题,但可能引发误锁,因此需要完善的哈希函数和冲突处理。

配置参数与解锁方式

扩展编译安装后,需要在postgresql.conf中设置shared_preload_libraries让扩展在服务器启动时加载。示例中通过GUC暴露了auth_lock.max_failures和auth_lock.lock_minutes两个参数,管理员可以用ALTER SYSTEM命令动态调整阈值和锁定时长,也可以通过reload让新配置生效。

ALTER SYSTEM SET shared_preload_libraries = 'auth_lock';
ALTER SYSTEM SET auth_lock.max_failures = 8;
ALTER SYSTEM SET auth_lock.lock_minutes = 15;
SELECT pg_reload_conf();

如果对外提供的是多个应用共用的账户,锁定阈值不宜设置过低,否则一次配置错误就可能让所有应用无法连接。常见做法是为普通账户设置5到10次失败锁定,为高权限账户设置更低的阈值,并同时监控锁定事件。扩展内部可以加入简单日志记录,每次锁定发生时写入服务器日志,便于安全审计。

解锁可以设计为自动和手动两种。自动解锁由时间窗口控制,超过锁定时长后计数清零,这与上面代码中的判断逻辑一致。手动解锁则需要扩展提供一个函数或者通过管理表操作。如果扩展不提供SQL函数,管理员可以重启实例清除共享内存状态,但这样会同时清空所有账户的失败计数。更好的方案是注册一个C函数,接收用户名参数,将该用户对应的项清零并解除锁定。

下面展示一个手动解锁函数的SQL声明和一个简单的调用示例。实际函数需要能够在后端进程中访问共享内存,因此必须在C层实现。

CREATE FUNCTION auth_lock_reset(account_name text)
RETURNS boolean
AS 'MODULE_PATHNAME', 'auth_lock_reset'
LANGUAGE C STRICT;

SELECT auth_lock_reset('app_user');

如果暂时不想编写完整的C扩展,也可以使用log_failures或外部工具记录认证失败日志,再由定时任务分析日志并执行锁操作。不过这种方案通常滞后于实际攻击,而且锁定的动作无法在认证阶段即刻生效。对于要求较高的系统,建议还是采用认证钩子方案。

生产环境注意事项

这一节重点说明扩展部署过程中容易踩到的坑。首先是共享内存的分配。RequestAddinShmemSpace在_PG_init中只能调用一次,并且必须在process_shared_preload_libraries_in_progress为真时进行。如果扩展不是通过shared_preload_libraries加载,这段分配逻辑会失败或导致服务器启动异常。因此部署文档必须明确要求将扩展加入共享预加载库。

其次是并发控制。多个客户端同时认证时,失败计数需要原子更新。PostgreSQL的共享内存不会自动提供线程安全,必须配合LWLock或自旋锁。如果为了简单而忽略锁,计数可能在高并发下被覆盖,攻击者可以利用这一点绕过锁定。建议在每次更新前获取一个独立的锁事务保证串行,或者使用原子指令。

还有一个实际问题是代理或中间件场景下,数据库看到的客户端地址都是同一台代理服务器。此时按IP锁定会误伤所有经过该代理的用户,因此默认只按用户名锁定更合适。如果能够从应用层传递真实客户端地址,再结合port中的raddr进行组合判断,可以做到更细粒度,但这需要额外的信任机制。

最后是测试验证。部署完成后,可以使用psql故意输入错误密码,观察失败次数累积和锁定效果。测试时要避免使用生产账户,最好在临时实例上验证。锁定触发后,即使输入正确密码也应该被拒绝,并返回账户已锁定的错误信息。通过调整参数可以缩短锁定时长,便于快速验证自动解锁是否按预期恢复。

综上,PostgreSQL本身没有内置账户锁定,但通过客户端认证钩子和共享内存可以高效地补上这一层防护。扩展的难点在于认证阶段无法安全访问数据库表,因此状态管理要放在共享内存中,并做好并发保护。阈值、锁定时长和解锁策略需要结合业务场景权衡,避免误锁导致业务中断。配合日志监控和外部工具,可以构建一套相对完整的防暴力破解方案。

PostgreSQL登录失败锁定账户安全修改时间:2026-09-22 03:40:14

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0922/60317.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。