diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 18550687647..022e164efb9 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -1724,9 +1724,9 @@ msgstr "" msgid "The verification code is incorrect or the verification code has expired" msgstr "" -#: apps/users/serializers/user.py:79 -#: apps/users/serializers/user.py:90 -msgid "Too many verification code attempts, please request a new code" +#: apps/users/serializers/user.py:84 +#: apps/users/serializers/user.py:97 +msgid "Too many verification code attempts, please try again later" msgstr "" #: apps/common/constants/exception_code_constants.py:39 diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index c4f6a8cb181..ff6a3fa26b2 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -1731,10 +1731,10 @@ msgstr "该邮箱未注册,请先注册" msgid "The verification code is incorrect or the verification code has expired" msgstr "验证码不正确或已过期" -#: apps/users/serializers/user.py:79 -#: apps/users/serializers/user.py:90 -msgid "Too many verification code attempts, please request a new code" -msgstr "验证码错误次数过多,请重新获取验证码" +#: apps/users/serializers/user.py:84 +#: apps/users/serializers/user.py:97 +msgid "Too many verification code attempts, please try again later" +msgstr "验证码尝试次数过多,请稍后重试" #: apps/common/constants/exception_code_constants.py:39 msgid "The username has been registered, please log in directly" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 7e89607dc89..2df1e8bc321 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -1731,10 +1731,10 @@ msgstr "該郵箱未註冊,請先註冊" msgid "The verification code is incorrect or the verification code has expired" msgstr "驗證碼不正確或已過期" -#: apps/users/serializers/user.py:79 -#: apps/users/serializers/user.py:90 -msgid "Too many verification code attempts, please request a new code" -msgstr "驗證碼錯誤次數過多,請重新取得驗證碼" +#: apps/users/serializers/user.py:84 +#: apps/users/serializers/user.py:97 +msgid "Too many verification code attempts, please try again later" +msgstr "驗證碼嘗試次數過多,請稍後重試" #: apps/common/constants/exception_code_constants.py:39 msgid "The username has been registered, please log in directly" diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 3ec3b77e7f4..c57ec5577ae 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -58,40 +58,48 @@ # 验证码校验相关的安全限制 MAX_VERIFY_CODE_ATTEMPTS = 5 -VERIFY_CODE_EXPIRE_SECONDS = 60 * 30 -VERIFY_CODE_FAILED_ATTEMPTS = VERIFY_CODE_EXPIRE_SECONDS +VERIFY_CODE_EXPIRE_SECONDS = 10 * 60 +# 达到错误上限后的锁定冷却时长 +VERIFY_CODE_LOCKOUT_SECONDS = 10 * 60 def check_verify_code_attempts(email: str, type_code: str, submitted_code: str) -> bool: """ 校验验证码并限制错误尝试次数,防止验证码被暴力破解(CWE-307)。 - 连续错误达到上限后,使当前验证码立即失效,必须重新发送验证码。 + 失败计数按邮箱累计且不随重新发送验证码清零:连续错误达到上限后,进入固定冷却期的 + 锁定,锁定期间即使验证码正确也一律拒绝,必须等待冷却期结束才能重新尝试,从而 + 避免通过反复发送验证码维持无限猜解节奏。 校验通过时返回 True,否则抛出校验异常。 """ code_cache_key = email + ":" + type_code failed_cache_key = code_cache_key + "_failed_attempts" + lock_cache_key = code_cache_key + "_locked" cached_code = cache.get(get_key(code_cache_key), version=version) failed_attempts = int(cache.get(get_key(failed_cache_key), version=version) or 0) - # 已锁定:验证码已被置为失效,要求重新发送 - if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: + # 已进入锁定冷却期(独立锁 key,固定 10 分钟):无论验证码是否正确都拒绝, + # 且不刷新锁定时长 + if cache.get(get_key(lock_cache_key), version=version): cache.delete(get_key(code_cache_key), version=version) - raise AppApiException(500, _("Too many verification code attempts, please request a new code")) + raise AppApiException(500, _("Too many verification code attempts, please try again later")) if cached_code is None: raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() if cached_code != submitted_code: failed_attempts += 1 - cache.set(get_key(failed_cache_key), failed_attempts, timeout=VERIFY_CODE_FAILED_ATTEMPTS, version=version) + cache.set(get_key(failed_cache_key), failed_attempts, + timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version) if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: - # 达到最大尝试次数,立即使验证码失效并进入锁定状态 + # 错满 5 次:验证码立即失效,并写入独立锁 key 进入固定 10 分钟锁定 cache.delete(get_key(code_cache_key), version=version) - raise AppApiException(500, _("Too many verification code attempts, please request a new code")) + cache.set(get_key(lock_cache_key), True, timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version) + raise AppApiException(500, _("Too many verification code attempts, please try again later")) raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() - # 校验通过,清除错误尝试计数 + # 校验通过,清除错误尝试计数与锁定 cache.delete(get_key(failed_cache_key), version=version) + cache.delete(get_key(lock_cache_key), version=version) return True @@ -1149,6 +1157,10 @@ def is_valid(self, *, raise_exception=False): if ttl is not None and ttl > 0: raise AppApiException(500, _("Do not send emails again within {seconds} seconds").format( seconds=int(ttl.total_seconds()))) + # 若邮箱处于验证码锁定冷却期,拒绝继续发送,避免验证码邮件轰炸 + lock_cache_key = code_cache_key + "_locked" + if cache.get(get_key(lock_cache_key), version=version): + raise AppApiException(500, _("Too many verification code attempts, please try again later")) return True def send(self): @@ -1198,7 +1210,6 @@ def send(self): cache.delete(get_key(code_cache_key_lock)) return True cache.set(get_key(code_cache_key), code, timeout=VERIFY_CODE_EXPIRE_SECONDS, version=version) - cache.delete(get_key(code_cache_key + "_failed_attempts"), version=version) return True