Django 用户认证与授权

Django 认证系统简介

Django 提供了一个强大且灵活的认证系统,用于处理用户账户、组、权限和基于会话的认证。它提供了完整的用户管理功能,包括注册、登录、注销、密码重置等。

1
用户认证
验证用户身份
2
会话管理
维护用户登录状态
3
权限授权
控制资源访问
认证系统组件
  • 用户 (Users) - 系统的用户账户
  • 权限 (Permissions) - 动作授权标记
  • 分组 (Groups) - 权限分类方式
  • 超级用户 - 拥有所有权限
  • 员工用户 - 可访问管理后台
内置功能
功能 描述 URL
登录 用户认证 /accounts/login/
注销 结束会话 /accounts/logout/
密码修改 更改密码 /accounts/password_change/
密码重置 忘记密码恢复 /accounts/password_reset/

用户模型和配置

Django 使用内置的 User 模型管理用户,也支持自定义用户模型。

内置用户模型
使用内置 User 模型
from django.contrib.auth.models import User

# 创建用户
user = User.objects.create_user(
    username='john',
    email='john@example.com',
    password='password123'
)

# 创建超级用户
admin_user = User.objects.create_superuser(
    username='admin',
    email='admin@example.com',
    password='admin123'
)

# 验证用户
from django.contrib.auth import authenticate
user = authenticate(username='john', password='password123')
if user is not None:
    # 用户认证成功
    print(f"用户 {user.username} 认证成功")
else:
    # 认证失败
    print("认证失败")
自定义用户模型
models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    # 添加额外字段
    phone_number = models.CharField(max_length=15, blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    profile_picture = models.ImageField(
        upload_to='profile_pics/',
        null=True,
        blank=True
    )

    # 自定义方法
    def get_full_name(self):
        return f"{self.first_name} {self.last_name}".strip()

    def __str__(self):
        return self.email  # 使用邮箱作为标识

    class Meta:
        # 按邮箱排序
        ordering = ['email']
settings.py
# 指定自定义用户模型
AUTH_USER_MODEL = 'myapp.CustomUser'

# 认证后端
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    # 可以添加其他认证后端
]

# 登录URL
LOGIN_URL = '/accounts/login/'

# 登录后重定向URL
LOGIN_REDIRECT_URL = '/dashboard/'

# 注销后重定向URL
LOGOUT_REDIRECT_URL = '/'

用户认证视图

Django 提供了内置的视图来处理常见的认证操作,也可以创建自定义视图。

使用内置认证视图
urls.py
from django.urls import path, include
from django.contrib.auth import views as auth_views

