perf: 优化忘记密码

pull/11743/head
ibuler 2023-10-07 13:07:47 +08:00 committed by Bryan
parent 896d42c53e
commit 27c505853b
8 changed files with 678 additions and 599 deletions

View File

@ -1,3 +1,5 @@
import time
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import reverse from django.shortcuts import reverse
@ -7,7 +9,7 @@ from rest_framework.generics import CreateAPIView
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from authentication.errors import PasswordInvalid from authentication.errors import PasswordInvalid, IntervalTooShort
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.mixins import authenticate from authentication.mixins import authenticate
from authentication.serializers import ( from authentication.serializers import (
@ -38,18 +40,18 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
return None, err_msg return None, err_msg
return user, None return user, None
def create(self, request, *args, **kwargs): @staticmethod
token = request.GET.get('token') def safe_send_code(token, code, target, form_type, content):
userinfo = cache.get(token) token_sent_key = '{}_send_at'.format(token)
if not userinfo: token_send_at = cache.get(token_sent_key, 0)
return HttpResponseRedirect(reverse('authentication:forgot-previewing')) if token_send_at:
raise IntervalTooShort(60)
SendAndVerifyCodeUtil(target, code, backend=form_type, **content).gen_and_send_async()
cache.set(token_sent_key, int(time.time()), 60)
serializer = self.get_serializer(data=request.data) def prepare_code_data(self, user_info, serializer):
serializer.is_valid(raise_exception=True) username = user_info.get('username')
username = userinfo.get('username')
form_type = serializer.validated_data['form_type'] form_type = serializer.validated_data['form_type']
code = random_string(6, lower=False, upper=False)
other_args = {}
target = serializer.validated_data[form_type] target = serializer.validated_data[form_type]
if form_type == 'sms': if form_type == 'sms':
@ -59,15 +61,30 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
query_key = form_type query_key = form_type
user, err = self.is_valid_user(username=username, **{query_key: target}) user, err = self.is_valid_user(username=username, **{query_key: target})
if not user: if not user:
return Response({'error': err}, status=400) raise ValueError(err)
code = random_string(6, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password')) subject = '%s: %s' % (get_login_title(), _('Forgot password'))
context = { context = {
'user': user, 'title': subject, 'code': code, 'user': user, 'title': subject, 'code': code,
} }
message = render_to_string('authentication/_msg_reset_password_code.html', context) message = render_to_string('authentication/_msg_reset_password_code.html', context)
other_args['subject'], other_args['message'] = subject, message content = {'subject': subject, 'message': message}
SendAndVerifyCodeUtil(target, code, backend=form_type, **other_args).gen_and_send_async() return code, target, form_type, content
def create(self, request, *args, **kwargs):
token = request.GET.get('token')
user_info = cache.get(token)
if not user_info:
return HttpResponseRedirect(reverse('authentication:forgot-previewing'))
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
code, target, form_type, content = self.prepare_code_data(user_info, serializer)
except ValueError as e:
return Response({'error': str(e)}, status=400)
self.safe_send_code(token, code, target, form_type, content)
return Response({'data': 'ok'}, status=200) return Response({'data': 'ok'}, status=200)

View File

@ -36,3 +36,11 @@ class FeiShuNotBound(JMSException):
class PasswordInvalid(JMSException): class PasswordInvalid(JMSException):
default_code = 'passwd_invalid' default_code = 'passwd_invalid'
default_detail = _('Your password is invalid') default_detail = _('Your password is invalid')
class IntervalTooShort(JMSException):
default_code = 'interval_too_short'
default_detail = _('Please wait for %s seconds before retry')
def __init__(self, interval, *args, **kwargs):
super().__init__(detail=self.default_detail % interval, *args, **kwargs)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import APIException
class JMSException(APIException): class JMSException(APIException):

View File

@ -26,7 +26,6 @@ class SendAndVerifyCodeUtil(object):
self.target = target self.target = target
self.backend = backend self.backend = backend
self.key = key or self.KEY_TMPL.format(target) self.key = key or self.KEY_TMPL.format(target)
self.verify_key = self.key + '_verify'
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
self.other_args = kwargs self.other_args = kwargs
@ -48,11 +47,6 @@ class SendAndVerifyCodeUtil(object):
raise raise
def verify(self, code): def verify(self, code):
times = cache.get(self.verify_key, 0)
if times >= 3:
self.__clear()
raise CodeExpired
cache.set(self.verify_key, times + 1, timeout=self.timeout)
right = cache.get(self.key) right = cache.get(self.key)
if not right: if not right:
raise CodeExpired raise CodeExpired
@ -65,7 +59,6 @@ class SendAndVerifyCodeUtil(object):
def __clear(self): def __clear(self):
cache.delete(self.key) cache.delete(self.key)
cache.delete(self.verify_key)
def __ttl(self): def __ttl(self):
return cache.ttl(self.key) return cache.ttl(self.key)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -606,7 +606,8 @@ class TokenMixin:
def generate_reset_token(self): def generate_reset_token(self):
token = random_string(50) token = random_string(50)
self.set_cache(token) key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
cache.set(key, {'id': self.id, 'email': self.email}, 3600)
return token return token
@classmethod @classmethod
@ -626,10 +627,6 @@ class TokenMixin:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return None return None
def set_cache(self, token):
key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
cache.set(key, {'id': self.id, 'email': self.email}, 3600)
@classmethod @classmethod
def expired_reset_password_token(cls, token): def expired_reset_password_token(cls, token):
key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token) key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)

View File

@ -2,6 +2,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import time
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
@ -9,6 +11,7 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView, RedirectView from django.views.generic import FormView, RedirectView
from authentication.errors import IntervalTooShort
from common.utils import FlashMessageUtil, get_object_or_none, random_string from common.utils import FlashMessageUtil, get_object_or_none, random_string
from common.utils.verify_code import SendAndVerifyCodeUtil from common.utils.verify_code import SendAndVerifyCodeUtil
from users.notifications import ResetPasswordSuccessMsg from users.notifications import ResetPasswordSuccessMsg
@ -37,6 +40,20 @@ class UserForgotPasswordPreviewingView(FormView):
def get_redirect_url(token): def get_redirect_url(token):
return reverse('authentication:forgot-password') + '?token=%s' % token return reverse('authentication:forgot-password') + '?token=%s' % token
@staticmethod
def generate_previewing_token(user):
sent_ttl = 60
token_sent_at_key = '{}_send_at'.format(user.username)
token_sent_at = cache.get(token_sent_at_key, 0)
if token_sent_at:
raise IntervalTooShort(sent_ttl)
token = random_string(36)
user_info = {'username': user.username, 'phone': user.phone, 'email': user.email}
cache.set(token, user_info, 5 * 60)
cache.set(token_sent_at_key, time.time(), sent_ttl)
return token
def form_valid(self, form): def form_valid(self, form):
username = form.cleaned_data['username'] username = form.cleaned_data['username']
user = get_object_or_none(User, username=username) user = get_object_or_none(User, username=username)
@ -49,9 +66,11 @@ class UserForgotPasswordPreviewingView(FormView):
form.add_error('username', error) form.add_error('username', error)
return super().form_invalid(form) return super().form_invalid(form)
token = random_string(36) try:
user_map = {'username': user.username, 'phone': user.phone, 'email': user.email} token = self.generate_previewing_token(user)
cache.set(token, user_map, 5 * 60) except IntervalTooShort as e:
form.add_error('username', e)
return super().form_invalid(form)
return redirect(self.get_redirect_url(token)) return redirect(self.get_redirect_url(token))
@ -103,13 +122,25 @@ class UserForgotPasswordView(FormView):
reset_password_url = reverse('authentication:reset-password') reset_password_url = reverse('authentication:reset-password')
return reset_password_url + query_params return reset_password_url + query_params
@staticmethod
def safe_verify_code(token, target, form_type, code):
token_verified_key = '{}_verified'.format(token)
token_verified_times = cache.get(token_verified_key, 0)
if token_verified_times >= 3:
cache.delete(token)
raise ValueError('Verification code has been used more than 3 times, please re-verify')
cache.set(token_verified_key, token_verified_times + 1, 5 * 60)
sender_util = SendAndVerifyCodeUtil(target, backend=form_type)
return sender_util.verify(code)
def form_valid(self, form): def form_valid(self, form):
token = self.request.GET.get('token') token = self.request.GET.get('token')
userinfo = cache.get(token) user_info = cache.get(token)
if not userinfo: if not user_info:
return redirect(self.get_redirect_url(return_previewing=True)) return redirect(self.get_redirect_url(return_previewing=True))
username = userinfo.get('username') username = user_info.get('username')
form_type = form.cleaned_data['form_type'] form_type = form.cleaned_data['form_type']
target = form.cleaned_data[form_type] target = form.cleaned_data[form_type]
code = form.cleaned_data['code'] code = form.cleaned_data['code']
@ -120,8 +151,9 @@ class UserForgotPasswordView(FormView):
target = target.lstrip('+') target = target.lstrip('+')
try: try:
sender_util = SendAndVerifyCodeUtil(target, backend=form_type) self.safe_verify_code(token, target, form_type, code)
sender_util.verify(code) except ValueError as e:
return redirect(self.get_redirect_url(return_previewing=True))
except Exception as e: except Exception as e:
form.add_error('code', str(e)) form.add_error('code', str(e))
return super().form_invalid(form) return super().form_invalid(form)