Skip to content

Commit 7176e8d

Browse files
committed
fix: implement atomic increment for login failure tracking to prevent race conditions
1 parent f0ecf61 commit 7176e8d

1 file changed

Lines changed: 42 additions & 30 deletions

File tree

apps/users/serializers/login.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
@desc:
88
"""
99
import base64
10-
import datetime
1110
import json
1211

1312
from captcha.image import ImageCaptcha
@@ -23,7 +22,7 @@
2322
from common.database_model_manage.database_model_manage import DatabaseModelManage
2423
from common.exception.app_exception import AppApiException
2524
from common.utils.common import password_encrypt, get_random_chars
26-
from common.utils.rsa_util import encrypt, decrypt
25+
from common.utils.rsa_util import decrypt
2726
from maxkb.const import CONFIG
2827
from users.models import User
2928

@@ -48,26 +47,35 @@ class LoginResponse(serializers.Serializer):
4847

4948

5049
def record_login_fail(username: str, expire: int = 600):
51-
"""记录登录失败次数"""
50+
"""记录登录失败次数(原子)返回当前失败计数"""
5251
if not username:
53-
return
52+
return 0
5453
fail_key = system_get_key(f'system_{username}')
55-
fail_count = cache.get(fail_key, version=system_version)
56-
if fail_count is None:
54+
try:
55+
fail_count = cache.incr(fail_key, 1, version=system_version)
56+
except ValueError:
57+
# key 不存在,初始化并设置过期
5758
cache.set(fail_key, 1, timeout=expire, version=system_version)
58-
else:
59-
cache.incr(fail_key, 1, version=system_version)
59+
fail_count = 1
60+
return fail_count
6061

6162

6263
def record_login_fail_lock(username: str, expire: int = 10):
64+
"""
65+
原来的实现使用 get() -> set()/incr() 非原子操作,会在高并发下产生竞态条件。
66+
使用 cache.incr 保证原子递增,并在不存在时初始化计数器并返回当前值。
67+
返回当前的失败计数(整数)。
68+
"""
6369
if not username:
64-
return
70+
return 0
6571
fail_key = system_get_key(f'system_{username}_lock_count')
66-
fail_count = cache.get(fail_key, version=system_version)
67-
if fail_count is None:
72+
try:
73+
fail_count = cache.incr(fail_key, 1, version=system_version)
74+
except ValueError:
75+
# key 不存在,初始化并设置过期(分钟转秒)
6876
cache.set(fail_key, 1, timeout=expire * 60, version=system_version)
69-
else:
70-
cache.incr(fail_key, 1, version=system_version)
77+
fail_count = 1
78+
return fail_count
7179

7280

7381
class LoginSerializer(serializers.Serializer):
@@ -190,34 +198,38 @@ def _validate_captcha(username: str, captcha: str) -> None:
190198

191199
@staticmethod
192200
def _handle_failed_login(username: str, is_license_valid: bool, failed_attempts: int, lock_time: int) -> None:
193-
"""处理登录失败"""
201+
"""处理登录失败
202+
203+
修复:使用原子递增(cache.incr)来获取最新的失败计数,然后基于该计数做出 >= 判断;
204+
使用 cache.add 原子创建锁,避免多个并发线程都错过 remain==0 的情况导致永不加锁。
205+
"""
194206
record_login_fail(username)
195-
record_login_fail_lock(username, lock_time)
207+
lock_fail_count = record_login_fail_lock(username, lock_time)
196208

209+
# 如果不是企业版或禁用锁定功能,直接返回(但计数已经记录)
197210
if not is_license_valid or failed_attempts <= 0:
198211
return
199212

200-
fail_count = cache.get(system_get_key(f'system_{username}_lock_count'), version=system_version) or 0
201-
remain_attempts = failed_attempts - fail_count
202-
203-
if remain_attempts > 0:
213+
# 计算剩余次数,并基于原子计数进行判断
214+
if lock_fail_count < failed_attempts:
215+
remain_attempts = failed_attempts - lock_fail_count
204216
raise AppApiException(
205217
1005,
206218
_("Login failed %s times, account will be locked, you have %s more chances !") % (
207219
failed_attempts, remain_attempts
208220
)
209221
)
210-
elif remain_attempts == 0:
211-
cache.set(
212-
system_get_key(f'system_{username}_lock'),
213-
1,
214-
timeout=lock_time * 60,
215-
version=system_version
216-
)
217-
raise AppApiException(
218-
1005,
219-
_("This account has been locked for %s minutes, please try again later") % lock_time
220-
)
222+
223+
cache.add(
224+
system_get_key(f'system_{username}_lock'),
225+
1,
226+
timeout=lock_time * 60,
227+
version=system_version
228+
)
229+
raise AppApiException(
230+
1005,
231+
_("This account has been locked for %s minutes, please try again later") % lock_time
232+
)
221233

222234

223235
class CaptchaResponse(serializers.Serializer):

0 commit comments

Comments
 (0)