urlpatterns = [
    # 内置认证URL
    path('accounts/', include('django.contrib.auth.urls')),

    # 或者单独配置
    path('accounts/login/',
         auth_views.LoginView.as_view(template_name='registration/login.html'),
         name='login'),

    path('accounts/logout/',
         auth_views.LogoutView.as_view(),
         name='logout'),

    path('accounts/password_change/',
         auth_views.PasswordChangeView.as_view(),
         name='password_change'),

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

    path('accounts/password_reset/',
         auth_views.PasswordResetView.as_view(),
         name='password_reset'),

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

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

    path('accounts/reset/done/',
         auth_views.PasswordResetCompleteView.as_view(),
         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
from django.contrib.auth.decorators import login_required

def custom_login(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request, username=username, password=password)

        if user is not None:
            login(request, user)

            # 重定向到next参数或默认页面
            next_url = request.GET.get('next', '/dashboard/')
            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')

@login_required
def dashboard(request):
    return render(request, 'accounts/dashboard.html')
登录模板示例
templates/registration/login.html
{% extends 'base.html' %}

{% block content %}
<div class="login-form">
    <h2 class="text-center mb-4">用户登录</h2>

    {% if form.errors %}
        <div class="alert alert-danger">
            用户名或密码错误,请重试。
        </div>
    {% endif %}

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

        <div class="form-group">
            <label for="{{ form.username.id_for_label }}">用户名</label>
            {{ form.username }}
        </div>

        <div class="form-group">
            <label for="{{ form.password.id_for_label }}">密码</label>
            {{ form.password }}
        </div>

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

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

用户注册

创建用户注册功能,允许新用户创建账户。

用户注册表单
forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import CustomUser

class UserRegisterForm(UserCreationForm):
    email = forms.EmailField(required=True)

    class Meta:
        model = User
        # 或者使用自定义用户模型
        # model = CustomUser
        fields = ['username', 'email', 'password1', 'password2']

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

class CustomUserRegisterForm(UserCreationForm):
    class Meta:
        model = CustomUser
        fields = ['username', 'email', 'phone_number', 'password1', 'password2']

        widgets = {
            'username': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'}),
            'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
        }
注册视图
views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib import messages
from .forms import UserRegisterForm

def register(request):
    if request.method == 'POST':
        form = UserRegisterForm(request.POST)
        if form.is_valid():
            # 保存用户
            user = form.save()

            # 可选:立即登录用户
            login(request, user)

            # 发送欢迎消息
            messages.success(
                request,
                f'账户创建成功!欢迎 {user.username}'
            )

            return redirect('home')
    else:
        form = UserRegisterForm()

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

# 基于类的注册视图
from django.views.generic import CreateView
from django.urls import reverse_lazy

class RegisterView(CreateView):
    form_class = UserRegisterForm
    template_name = 'accounts/register.html'
    success_url = reverse_lazy('login')

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.success(
            self.request,
            '注册成功!请登录您的账户。'
        )
        return response
注册流程:
  1. 显示注册表单
  2. 验证用户输入
  3. 检查用户名/邮箱唯一性
  4. 创建用户账户
  5. 发送确认邮件(可选)
  6. 重定向到成功页面

权限和授权

Django 提供了细粒度的权限控制系统,可以控制用户对特定资源的访问。

权限系统

权限管理
from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType
from myapp.models import Article

# 获取用户权限
user = User.objects.get(username='john')
user_permissions = user.user_permissions.all()
user_has_perm = user.has_perm('myapp.add_article')

# 获取用户组权限
group, created = Group.objects.get_or_create(name='Editors')
user.groups.add(group)

# 添加权限到组
content_type = ContentType.objects.get_for_model(Article)
permission = Permission.objects.get(
    codename='change_article',
    content_type=content_type
)
group.permissions.add(permission)

# 直接为用户添加权限
user.user_permissions.add(permission)

# 检查权限
if user.has_perm('myapp.change_article'):
    print("用户有编辑文章的权限")

# 程序化权限检查
if user.has_perm('myapp.delete_article'):
    article.delete()
模型权限
models.py
from django.db import models
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE
    )
    is_published = models.BooleanField(default=False)

    class Meta:
        permissions = [
            ("can_publish", "可以发布文章"),
            ("can_feature", "可以推荐文章"),
            ("advanced_edit", "高级编辑权限"),
        ]

    # 对象级权限
    def user_can_edit(self, user):
        return user == self.author or user.has_perm('myapp.change_article')

    def user_can_delete(self, user):
        return (user == self.author or
                user.has_perm('myapp.delete_article') or
                user.is_staff)
权限矩阵
用户类型 查看 添加 修改 删除
匿名用户
普通用户 ✓ (自己的) ✓ (自己的)
编辑 ✓ (所有)
管理员

访问控制

Django 提供了多种方式控制对视图和模板的访问。

视图访问控制

使用装饰器
views.py
from django.contrib.auth.decorators import (
    login_required,
    permission_required,
    user_passes_test
)
from django.http import HttpResponseForbidden

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

# 要求特定权限
@permission_required('myapp.change_article', raise_exception=True)
def edit_article(request, article_id):
    # 编辑文章逻辑
    pass

# 要求多个权限
@permission_required(['myapp.add_article', 'myapp.change_article'])
def manage_articles(request):
    pass

# 自定义测试函数
def is_staff_user(user):
    return user.is_staff

@user_passes_test(is_staff_user)
def staff_dashboard(request):
    return render(request, 'staff/dashboard.html')

# 组合装饰器
@login_required
@permission_required('myapp.can_publish')
def publish_article(request, article_id):
    pass
基于类的视图访问控制
views.py
from django.contrib.auth.mixins import (
    LoginRequiredMixin,
    PermissionRequiredMixin,
    UserPassesTestMixin
)
from django.views.generic import ListView, CreateView, UpdateView
from django.urls import reverse_lazy

# 要求登录
class UserProfileView(LoginRequiredMixin, ListView):
    model = User
    template_name = 'accounts/profile.html'

# 要求权限
class ArticleCreateView(PermissionRequiredMixin, CreateView):
    model = Article
    fields = ['title', 'content']
    template_name = 'articles/create.html'
    permission_required = 'myapp.add_article'
    success_url = reverse_lazy('article_list')

# 自定义测试
class StaffOnlyView(UserPassesTestMixin, ListView):
    model = User
    template_name = 'staff/users.html'

    def test_func(self):
        return self.request.user.is_staff

# 组合混入类
class ArticleUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = Article
    fields = ['title', 'content', 'is_published']
    template_name = 'articles/update.html'
    permission_required = 'myapp.change_article'
    success_url = reverse_lazy('article_list')

    # 对象级权限检查
    def get_queryset(self):
        queryset = super().get_queryset()
        if not self.request.user.is_staff:
            queryset = queryset.filter(author=self.request.user)
        return queryset

模板中的访问控制

模板中的权限检查
{% if user.is_authenticated %}
    <p>欢迎, {{ user.username }}!</p>
    <a href="{% url 'logout' %}">退出登录</a>
{% else %}
    <a href="{% url 'login' %}">登录</a>
    <a href="{% url 'register' %}">注册</a>
{% endif %}

{# 检查特定权限 #}
{% if perms.myapp.add_article %}
    <a href="{% url 'article_create' %}" class="btn btn-primary">
        写文章
    </a>
{% endif %}

{# 检查多个权限 #}
{% if perms.myapp.change_article or perms.myapp.delete_article %}
    <div class="admin-actions">
        {% if perms.myapp.change_article %}
            <a href="{% url 'article_edit' article.id %}">编辑</a>
        {% endif %}
        {% if perms.myapp.delete_article %}
            <a href="{% url 'article_delete' article.id %}">删除</a>
        {% endif %}
    </div>
{% endif %}

{# 检查用户组 #}
{% if user.groups.all.0.name == "Editors" %}
    <div class="editor-tools">
        <!-- 编辑工具 -->
    </div>
{% endif %}
模板上下文变量
变量 描述
user 当前用户对象
user.is_authenticated 用户是否已认证
user.is_anonymous 用户是否匿名
user.is_staff 用户是否是员工
user.is_superuser 用户是否是超级用户
perms 权限对象
注意: 模板中的权限检查主要用于UI控制,不能替代视图中的服务器端权限验证。

会话管理

Django 使用会话来维护用户的登录状态和其他临时数据。

会话操作
使用会话
# 设置会话数据
request.session['user_preference'] = 'dark_mode'
request.session['cart_items'] = [1, 2, 3]
request.session.set_expiry(3600)  # 1小时后过期

# 获取会话数据
preference = request.session.get('user_preference', 'light_mode')
cart_items = request.session.get('cart_items', [])

# 检查会话键
if 'user_id' in request.session:
    user_id = request.session['user_id']

# 删除会话数据
if 'temp_data' in request.session:
    del request.session['temp_data']

# 清空会话
request.session.flush()

# 会话设置
from django.conf import settings

# settings.py 中的会话配置
SESSION_ENGINE = 'django.contrib.sessions.backends.db'  # 默认
SESSION_COOKIE_AGE = 1209600  # 2周,默认
SESSION_COOKIE_SECURE = True  # 仅HTTPS
SESSION_COOKIE_HTTPONLY = True  # 防止XSS
SESSION_EXPIRE_AT_BROWSER_CLOSE = False  # 持久会话
会话后端
会话后端配置
# settings.py

# 数据库后端(默认)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'

# 缓存后端
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

# 文件后端
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = '/tmp/django_sessions'

# 缓存+数据库后端(持久化)
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'

# 自定义会话后端
SESSION_ENGINE = 'myapp.session_backends.CustomSessionBackend'

# 安全配置
SESSION_COOKIE_SECURE = True  # 仅HTTPS传输
SESSION_COOKIE_HTTPONLY = True  # 防止JavaScript访问
SESSION_COOKIE_SAMESITE = 'Lax'  # CSRF保护
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
会话安全级别
高安全性配置:
  • SESSION_COOKIE_SECURE = True
  • SESSION_COOKIE_HTTPONLY = True
  • CSRF_COOKIE_SECURE = True
  • 较短的会话过期时间
低安全性配置:
  • 使用HTTP而不是HTTPS
  • SESSION_COOKIE_HTTPONLY = False
  • 过长的会话过期时间
  • 缺乏CSRF保护

高级认证功能

社交认证

使用 django-allauth
# settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.github',
    # ...
]

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

SITE_ID = 1

# allauth 配置
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
LOGIN_REDIRECT_URL = '/'

# 社交账号配置
SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': ['profile', 'email'],
        'AUTH_PARAMS': {'access_type': 'online'},
    },
    'github': {
        'SCOPE': ['user:email'],
    }
}
URL配置
from django.urls import path, include

urlpatterns = [
    path('accounts/', include('allauth.urls')),
    # ...
]
JWT 认证
使用 djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
}

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

class ProtectedView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({"message": "这是受保护的数据"})

自定义认证后端

自定义认证后端
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model

class EmailBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            # 尝试通过邮箱查找用户
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            try:
                # 回退到用户名
                user = UserModel.objects.get(username=username)
            except UserModel.DoesNotExist:
                return None

        # 验证密码
        if user.check_password(password):
            return user
        return None

    def get_user(self, user_id):
        UserModel = get_user_model()
        try:
            return UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
settings.py 配置
AUTHENTICATION_BACKENDS = [
    'myapp.backends.EmailBackend',  # 自定义后端
    'django.contrib.auth.backends.ModelBackend',  # 默认后端
]
认证后端顺序: Django 会按顺序尝试每个认证后端,直到有一个返回用户对象或所有后端都返回 None。

安全最佳实践

密码安全
  • 使用强密码哈希算法
  • 实施密码复杂度要求
  • 定期要求更改密码
  • 使用 HTTPS 传输密码
  • 防止暴力破解攻击
# settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'validate_password_length'},
    {'NAME': 'validate_password_complexity'},
]
会话安全
  • 使用安全 Cookie 标志
  • 设置合理的会话超时
  • 实现会话固定保护
  • 监控异常登录活动
  • 提供全局注销功能
# 安全会话配置
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_AGE = 3600 # 1小时
访问控制
  • 实施最小权限原则
  • 定期审计用户权限
  • 使用基于角色的访问控制
  • 实现对象级权限控制
  • 记录安全相关事件
监控和审计
  • 记录登录和注销事件
  • 监控失败登录尝试
  • 实施账户锁定机制
  • 定期检查安全日志
  • 设置安全警报
# 登录审计
from django.contrib.auth.signals import (
    user_logged_in,
    user_login_failed
)

def log_login(sender, request, user, **kwargs):
    log_security_event(
        f"用户 {user} 登录成功"
    )