mirror of https://github.com/jumpserver/jumpserver
perf: 优化忘记密码
parent
896d42c53e
commit
27c505853b
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue