跳到主要内容

用户认证

Django 内置了完整的用户认证系统,包括用户注册、登录、权限管理等功能。本节介绍如何使用和扩展 Django 的认证系统。

认证系统概述

Django 认证系统包含以下组件:

  • 用户(User):用户模型,包含用户名、密码、邮箱等基本信息
  • 权限(Permission):二进制标志,控制用户是否可以执行特定任务
  • 组(Group):将权限应用于多个用户的方式
  • 认证后端:可插拔的认证机制

用户模型

使用内置 User 模型

from django.contrib.auth.models import User


# 创建用户
user = User.objects.create_user(
username='john',
email='[email protected]',
password='password123'
)

# 创建超级用户
superuser = User.objects.create_superuser(
username='admin',
email='[email protected]',
password='admin123'
)

# 用户属性
user.username # 用户名
user.email # 邮箱
user.first_name # 名
user.last_name # 姓
user.is_active # 是否激活
user.is_staff # 是否员工
user.is_superuser # 是否超级用户
user.last_login # 最后登录时间
user.date_joined # 注册时间

# 设置密码
user.set_password('new_password')
user.save()

# 验证密码
user.check_password('password123') # 返回 True 或 False

扩展 User 模型

方式一:OneToOneField

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


class Profile(models.Model):
"""用户资料"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
avatar = models.ImageField(upload_to='avatars/', blank=True)
bio = models.TextField('个人简介', blank=True)
phone = models.CharField('手机号', max_length=11, blank=True)
birthday = models.DateField('生日', null=True, blank=True)

def __str__(self):
return f'{self.user.username} 的资料'


# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""创建用户时自动创建资料"""
if created:
Profile.objects.create(user=instance)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""保存用户时保存资料"""
instance.profile.save()

方式二:继承 AbstractUser

# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
"""自定义用户模型"""
avatar = models.ImageField(upload_to='avatars/', blank=True)
bio = models.TextField('个人简介', blank=True)
phone = models.CharField('手机号', max_length=11, blank=True)

class Meta:
verbose_name = '用户'
verbose_name_plural = '用户'


# settings.py
AUTH_USER_MODEL = 'myapp.CustomUser'

方式三:继承 AbstractBaseUser

完全自定义用户模型:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin


class CustomUserManager(BaseUserManager):
"""自定义用户管理器"""

def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('邮箱必须填写')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user

def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)


class CustomUser(AbstractBaseUser, PermissionsMixin):
"""使用邮箱作为用户名的用户模型"""
email = models.EmailField('邮箱', unique=True)
username = models.CharField('用户名', max_length=100)
avatar = models.ImageField(upload_to='avatars/', blank=True)
is_active = models.BooleanField('是否激活', default=True)
is_staff = models.BooleanField('是否员工', default=False)
created = models.DateTimeField('注册时间', auto_now_add=True)

objects = CustomUserManager()

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']

class Meta:
verbose_name = '用户'
verbose_name_plural = '用户'

用户登录和登出

使用内置视图

# urls.py
from django.urls import path
from django.contrib.auth import views as auth_views

urlpatterns = [
# 登录
path('login/', auth_views.LoginView.as_view(
template_name='accounts/login.html',
redirect_authenticated_user=True,
), name='login'),

# 登出
path('logout/', auth_views.LogoutView.as_view(
next_page='home',
), name='logout'),

# 密码修改
path('password_change/', auth_views.PasswordChangeView.as_view(
template_name='accounts/password_change.html',
success_url='/accounts/password_change/done/',
), name='password_change'),

path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(
template_name='accounts/password_change_done.html',
), name='password_change_done'),

# 密码重置
path('password_reset/', auth_views.PasswordResetView.as_view(
template_name='accounts/password_reset.html',
email_template_name='accounts/password_reset_email.html',
success_url='/accounts/password_reset/done/',
), name='password_reset'),

path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(
template_name='accounts/password_reset_done.html',
), name='password_reset_done'),

path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(
template_name='accounts/password_reset_confirm.html',
success_url='/accounts/reset/done/',
), name='password_reset_confirm'),

path('reset/done/', auth_views.PasswordResetCompleteView.as_view(
template_name='accounts/password_reset_complete.html',
), name='password_reset_complete'),
]

自定义登录视图

# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib import messages


def custom_login(request):
"""自定义登录视图"""
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')

user = authenticate(request, username=username, password=password)

if user is not None:
login(request, user)
messages.success(request, f'欢迎回来,{user.username}!')

# 跳转到 next 参数指定的页面
next_url = request.GET.get('next', 'home')
return redirect(next_url)
else:
messages.error(request, '用户名或密码错误')

return render(request, 'accounts/login.html')


def custom_logout(request):
"""自定义登出视图"""
logout(request)
messages.success(request, '您已成功登出')
return redirect('home')

登录模板

<!-- templates/accounts/login.html -->
{% extends 'base.html' %}

{% block content %}
<h2>登录</h2>

<form method="post">
{% csrf_token %}

<div class="form-group">
<label for="username">用户名</label>
<input type="text" name="username" id="username" class="form-control" required>
</div>

<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" id="password" class="form-control" required>
</div>

<button type="submit" class="btn btn-primary">登录</button>

<p class="mt-3">
<a href="{% url 'password_reset' %}">忘记密码?</a>
</p>
</form>
{% endblock %}

用户注册

注册视图

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


class UserRegistrationForm(forms.ModelForm):
"""用户注册表单"""
password = forms.CharField(
label='密码',
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
min_length=8,
)
password2 = forms.CharField(
label='确认密码',
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
)

class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
}

def clean_password2(self):
password = self.cleaned_data.get('password')
password2 = self.cleaned_data.get('password2')
if password and password2 and password != password2:
raise forms.ValidationError('两次输入的密码不一致')
return password2

def clean_email(self):
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError('该邮箱已被注册')
return email


# views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import UserRegistrationForm


def register(request):
"""用户注册"""
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.set_password(form.cleaned_data['password'])
user.save()

# 自动登录
# from django.contrib.auth import login
# login(request, user)

messages.success(request, '注册成功,请登录!')
return redirect('login')
else:
form = UserRegistrationForm()

return render(request, 'accounts/register.html', {'form': form})

使用 CreateView

from django.views.generic import CreateView
from django.urls import reverse_lazy
from django.contrib.auth import login
from .forms import UserRegistrationForm


class SignUpView(CreateView):
form_class = UserRegistrationForm
template_name = 'accounts/register.html'
success_url = reverse_lazy('home')

def form_valid(self, form):
response = super().form_valid(form)
# 注册后自动登录
login(self.request, self.object)
return response

权限和授权

检查权限

# 视图中检查权限
from django.contrib.auth.decorators import login_required, permission_required


@login_required
def profile(request):
"""需要登录"""
return render(request, 'accounts/profile.html')


@permission_required('blog.add_post')
def create_post(request):
"""需要特定权限"""
pass


@permission_required('blog.delete_post', raise_exception=True)
def delete_post(request, pk):
"""无权限时抛出 403"""
pass


# 类视图中检查权限
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin


class CreatePostView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Post
form_class = PostForm
permission_required = 'blog.add_post'
raise_exception = True

在模板中检查权限

{% if user.is_authenticated %}
<p>欢迎,{{ user.username }}!</p>

{% if perms.blog.add_post %}
<a href="{% url 'blog:post_create' %}">创建文章</a>
{% endif %}

{% if perms.blog.change_post %}
<a href="{% url 'blog:post_update' post.pk %}">编辑文章</a>
{% endif %}
{% else %}
<a href="{% url 'login' %}">登录</a>
{% endif %}

自定义权限

# models.py
class Post(models.Model):
title = models.CharField(max_length=200)

class Meta:
permissions = [
('can_publish', '可以发布文章'),
('can_edit_all', '可以编辑所有文章'),
]


# 编程方式添加权限
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType


# 获取内容类型
content_type = ContentType.objects.get_for_model(Post)

# 创建权限
permission = Permission.objects.create(
codename='can_publish',
name='可以发布文章',
content_type=content_type,
)

# 给用户添加权限
user = User.objects.get(username='john')
user.user_permissions.add(permission)

# 给组添加权限
from django.contrib.auth.models import Group

editors = Group.objects.get(name='Editors')
editors.permissions.add(permission)

用户组

from django.contrib.auth.models import Group, User


# 创建组
editors = Group.objects.create(name='Editors')

# 添加用户到组
user = User.objects.get(username='john')
user.groups.add(editors)

# 从组中移除用户
user.groups.remove(editors)

# 检查用户是否在组中
if user.groups.filter(name='Editors').exists():
pass

# 获取用户的所有组
user.groups.all()

# 获取组的所有用户
editors.user_set.all()

认证后端

自定义认证后端

# backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User


class EmailBackend(ModelBackend):
"""使用邮箱登录"""

def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
return None

if user.check_password(password) and self.user_can_authenticate(user):
return user
return None


# settings.py
AUTHENTICATION_BACKENDS = [
'myapp.backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend',
]

完整示例

# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.views.generic import CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .forms import UserRegistrationForm, ProfileForm, UserUpdateForm
from .models import Profile


def register(request):
"""用户注册"""
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, '注册成功!')
return redirect('home')
else:
form = UserRegistrationForm()
return render(request, 'accounts/register.html', {'form': form})


@login_required
def profile(request):
"""用户资料"""
return render(request, 'accounts/profile.html')


@login_required
def edit_profile(request):
"""编辑资料"""
if request.method == 'POST':
user_form = UserUpdateForm(request.POST, instance=request.user)
profile_form = ProfileForm(
request.POST,
request.FILES,
instance=request.user.profile
)

if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, '资料更新成功!')
return redirect('profile')
else:
user_form = UserUpdateForm(instance=request.user)
profile_form = ProfileForm(instance=request.user.profile)

return render(request, 'accounts/edit_profile.html', {
'user_form': user_form,
'profile_form': profile_form,
})