fix(认证): 原子更新登录限流并统一凭据校验耗时

将失败计数与过期时间放入同一 Redis 事务,避免永久锁定。
不存在的账号执行同成本哈希校验,登录测试强制读取环境口令。
This commit is contained in:
liuwenxin committed 2026-11-03 17:18:36 +08:00
1 parent 22605f4106
commit 67adecbf1b
5 files changed
+97 -8

No files matched your search

@@ -0,0 +1,57 @@
"""不存在的账号仍做哈希校验,但永远不能通过登录。"""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from app.core.exceptions import BadRequestError
from app.services import auth_service
@pytest.mark.parametrize("exists", [True, False])
def test_凭据错误统一校验并记失败(monkeypatch, exists):
user = SimpleNamespace(id=1, password="stored-hash") if exists else None
monkeypatch.setattr(auth_service.user_repository, "get_by_username", lambda *_: user)
monkeypatch.setattr(auth_service.login_limiter, "is_locked", lambda *_: False)
monkeypatch.setattr(auth_service.captcha_service, "verify_and_consume", lambda *_: True)
verify = Mock(return_value=False)
failure = Mock()
record = Mock()
monkeypatch.setattr(auth_service, "verify_password", verify)
monkeypatch.setattr(auth_service.login_limiter, "record_failure", failure)
monkeypatch.setattr(auth_service, "_record", record)
with pytest.raises(BadRequestError, match=auth_service.INVALID_CREDENTIALS):
auth_service.login(
Mock(),
username="unknown",
password="wrong",
captcha_id="id",
captcha_code="ABCD",
ip=None,
user_agent=None,
)
verify.assert_called_once_with(
"wrong", user.password if user else auth_service._DUMMY_PASSWORD_HASH
)
failure.assert_called_once_with("unknown")
assert record.call_args.kwargs["success"] is False
def test_输入匹配假哈希也不能登录(monkeypatch):
monkeypatch.setattr(auth_service.user_repository, "get_by_username", lambda *_: None)
monkeypatch.setattr(auth_service.login_limiter, "is_locked", lambda *_: False)
monkeypatch.setattr(auth_service.captcha_service, "verify_and_consume", lambda *_: True)
monkeypatch.setattr(auth_service, "verify_password", lambda *_: True)
monkeypatch.setattr(auth_service.login_limiter, "record_failure", Mock())
monkeypatch.setattr(auth_service, "_record", Mock())
with pytest.raises(BadRequestError, match=auth_service.INVALID_CREDENTIALS):
auth_service.login(
Mock(),
username="unknown",
password="dummy",
captcha_id="id",
captcha_code="ABCD",
ip=None,
user_agent=None,
)
+20
View File
@@ -5,6 +5,8 @@
正好给慢速撞库留出缝隙。
"""
import pytest
from app.core.config import get_settings
from app.core.constants import REDIS_KEY_LOGIN_FAIL
from app.services import login_limiter
@@ -62,6 +64,24 @@ class TestSlidingWindow:
assert _ttl("u") > 600
class TestAtomicWrite:
"""计数和 TTL 必须同事务生效。"""
def test_EXEC失败时不留下无TTL计数(self, fake_redis, monkeypatch) -> None:
key = REDIS_KEY_LOGIN_FAIL.format(username="u")
pipeline = fake_redis.pipeline(transaction=True)
def fail_execute():
raise RuntimeError("EXEC 失败")
monkeypatch.setattr(pipeline, "execute", fail_execute)
monkeypatch.setattr(fake_redis, "pipeline", lambda **_: pipeline)
with pytest.raises(RuntimeError, match="EXEC 失败"):
login_limiter.record_failure("u")
assert fake_redis.get(key) is None
class TestClearAndRemaining:
"""解锁与剩余时长。"""