Skip to content

第9章:用户认证系统

9.2 用户注册和登录

注册表单

创建用户注册功能需要自定义表单来处理用户输入和验证:

python
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class CustomUserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=30, required=True)
    last_name = forms.CharField(max_length=30, required=True)
    
    class Meta:
        model = User
        fields = ("username", "first_name", "last_name", "email", "password1", "password2")
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if User.objects.filter(email=email).exists():
            raise ValidationError("该邮箱已被注册")
        return email
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data["email"]
        user.first_name = self.cleaned_data["first_name"]
        user.last_name = self.cleaned_data["last_name"]
        if commit:
            user.save()
        return user

# 更复杂的注册表单
class ExtendedUserCreationForm(UserCreationForm):
    email = forms.EmailField(
        required=True,
        widget=forms.EmailInput(attrs={'class': 'form-control'})
    )
    first_name = forms.CharField(
        max_length=30,
        required=True,
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    last_name = forms.CharField(
        max_length=30,
        required=True,
        widget=forms.TextInput(attrs={'class': 'form-control'})
    )
    
    class Meta:
        model = User
        fields = ("username", "first_name", "last_name", "email", "password1", "password2")
        widgets = {
            'username': forms.TextInput(attrs={'class': 'form-control'}),
        }
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为密码字段添加样式
        self.fields['password1'].widget.attrs.update({'class': 'form-control'})
        self.fields['password2'].widget.attrs.update({'class': 'form-control'})
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if User.objects.filter(email=email).exists():
            raise ValidationError("该邮箱已被注册,请使用其他邮箱")
        return email
    
    def clean_username(self):
        username = self.cleaned_data['username']
        if User.objects.filter(username=username).exists():
            raise ValidationError("该用户名已被使用,请选择其他用户名")
        return username

登录/登出视图

实现自定义登录和登出功能:

python
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .forms import CustomUserCreationForm

def register(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            username = form.cleaned_data.get('username')
            messages.success(request, f'账户 {username} 创建成功!现在可以登录了。')
            return redirect('login')
    else:
        form = CustomUserCreationForm()
    
    return render(request, 'registration/register.html', {'form': form})

def user_login(request):
    if request.method == 'POST':
        form = AuthenticationForm(request, data=request.POST)
        if form.is_valid():
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            user = authenticate(username=username, password=password)
            if user is not None:
                login(request, user)
                messages.info(request, f"欢迎您,{username}!")
                # 重定向到用户之前访问的页面或首页
                next_page = request.GET.get('next', 'home')
                return redirect(next_page)
            else:
                messages.error(request, "用户名或密码错误")
        else:
            messages.error(request, "用户名或密码错误")
    else:
        form = AuthenticationForm()
    
    return render(request, 'registration/login.html', {'form': form})

@login_required
def user_logout(request):
    logout(request)
    messages.info(request, "您已成功退出登录")
    return redirect('home')

# 检查用户是否已登录的装饰器使用
@login_required
def profile(request):
    return render(request, 'registration/profile.html')

# 限制只有未登录用户才能访问的页面
from django.contrib.auth.decorators import user_passes_test

def anonymous_required(function=None, redirect_url=None):
    if not redirect_url:
        redirect_url = 'home'
    
    actual_decorator = user_passes_test(
        lambda u: u.is_anonymous,
        login_url=redirect_url
    )
    
    if function:
        return actual_decorator(function)
    return actual_decorator

@anonymous_required
def register_view(request):
    # 只有未登录用户才能访问注册页面
    pass

登录模板示例:

html
<!-- registration/login.html -->
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h3>用户登录</h3>
                </div>
                <div class="card-body">
                    {% if messages %}
                        {% for message in messages %}
                            <div class="alert alert-{{ message.tags }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                    
                    <form method="post">
                        {% csrf_token %}
                        
                        <div class="mb-3">
                            <label for="{{ form.username.id_for_label }}" class="form-label">用户名</label>
                            {{ form.username }}
                            {% if form.username.errors %}
                                <div class="text-danger">{{ form.username.errors }}</div>
                            {% endif %}
                        </div>
                        
                        <div class="mb-3">
                            <label for="{{ form.password.id_for_label }}" class="form-label">密码</label>
                            {{ form.password }}
                            {% if form.password.errors %}
                                <div class="text-danger">{{ form.password.errors }}</div>
                            {% endif %}
                        </div>
                        
                        <button type="submit" class="btn btn-primary">登录</button>
                        <a href="{% url 'password_reset' %}" class="btn btn-link">忘记密码?</a>
                    </form>
                    
                    <div class="mt-3">
                        <p>还没有账户?<a href="{% url 'register' %}">立即注册</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

注册模板示例:

html
<!-- registration/register.html -->
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h3>用户注册</h3>
                </div>
                <div class="card-body">
                    {% if messages %}
                        {% for message in messages %}
                            <div class="alert alert-{{ message.tags }}">{{ message }}</div>
                        {% endfor %}
                    {% endif %}
                    
                    <form method="post">
                        {% csrf_token %}
                        
                        <div class="mb-3">
                            {{ form.username.label_tag }}
                            {{ form.username }}
                            {% if form.username.errors %}
                                <div class="text-danger">{{ form.username.errors }}</div>
                            {% endif %}
                            <div class="form-text">必填。150个字符或更少。只能包含字母、数字和@/./+/-/_字符。</div>
                        </div>
                        
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                {{ form.first_name.label_tag }}
                                {{ form.first_name }}
                                {% if form.first_name.errors %}
                                    <div class="text-danger">{{ form.first_name.errors }}</div>
                                {% endif %}
                            </div>
                            <div class="col-md-6 mb-3">
                                {{ form.last_name.label_tag }}
                                {{ form.last_name }}
                                {% if form.last_name.errors %}
                                    <div class="text-danger">{{ form.last_name.errors }}</div>
                                {% endif %}
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            {{ form.email.label_tag }}
                            {{ form.email }}
                            {% if form.email.errors %}
                                <div class="text-danger">{{ form.email.errors }}</div>
                            {% endif %}
                        </div>
                        
                        <div class="mb-3">
                            {{ form.password1.label_tag }}
                            {{ form.password1 }}
                            {% if form.password1.errors %}
                                <div class="text-danger">{{ form.password1.errors }}</div>
                            {% endif %}
                            <div class="form-text">
                                <ul>
                                    <li>密码不能与您的其他个人信息太相似。</li>
                                    <li>密码必须包含至少8个字符。</li>
                                    <li>密码不能是常用密码。</li>
                                    <li>密码不能全为数字。</li>
                                </ul>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            {{ form.password2.label_tag }}
                            {{ form.password2 }}
                            {% if form.password2.errors %}
                                <div class="text-danger">{{ form.password2.errors }}</div>
                            {% endif %}
                            <div class="form-text">为了验证,请输入与上面相同的密码。</div>
                        </div>
                        
                        <button type="submit" class="btn btn-primary">注册</button>
                    </form>
                    
                    <div class="mt-3">
                        <p>已有账户?<a href="{% url 'login' %}">立即登录</a></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

密码重置

Django提供了完整的密码重置功能:

python
# 自定义密码重置表单
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.template import loader
from django.core.mail import EmailMultiAlternatives
from django.contrib.sites.shortcuts import get_current_site

class CustomPasswordResetForm(PasswordResetForm):
    def send_mail(self, subject_template_name, email_template_name,
                  context, from_email, to_email, html_email_template_name=None):
        """
        发送密码重置邮件
        """
        subject = loader.render_to_string(subject_template_name, context)
        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()

# 自定义密码重置视图
from django.contrib.auth.views import PasswordResetView
from .forms import CustomPasswordResetForm

class CustomPasswordResetView(PasswordResetView):
    form_class = CustomPasswordResetForm
    template_name = 'registration/password_reset_form.html'
    email_template_name = 'registration/password_reset_email.html'
    subject_template_name = 'registration/password_reset_subject.txt'
    success_url = '/accounts/password_reset/done/'

# URL配置
urlpatterns = [
    path('password_reset/', CustomPasswordResetView.as_view(), name='password_reset'),
    # 其他认证URL
]

密码重置模板:

html
<!-- registration/password_reset_form.html -->
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h3>重置密码</h3>
                </div>
                <div class="card-body">
                    <p>请输入您的邮箱地址,我们将发送密码重置链接到您的邮箱。</p>
                    
                    <form method="post">
                        {% csrf_token %}
                        <div class="mb-3">
                            {{ form.email.label_tag }}
                            {{ form.email }}
                            {% if form.email.errors %}
                                <div class="text-danger">{{ form.email.errors }}</div>
                            {% endif %}
                        </div>
                        <button type="submit" class="btn btn-primary">发送重置链接</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- registration/password_reset_email.html -->
{% autoescape off %}
您好,

您收到这封邮件是因为您请求重置在 {{ site_name }} 网站上的用户账户密码。

请点击下面的链接并选择新密码:
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}

您的用户名是:{{ user.get_username }}

如果您没有请求密码重置,请忽略这封邮件。

感谢使用我们的网站!
{{ site_name }} 团队
{% endautoescape %}

<!-- registration/password_reset_subject.txt -->
{{ site_name }} 密码重置

邮箱验证

实现邮箱验证功能:

python
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.crypto import get_random_string

class EmailVerification(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=50, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    is_verified = models.BooleanField(default=False)
    
    def save(self, *args, **kwargs):
        if not self.token:
            self.token = get_random_string(50)
        super().save(*args, **kwargs)

# forms.py
from django import forms
from django.contrib.auth.models import User

class EmailVerificationForm(forms.Form):
    email = forms.EmailField()
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if not User.objects.filter(email=email, is_active=True).exists():
            raise forms.ValidationError("该邮箱地址未注册或未激活")
        return email

# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.models import User
from django.contrib import messages
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.sites.shortcuts import get_current_site
from .models import EmailVerification

def send_verification_email(request, user):
    """发送验证邮件"""
    # 创建或获取验证记录
    verification, created = EmailVerification.objects.get_or_create(user=user)
    verification.token = get_random_string(50)
    verification.save()
    
    # 构建验证链接
    current_site = get_current_site(request)
    subject = '请验证您的邮箱地址'
    message = render_to_string('registration/verification_email.html', {
        'user': user,
        'domain': current_site.domain,
        'token': verification.token,
    })
    
    send_mail(subject, message, 'noreply@yoursite.com', [user.email])

def verify_email(request, token):
    """验证邮箱"""
    try:
        verification = EmailVerification.objects.get(token=token)
        if not verification.is_verified:
            verification.is_verified = True
            verification.save()
            messages.success(request, '您的邮箱已成功验证!')
        else:
            messages.info(request, '您的邮箱已经验证过了。')
    except EmailVerification.DoesNotExist:
        messages.error(request, '无效的验证链接。')
    
    return redirect('home')

# 在用户注册后发送验证邮件
def register(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            # 发送验证邮件
            send_verification_email(request, user)
            messages.success(request, '注册成功!请检查您的邮箱并点击验证链接。')
            return redirect('login')
    else:
        form = CustomUserCreationForm()
    
    return render(request, 'registration/register.html', {'form': form})

邮箱验证模板:

html
<!-- registration/verification_email.html -->
{% autoescape off %}
您好 {{ user.username }},

感谢您注册 {{ site_name }}!

请点击下面的链接验证您的邮箱地址:
http://{{ domain }}{% url 'verify_email' token=token %}

如果您没有注册账户,请忽略这封邮件。

谢谢!
{{ site_name }} 团队
{% endautoescape %}

通过这些功能,你可以构建一个完整的用户认证系统,包括注册、登录、密码重置和邮箱验证等功能。

小结

用户注册和登录是Web应用的核心功能:

  1. ✅ 自定义注册表单处理用户输入和验证
  2. ✅ 登录/登出视图管理用户会话
  3. ✅ 密码重置功能保障账户安全
  4. ✅ 邮箱验证确保账户有效性
  5. ✅ 模板和消息系统的良好集成

完整的认证流程提升了用户体验和系统安全性。

下一篇

我们将学习权限和装饰器的高级用法。

9.3 角色权限 →

目录

返回课程目录

Released under the Apache 2.0 License.