Skip to content

Commit fb23e94

Browse files
committed
fix: update verification code attempt handling to include lockout period and improve user feedback
1 parent 65d2c01 commit fb23e94

4 files changed

Lines changed: 33 additions & 22 deletions

File tree

apps/locales/en_US/LC_MESSAGES/django.po

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,9 +1724,9 @@ msgstr ""
17241724
msgid "The verification code is incorrect or the verification code has expired"
17251725
msgstr ""
17261726

1727-
#: apps/users/serializers/user.py:79
1728-
#: apps/users/serializers/user.py:90
1729-
msgid "Too many verification code attempts, please request a new code"
1727+
#: apps/users/serializers/user.py:84
1728+
#: apps/users/serializers/user.py:97
1729+
msgid "Too many verification code attempts, please try again later"
17301730
msgstr ""
17311731

17321732
#: apps/common/constants/exception_code_constants.py:39

apps/locales/zh_CN/LC_MESSAGES/django.po

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,10 +1731,10 @@ msgstr "该邮箱未注册,请先注册"
17311731
msgid "The verification code is incorrect or the verification code has expired"
17321732
msgstr "验证码不正确或已过期"
17331733

1734-
#: apps/users/serializers/user.py:79
1735-
#: apps/users/serializers/user.py:90
1736-
msgid "Too many verification code attempts, please request a new code"
1737-
msgstr "验证码错误次数过多,请重新获取验证码"
1734+
#: apps/users/serializers/user.py:84
1735+
#: apps/users/serializers/user.py:97
1736+
msgid "Too many verification code attempts, please try again later"
1737+
msgstr "验证码尝试次数过多,请稍后重试"
17381738

17391739
#: apps/common/constants/exception_code_constants.py:39
17401740
msgid "The username has been registered, please log in directly"

apps/locales/zh_Hant/LC_MESSAGES/django.po

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,10 +1731,10 @@ msgstr "該郵箱未註冊,請先註冊"
17311731
msgid "The verification code is incorrect or the verification code has expired"
17321732
msgstr "驗證碼不正確或已過期"
17331733

1734-
#: apps/users/serializers/user.py:79
1735-
#: apps/users/serializers/user.py:90
1736-
msgid "Too many verification code attempts, please request a new code"
1737-
msgstr "驗證碼錯誤次數過多,請重新取得驗證碼"
1734+
#: apps/users/serializers/user.py:84
1735+
#: apps/users/serializers/user.py:97
1736+
msgid "Too many verification code attempts, please try again later"
1737+
msgstr "驗證碼嘗試次數過多,請稍後重試"
17381738

17391739
#: apps/common/constants/exception_code_constants.py:39
17401740
msgid "The username has been registered, please log in directly"

apps/users/serializers/user.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,40 +58,48 @@
5858

5959
# 验证码校验相关的安全限制
6060
MAX_VERIFY_CODE_ATTEMPTS = 5
61-
VERIFY_CODE_EXPIRE_SECONDS = 60 * 30
62-
VERIFY_CODE_FAILED_ATTEMPTS = VERIFY_CODE_EXPIRE_SECONDS
61+
VERIFY_CODE_EXPIRE_SECONDS = 10 * 60
62+
# 达到错误上限后的锁定冷却时长
63+
VERIFY_CODE_LOCKOUT_SECONDS = 10 * 60
6364

6465

6566
def check_verify_code_attempts(email: str, type_code: str, submitted_code: str) -> bool:
6667
"""
6768
校验验证码并限制错误尝试次数,防止验证码被暴力破解(CWE-307)。
68-
连续错误达到上限后,使当前验证码立即失效,必须重新发送验证码。
69+
失败计数按邮箱累计且不随重新发送验证码清零:连续错误达到上限后,进入固定冷却期的
70+
锁定,锁定期间即使验证码正确也一律拒绝,必须等待冷却期结束才能重新尝试,从而
71+
避免通过反复发送验证码维持无限猜解节奏。
6972
校验通过时返回 True,否则抛出校验异常。
7073
"""
7174
code_cache_key = email + ":" + type_code
7275
failed_cache_key = code_cache_key + "_failed_attempts"
76+
lock_cache_key = code_cache_key + "_locked"
7377
cached_code = cache.get(get_key(code_cache_key), version=version)
7478
failed_attempts = int(cache.get(get_key(failed_cache_key), version=version) or 0)
7579

76-
# 已锁定:验证码已被置为失效,要求重新发送
77-
if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS:
80+
# 已进入锁定冷却期(独立锁 key,固定 10 分钟):无论验证码是否正确都拒绝,
81+
# 且不刷新锁定时长
82+
if cache.get(get_key(lock_cache_key), version=version):
7883
cache.delete(get_key(code_cache_key), version=version)
79-
raise AppApiException(500, _("Too many verification code attempts, please request a new code"))
84+
raise AppApiException(500, _("Too many verification code attempts, please try again later"))
8085

8186
if cached_code is None:
8287
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
8388

8489
if cached_code != submitted_code:
8590
failed_attempts += 1
86-
cache.set(get_key(failed_cache_key), failed_attempts, timeout=VERIFY_CODE_FAILED_ATTEMPTS, version=version)
91+
cache.set(get_key(failed_cache_key), failed_attempts,
92+
timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version)
8793
if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS:
88-
# 达到最大尝试次数,立即使验证码失效并进入锁定状态
94+
# 错满 5 次:验证码立即失效,并写入独立锁 key 进入固定 10 分钟锁定
8995
cache.delete(get_key(code_cache_key), version=version)
90-
raise AppApiException(500, _("Too many verification code attempts, please request a new code"))
96+
cache.set(get_key(lock_cache_key), True, timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version)
97+
raise AppApiException(500, _("Too many verification code attempts, please try again later"))
9198
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
9299

93-
# 校验通过,清除错误尝试计数
100+
# 校验通过,清除错误尝试计数与锁定
94101
cache.delete(get_key(failed_cache_key), version=version)
102+
cache.delete(get_key(lock_cache_key), version=version)
95103
return True
96104

97105

@@ -1149,6 +1157,10 @@ def is_valid(self, *, raise_exception=False):
11491157
if ttl is not None and ttl > 0:
11501158
raise AppApiException(500, _("Do not send emails again within {seconds} seconds").format(
11511159
seconds=int(ttl.total_seconds())))
1160+
# 若邮箱处于验证码锁定冷却期,拒绝继续发送,避免验证码邮件轰炸
1161+
lock_cache_key = code_cache_key + "_locked"
1162+
if cache.get(get_key(lock_cache_key), version=version):
1163+
raise AppApiException(500, _("Too many verification code attempts, please try again later"))
11521164
return True
11531165

11541166
def send(self):
@@ -1198,7 +1210,6 @@ def send(self):
11981210
cache.delete(get_key(code_cache_key_lock))
11991211
return True
12001212
cache.set(get_key(code_cache_key), code, timeout=VERIFY_CODE_EXPIRE_SECONDS, version=version)
1201-
cache.delete(get_key(code_cache_key + "_failed_attempts"), version=version)
12021213
return True
12031214

12041215

0 commit comments

Comments
 (0)