bigywater.tech
Flask · 131

登录失败锁定

登录失败多少次该锁?怎么锁才不会被人拿来封号?响应和时间会不会泄漏账号是否存在?并发请求能不能绕过计数?这篇在真实的 PostgreSQL 上把这些细节逐个验证(只验证防护这一端)。

66 → 0.3 ms
存在与不存在的用户的响应时间差:朴素实现与统一实现
20 → 5
20 个并发错误尝试里真正去校验密码的次数:非原子计数与原子计数(上限 5)
同一句话
统一响应里对不存在的用户、密码错误、账号锁定都返回的消息

锁定与恢复

实测 · 本机执行输出
  连续 5 次错误密码: [401, 401, 401, 401, 401]
  锁定期间用正确的密码 → 401 {'error': 'invalid credentials or account temporarily locked'}
  2 秒后用正确的密码 → 200 {'ok': True}
  成功之后库里的失败计数: 0
  bob 失败 2 次后成功登录一次,计数: 0 (中途成功会清零)

上一篇:密码哈希与密码升级;下一篇:开发模式 Debugger 风险。

第 129 篇讲过用 Flask-Limiter 限流,这一篇讲应用层的账号锁定。

统一的响应

实测 · 本机执行输出
  朴素:不存在 404 / 密码错 401,消息不同
     不存在的用户 → 404 {'error': 'user not found'}
     存在的用户、错误密码 → 401 {'error': 'wrong password'}
  统一:都是 401 与同一句话
     不存在的用户 → 401 {'error': 'invalid credentials or account temporarily locked'}
     存在的用户、错误密码 → 401 {'error': 'invalid credentials or account temporarily locked'}

响应时间

实测 · 本机执行输出
  朴素                         不存在的用户     0.6 ms | 存在的用户(错误密码)   66.3 ms | 差   65.7 ms
  统一(不存在的用户也算哈希)             不存在的用户    66.1 ms | 存在的用户(错误密码)   66.4 ms | 差    0.3 ms

并发与原子计数

实测 · 本机执行输出
  读出再写回(非原子)                     真正去校验了密码的尝试 20 次(上限 5),被拒绝 0 次
  原子 UPDATE ... WHERE fails < 上限 真正去校验了密码的尝试 5 次(上限 5),被拒绝 15 次

并发用 20 个线程同时发请求(各自的 test client 与数据库连接),数据库是自己起的 PostgreSQL。

分步演示

登录失败锁定
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、自己起的 PostgreSQL(非默认端口)、Werkzeug pbkdf2:sha256:600000 哈希
    • 没有验证:渐进式延迟(退避)与验证码的效果、锁定通知邮件、管理员解锁流程、分布式多实例下计数的存放位置(redis)、时间差在真实网络抖动下的可检测性
    京ICP备2026067502号-1京公网安备11010502063487号