Django从零开始(六):用户认证与权限系统

一、Django认证系统概述

Django自带了一套完善的用户认证系统(Authentication System),包含用户注册、登录、登出、权限管理、分组等功能。它位于 django.contrib.auth 模块中。

认证系统核心组件

  • User模型:用户模型,包含用户名、密码、邮箱等字段
  • Authentication:验证用户身份(用户名+密码)
  • Authorization:权限和分组管理
  • Session:基于Session的状态管理
  • 视图和表单:内置的登录/登出/密码重置视图

二、User模型

2.1 默认User模型字段

# django.contrib.auth.models.User 的默认字段
username      # 用户名(必填,唯一)
password      # 密码(自动哈希存储)
email         # 邮箱(可选)
first_name    # 名(可选,max_length=150)
last_name     # 姓(可选,max_length=150)
is_active     # 是否激活(默认True)
is_staff      # 是否可登录Admin后台(默认False)
is_superuser  # 是否超级管理员(默认False)
date_joined   # 注册时间
last_login    # 最后登录时间

2.2 用户基本操作

from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout

# ===== 创建用户 =====

# 方法1:create_user(自动处理密码哈希)
user = User.objects.create_user(
    username='zhangsan',
    email='zhangsan@example.com',
    password='secure_password123',  # 明文密码,会自动哈希
    first_name='三',
    last_name='张',
)

# 方法2:先创建再设置密码
user = User(username='lisi', email='lisi@example.com')
user.set_password('secure_password123')  # 手动哈希密码
user.save()

# ===== 修改用户信息 =====
user.email = 'new_email@example.com'
user.save()

# 修改密码
user.set_password('new_password456')
user.save()

# 检查密码是否正确
if user.check_password('new_password456'):
    print('密码正确')

# ===== 删除用户 =====
user.delete()

2.3 自定义User模型(推荐做法)

Django官方强烈建议在项目开始时就自定义User模型,即使它与默认模型完全相同,这样未来需要扩展时会非常方便:

# users/models.py

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


class User(AbstractUser):
    """自定义用户模型"""
    avatar = models.ImageField('头像', upload_to='avatars/', null=True, blank=True)
    phone = models.CharField('手机号', max_length=11, blank=True, default='')
    bio = models.TextField('个人简介', blank=True, default='')
    birthday = models.DateField('生日', null=True, blank=True)
    website = models.URLField('个人网站', blank=True, default='')
    github = models.CharField('GitHub', max_length=100, blank=True, default='')

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

    def __str__(self):
        return self.username

    def get_full_name_cn(self):
        """中文名"""
        return f'{self.last_name}{self.first_name}' or self.username
# settings.py
AUTH_USER_MODEL = 'users.User'  # 指定自定义用户模型

三、注册功能实现

3.1 注册表单

# users/forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User


class RegisterForm(UserCreationForm):
    """用户注册表单"""
    email = forms.EmailField(
        label='邮箱',
        required=True,
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': '请输入邮箱',
        })
    )

    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']
        widgets = {
            'username': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '请输入用户名',
            }),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['password1'].widget.attrs.update({
            'class': 'form-control',
            'placeholder': '请输入密码',
        })
        self.fields['password2'].widget.attrs.update({
            'class': 'form-control',
            'placeholder': '请确认密码',
        })

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

    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        if commit:
            user.save()
        return user

3.2 注册视图

# users/views.py

from django.shortcuts import render, redirect
from django.contrib.auth import login
from .forms import RegisterForm

def register(request):
    """用户注册"""
    if request.user.is_authenticated:
        return redirect('blog:article_list')

    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            # 注册成功后自动登录
            login(request, user)
            return redirect('blog:article_list')
    else:
        form = RegisterForm()

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

四、登录与登出

4.1 手动实现登录

# users/views.py

from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import AuthenticationForm

def login_view(request):
    """用户登录"""
    if request.user.is_authenticated:
        return redirect('blog:article_list')

    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(request, username=username, password=password)
            if user is not None:
                login(request, user)
                # 记住我功能
                if not form.cleaned_data.get('remember_me'):
                    request.session.set_expiry(0)  # 浏览器关闭后过期
                else:
                    request.session.set_expiry(60 * 60 * 24 * 30)  # 30天

                next_url = request.GET.get('next', '/')
                return redirect(next_url)
    else:
        form = AuthenticationForm()

    return render(request, 'users/login.html', {'form': form})


def logout_view(request):
    """用户登出"""
    logout(request)
    return redirect('blog:article_list')

4.2 使用Django内置认证视图

# urls.py

