fix(认证): 原子更新登录限流并统一凭据校验耗时
将失败计数与过期时间放入同一 Redis 事务,避免永久锁定。 不存在的账号执行同成本哈希校验,登录测试强制读取环境口令。
This commit is contained in:
1 parent
22605f4106
commit
67adecbf1b
5 files changed
+97
-8
No files matched your search
@@ -24,6 +24,10 @@ logger = logging.getLogger(__name__)
|
||||
# 攻击者据此无法判断某个用户名是否真实存在。
|
||||
INVALID_CREDENTIALS = "用户名或密码错误"
|
||||
|
||||
# 与真实库哈希同算法、同轮次的假哈希。用户不存在时也执行一次校验,
|
||||
# 让两条失败路径耗时接近,避免响应时间暴露有效用户名。
|
||||
_DUMMY_PASSWORD_HASH = "$2b$12$T7f7tDu4ljCgkXigiJ0EHea3ik07F0Dqpv2B1rlUF1FljlylH68Um"
|
||||
|
||||
|
||||
def _record(
|
||||
session: Session,
|
||||
@@ -103,7 +107,10 @@ def login(
|
||||
|
||||
# ---- 3. 凭据校验 ----
|
||||
user = user_repository.get_by_username(session, username)
|
||||
if user is None or not verify_password(password, user.password):
|
||||
credentials_ok = verify_password(
|
||||
password, user.password if user is not None else _DUMMY_PASSWORD_HASH
|
||||
)
|
||||
if user is None or not credentials_ok:
|
||||
login_limiter.record_failure(username)
|
||||
_record(
|
||||
session,
|
||||
|
||||
@@ -43,10 +43,13 @@ def record_failure(username: str) -> int:
|
||||
正好给撞库留出缝隙。
|
||||
"""
|
||||
key = _key(username)
|
||||
redis = get_redis()
|
||||
count = int(redis.incr(key))
|
||||
redis.expire(key, get_settings().login_lock_minutes * 60)
|
||||
return count
|
||||
# INCR 和 EXPIRE 必须同事务提交。否则两步之间断连会留下无 TTL 的计数,
|
||||
# 该用户随后会被永久锁定;pipeline(transaction=True) 用 MULTI/EXEC 原子生效。
|
||||
pipe = get_redis().pipeline(transaction=True)
|
||||
pipe.incr(key)
|
||||
pipe.expire(key, get_settings().login_lock_minutes * 60)
|
||||
count, _ = pipe.execute()
|
||||
return int(count)
|
||||
|
||||
|
||||
def clear(username: str) -> None:
|
||||
|
||||
Reference in new issue
Block a user