用户认证
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,
})