77 @desc:
88"""
99import base64
10- import datetime
1110import json
1211
1312from captcha .image import ImageCaptcha
2322from common .database_model_manage .database_model_manage import DatabaseModelManage
2423from common .exception .app_exception import AppApiException
2524from 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
2726from maxkb .const import CONFIG
2827from users .models import User
2928
@@ -48,26 +47,35 @@ class LoginResponse(serializers.Serializer):
4847
4948
5049def 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
6263def 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
7381class 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
223235class CaptchaResponse (serializers .Serializer ):
0 commit comments