Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/locales/en_US/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions apps/locales/zh_CN/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions apps/locales/zh_Hant/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 22 additions & 11 deletions apps/users/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down
Loading