fix(认证): 原子更新登录限流并统一凭据校验耗时
将失败计数与过期时间放入同一 Redis 事务,避免永久锁定。 不存在的账号执行同成本哈希校验,登录测试强制读取环境口令。
This commit is contained in:
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,
|
||||
)
|
||||
@@ -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:
|
||||
"""解锁与剩余时长。"""
|
||||
|
||||
|
||||
Reference in new issue
Block a user