77 @desc:
88"""
99import base64
10- import datetime
1110import json
11+ import logging
1212
1313from captcha .image import ImageCaptcha
1414from django .core import signing
2323from common .database_model_manage .database_model_manage import DatabaseModelManage
2424from common .exception .app_exception import AppApiException
2525from common .utils .common import password_encrypt , get_random_chars
26- from common .utils .rsa_util import encrypt , decrypt
26+ from common .utils .rsa_util import decrypt
2727from maxkb .const import CONFIG
2828from users .models import User
29+ from common .utils .logger import maxkb_logger
2930
3031
3132class LoginRequest (serializers .Serializer ):
@@ -48,26 +49,34 @@ class LoginResponse(serializers.Serializer):
4849
4950
5051def record_login_fail (username : str , expire : int = 600 ):
51- """记录登录失败次数"""
52+ """记录登录失败次数(原子)返回当前失败计数 """
5253 if not username :
53- return
54+ return 0
5455 fail_key = system_get_key (f'system_{ username } ' )
55- fail_count = cache .get (fail_key , version = system_version )
56- if fail_count is None :
56+ try :
57+ fail_count = cache .incr (fail_key , 1 , version = system_version )
58+ except ValueError :
59+ # key 不存在,初始化并设置过期
5760 cache .set (fail_key , 1 , timeout = expire , version = system_version )
58- else :
59- cache . incr ( fail_key , 1 , version = system_version )
61+ fail_count = 1
62+ return fail_count
6063
6164
6265def record_login_fail_lock (username : str , expire : int = 10 ):
66+ """
67+ 使用 cache.incr 保证原子递增,并在不存在时初始化计数器并返回当前值。
68+ 这里的计数器用于判断是否应当进入“锁定”分支,避免依赖非原子 get -> set 的组合。
69+ """
6370 if not username :
64- return
71+ return 0
6572 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 :
73+ try :
74+ fail_count = cache .incr (fail_key , 1 , version = system_version )
75+ except ValueError :
76+ # key 不存在,初始化并设置过期(分钟转秒)
6877 cache .set (fail_key , 1 , timeout = expire * 60 , version = system_version )
69- else :
70- cache . incr ( fail_key , 1 , version = system_version )
78+ fail_count = 1
79+ return fail_count
7180
7281
7382class LoginSerializer (serializers .Serializer ):
@@ -93,8 +102,16 @@ def login(instance):
93102 encrypted_data = instance .get ("encryptedData" , "" )
94103
95104 if encrypted_data :
96- decrypted_data = json .loads (decrypt (encrypted_data ))
97- instance .update (decrypted_data )
105+ try :
106+ decrypted_raw = decrypt (encrypted_data )
107+ # decrypt 可能返回非 JSON 字符串,防护解析异常
108+ decrypted_data = json .loads (decrypted_raw ) if decrypted_raw else {}
109+ if isinstance (decrypted_data , dict ):
110+ instance .update (decrypted_data )
111+ except Exception as e :
112+ maxkb_logger .exception ("Failed to decrypt/parse encryptedData for user %s: %s" , username , e )
113+ raise AppApiException (500 , _ ("Invalid encrypted data" ))
114+
98115 try :
99116 LoginRequest (data = instance ).is_valid (raise_exception = True )
100117 except serializers .ValidationError :
@@ -128,7 +145,7 @@ def login(instance):
128145 LoginSerializer ._validate_captcha (username , captcha )
129146
130147 # 验证用户凭据
131- user = QuerySet ( User ) .filter (
148+ user = User . objects .filter (
132149 username = username ,
133150 password = password_encrypt (password )
134151 ).first ()
@@ -190,34 +207,61 @@ def _validate_captcha(username: str, captcha: str) -> None:
190207
191208 @staticmethod
192209 def _handle_failed_login (username : str , is_license_valid : bool , failed_attempts : int , lock_time : int ) -> None :
193- """处理登录失败"""
194- record_login_fail (username )
195- record_login_fail_lock (username , lock_time )
210+ """处理登录失败
211+
212+ 修复要点:
213+ - 使用 record_login_fail / record_login_fail_lock 两个原子 incr 来记录失败;
214+ - 不再依赖精确等于 0 的比较来触发锁,而是基于原子计数 >= 阈值来决定进入锁定分支;
215+ - 使用 cache.add 原子创建锁键,cache.add 保证只有第一个成功创建者可写入该键;
216+ 其他并发到达的请求若发现计数已到达阈值也应当返回“已锁定”响应,避免出现绕过。
217+ """
218+ # 记录普通失败计数(供验证码触发使用)
219+ try :
220+ record_login_fail (username )
221+ except Exception :
222+ maxkb_logger .exception ("Failed to record login fail for user %s" , username )
223+
224+ # 记录用于锁定判断的失败计数(按 lock_time 作为初始化过期分钟)
225+ lock_fail_count = 0
226+ try :
227+ lock_fail_count = record_login_fail_lock (username , lock_time )
228+ except Exception :
229+ maxkb_logger .exception ("Failed to record lock fail count for user %s" , username )
196230
231+ # 如果不是企业版或禁用锁定功能,直接返回(但计数已经记录)
197232 if not is_license_valid or failed_attempts <= 0 :
198233 return
199234
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 :
235+ # 当计数小于阈值,告知剩余尝试次数
236+ if lock_fail_count < failed_attempts :
237+ remain_attempts = failed_attempts - lock_fail_count
204238 raise AppApiException (
205239 1005 ,
206240 _ ("Login failed %s times, account will be locked, you have %s more chances !" ) % (
207241 failed_attempts , remain_attempts
208242 )
209243 )
210- elif remain_attempts == 0 :
211- cache .set (
244+
245+ # 当计数达到或超过阈值时,尝试原子创建锁键;无论 cache.add 返回 True/False,都返回已锁定响应,
246+ # 因为若为 False 说明其他并发请求已将账户标记为锁定,行为应一致。
247+ try :
248+ locked = cache .add (
212249 system_get_key (f'system_{ username } _lock' ),
213250 1 ,
214251 timeout = lock_time * 60 ,
215252 version = system_version
216253 )
217- raise AppApiException (
218- 1005 ,
219- _ ("This account has been locked for %s minutes, please try again later" ) % lock_time
220- )
254+ if locked :
255+ maxkb_logger .info ("Account %s locked by setting cache key" , username )
256+ else :
257+ maxkb_logger .info ("Account %s lock key already present (another request set it)" , username )
258+ except Exception :
259+ maxkb_logger .exception ("Failed to set lock key for user %s" , username )
260+
261+ raise AppApiException (
262+ 1005 ,
263+ _ ("This account has been locked for %s minutes, please try again later" ) % lock_time
264+ )
221265
222266
223267class CaptchaResponse (serializers .Serializer ):
0 commit comments