DRF通过邮箱密码重置

Django + Rest API 通过邮箱进行密码重置

/user/password/reset/ 用邮箱发送验证码

{email: 'handsomeahan515@gmail.com'}
  1. forms.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    import unicodedata

    from django import forms
    from django.contrib.auth import (
    authenticate, get_user_model, password_validation,
    )
    from django.contrib.auth.hashers import (
    UNUSABLE_PASSWORD_PREFIX, identify_hasher,
    )
    from django.contrib.auth.models import User
    from django.contrib.auth.tokens import default_token_generator
    from django.contrib.sites.shortcuts import get_current_site
    from django.core.mail import EmailMultiAlternatives
    from django.forms.utils import flatatt
    from django.template import loader
    from django.utils.encoding import force_bytes
    from django.utils.html import format_html, format_html_join
    from django.utils.http import urlsafe_base64_encode
    from django.utils.safestring import mark_safe
    from django.utils.text import capfirst
    from django.utils.translation import ugettext, ugettext_lazy as _


    class PasswordResetForm(forms.Form):
    email = forms.EmailField(label=_("Email"), max_length=254)

    def send_mail(self, subject_template_name, email_template_name,
    context, from_email, to_email, html_email_template_name=None):
    """
    Sends a django.core.mail.EmailMultiAlternatives to `to_email`.
    """
    subject = loader.render_to_string(subject_template_name, context)
    # Email subject *must not* contain newlines
    subject = ''.join(subject.splitlines())
    body = loader.render_to_string(email_template_name, context)

    email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
    if html_email_template_name is not None:
    html_email = loader.render_to_string(html_email_template_name, context)
    email_message.attach_alternative(html_email, 'text/html')

    email_message.send()

    def get_users(self, email):
    """Given an email, return matching user(s) who should receive a reset.

    This allows subclasses to more easily customize the default policies
    that prevent inactive users and users with unusable passwords from
    resetting their password.
    """
    active_users = get_user_model()._default_manager.filter(
    username__iexact=email, is_active=True)
    return (u for u in active_users if u.has_usable_password())

    def save(self, domain_override=None,
    subject_template_name='registration/password_reset_subject.txt',
    email_template_name='registration/password_reset_email.html',
    use_https=False, token_generator=default_token_generator,
    from_email=None, request=None, html_email_template_name=None,
    extra_email_context=None):
    """
    Generates a one-use only link for resetting password and sends to the
    user.
    """
    email = self.cleaned_data["email"]
    for user in self.get_users(email):
    if not domain_override:
    current_site = get_current_site(request)
    site_name = current_site.name
    domain = current_site.domain
    else:
    site_name = domain = domain_override
    """
    random verification code
    """
    context = {
    'email': user.email,
    'domain': domain,
    'site_name': site_name,
    # 'uid': urlsafe_base64_encode(force_bytes(user.pk)),
    'vcode': extra_email_context['vcode'],
    'user': user,
    'token': token_generator.make_token(user),
    'protocol': 'https' if use_https else 'http',
    }
    if extra_email_context is not None:
    context.update(extra_email_context)

    self.send_mail(
    subject_template_name, email_template_name, context, from_email,
    user.email, html_email_template_name=html_email_template_name,
    )
  2. serializers.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    class PasswordResetSerializer(serializers.Serializer):
    """
    Serializer for requesting a password reset e-mail.
    """
    email = serializers.EmailField()
    from .forms import PasswordResetForm

    password_reset_form_class = PasswordResetForm

    def get_email_options(self):
    """
    Override this method to change default e-mail options
    """
    email = self.validated_data["email"]
    active_user = UserModel._default_manager.filter(
    email__iexact=email, is_active=True).first()

    vcode = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6))
    active_user.vcode = vcode
    active_user.save()

    return {
    'extra_email_context': {'vcode': vcode},
    'subject_template_name': 'registration/subject.txt',
    'email_template_name': 'registration/password_reset.html',
    }

    def validate_email(self, value):
    # Create PasswordResetForm with the serializer
    self.reset_form = self.password_reset_form_class(
    data=self.initial_data)
    if not self.reset_form.is_valid():
    raise serializers.ValidationError(self.reset_form.errors)

    return value

    def save(self):
    request = self.context.get('request')
    # Set some values to trigger the send_email method.
    opts = {
    'use_https': request.is_secure(),
    'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
    'request': request,
    }

    opts.update(self.get_email_options())
    self.reset_form.save(**opts)
  3. views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class PasswordResetView(GenericAPIView):
    """
    Calls Django Auth PasswordResetForm save method.

    Accepts the following POST parameters: email
    Returns the success/fail message.
    """
    serializer_class = PasswordResetSerializer
    permission_classes = (AllowAny,)

    def post(self, request, *args, **kwargs):
    # Create a serializer with request.data
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    serializer.save()
    vcode = serializer.get_email_options()['extra_email_context']['vcode']
    # Return the success message with OK HTTP status
    return Response(
    {"detail": _("Password reset e-mail has been sent."), "code": 1, "vcode": vcode},
    status=status.HTTP_200_OK
    )

/user/password/reset/confirm/

使用vcode和新密码重新注册密码
{
    new_password1: "1qazse4",
    new_password2: "1qazse4",
    vcode: vcode
}
  1. serializers.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    class PasswordResetConfirmSerializer(serializers.Serializer):
    """
    Serializer for requesting a password reset uaername.
    """
    new_password1 = serializers.CharField(max_length=128)
    new_password2 = serializers.CharField(max_length=128)
    vcode = serializers.CharField()
    # token = serializers.CharField()

    set_password_form_class = SetPasswordForm

    def custom_validation(self, attrs):
    pass

    def validate(self, attrs):
    self._errors = {}

    # Decode the uidb64 to uid to get User object
    try:
    self.user = UserModel._default_manager.get(vcode=attrs['vcode'])
    except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
    raise ValidationError({'uid': ['Invalid value']})

    self.custom_validation(attrs)
    # Construct SetPasswordForm instance
    self.set_password_form = self.set_password_form_class(
    user=self.user, data=attrs
    )
    if not self.set_password_form.is_valid():
    raise serializers.ValidationError(self.set_password_form.errors)
    # if not default_token_generator.check_token(self.user, attrs['token']):
    # raise ValidationError({'token': ['Invalid value']})

    return attrs

    def save(self):
    self.set_password_form.save()
  2. views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class PasswordResetConfirmView(GenericAPIView):
    """
    Password reset e-mail link is confirmed, therefore
    this resets the user's password.

    Accepts the following POST parameters: token, uid,
    new_password1, new_password2
    Returns the success/fail message.
    """
    serializer_class = PasswordResetConfirmSerializer
    permission_classes = (AllowAny,)

    @sensitive_post_parameters_m
    def dispatch(self, *args, **kwargs):
    return super(PasswordResetConfirmView, self).dispatch(*args, **kwargs)

    def post(self, request):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(
    {"detail": _("Password has been reset with the new password.")}
    )