from django.contrib.auth import views as auth_views

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

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

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

    path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(
        template_name='users/password_change_done.html',
    ), name='password_change_done'),

    # 密码重置(忘记密码)
    path('password-reset/', auth_views.PasswordResetView.as_view(
        template_name='users/password_reset.html',
        email_template_name='users/password_reset_email.html',
        subject_template_name='users/password_reset_subject.txt',
        success_url='/password-reset/done/',
    ), name='password_reset'),

    path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(
        template_name='users/password_reset_done.html',
    ), name='password_reset_done'),

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

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

五、登录保护(访问控制)

5.1 装饰器方式

from django.contrib.auth.decorators import login_required, permission_required

# 要求登录才能访问
@login_required(login_url='/accounts/login/')
def dashboard(request):
    return render(request, 'dashboard.html')

# 要求特定权限
@permission_required('blog.can_publish', login_url='/accounts/login/')
def publish_article(request):
    pass

# 多个权限(满足任一)
@permission_required(['blog.change_article', 'blog.delete_article'])
def moderate_article(request):
    pass

5.2 类视图Mixin

from django.contrib.auth.mixins import (
    LoginRequiredMixin,
    PermissionRequiredMixin,
    UserPassesTestMixin,
)

class DashboardView(LoginRequiredMixin, TemplateView):
    """需要登录的仪表盘"""
    template_name = 'dashboard.html'
    login_url = '/accounts/login/'

class PublishView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
    """需要登录+发布权限"""
    model = Article
    fields = ['title', 'content']
    permission_required = 'blog.can_publish'
    raise_exception = True  # 403而不是重定向到登录

class ArticleUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    """只有作者本人可以编辑"""
    model = Article
    fields = ['title', 'content']

    def test_func(self):
        article = self.get_object()
        return self.request.user == article.author or self.request.user.is_superuser

六、权限系统

6.1 权限类型

# Django为每个模型自动创建4种权限
# add:    添加权限    blog.add_article
# change: 修改权限    blog.change_article
# delete: 删除权限    blog.delete_article
# view:   查看权限    blog.view_article

# 检查权限
user.has_perm('blog.add_article')
user.has_perm('blog.change_article')
user.has_perms(['blog.add_article', 'blog.change_article'])

# 在模板中检查权限
# {% if perms.blog.add_article %}
#     <a href="{% url 'blog:article_create' %}">写文章</a>
# {% endif %}

6.2 自定义权限

class Article(models.Model):
    # ...

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

6.3 分组管理

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

# 创建分组
editors, _ = Group.objects.get_or_create(name='编辑')
reviewers, _ = Group.objects.get_or_create(name='审核员')

# 给分组分配权限
publish_perm = Permission.objects.get(codename='can_publish')
edit_perm = Permission.objects.get(codename='can_edit_all')
moderate_perm = Permission.objects.get(codename='can_moderate')

editors.permissions.add(publish_perm, edit_perm)
reviewers.permissions.add(moderate_perm)

# 将用户加入分组
user.groups.add(editors)

# 检查分组
user.groups.filter(name='编辑').exists()
user.has_perm('blog.can_publish')  # 通过分组继承的权限也返回True

七、用户资料管理

# users/models.py

from django.db.models.signals import post_save
from django.dispatch import receiver

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

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


# 信号:用户创建时自动创建Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()


# users/forms.py
class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ['avatar', 'bio', 'phone', 'birthday', 'github']
        widgets = {
            'bio': forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}),
            'phone': forms.TextInput(attrs={'class': 'form-control'}),
            'birthday': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
            'github': forms.URLInput(attrs={'class': 'form-control'}),
        }


# users/views.py
@login_required
def profile_edit(request):
    if request.method == 'POST':
        form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
        if form.is_valid():
            form.save()
            messages.success(request, '资料更新成功!')
            return redirect('profile')
    else:
        form = ProfileForm(instance=request.user.profile)
    return render(request, 'users/profile_edit.html', {'form': form})

八、社交登录(django-allauth)

# 安装
pip install django-allauth

# settings.py
INSTALLED_APPS = [
    ...
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',   # GitHub登录
    'allauth.socialaccount.providers.google',   # Google登录
    'allauth.socialaccount.providers.weixin',   # 微信登录
]

SITE_ID = 1

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]

# allauth 配置
ACCOUNT_EMAIL_VERIFICATION = 'none'     # 邮箱验证策略
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_LOGOUT_ON_GET = True
LOGIN_REDIRECT_URL = '/'

# urls.py
urlpatterns += [path('accounts/', include('allauth.urls'))]

九、总结

本章我们学习了Django认证系统的核心功能:

  • User模型和自定义用户模型
  • 用户注册、登录、登出功能实现
  • Django内置认证视图的使用
  • 登录保护和权限控制
  • 权限和分组管理
  • 用户资料管理和社交登录

下一章我们将学习 Django REST Framework,开始构建RESTful API接口。

发表回复

后才能评论