在 Django 中實作登入驗證功能:LoginRequiredMixin 的應用與踩坑指南

分享在球館預約系統中使用 Django的LoginRequiredMixin 控制訪問權限的經驗與解決方案。從繼承順序錯誤、登入頁面設定問題到權限精細控制,文章提供了程式碼示例和實用技巧,幫助開發者有效實現用戶身份驗證功能。

Web Develop

專案背景

這個球館預約系統包含以下主要功能:
- 公告 (無需登入)
- 瀏覽可用場地(需要登入)
- 預約場地(需要登入)
- 查詢個人預約記錄(需要登入)
- 修改或取消預約(需要登入)
- 聯絡我們 (無需登入)
- 管理者後臺管理 (需登入且驗證帳號權限)

為了實現這些功能,我選擇使用 Django 的基於類的視圖(Class-Based Views)並結合 LoginRequiredMixin 來控制訪問權限。

什麼是 LoginRequiredMixin?

LoginRequiredMixin 是 Django 提供的一個混入類,用於限制未登入用戶訪問特定視圖。它相當於函數視圖中的 @login_required 裝飾器,但專門用於基於類的視圖。

實作過程

第一步:創建基本視圖

首先,為預約功能創建了基本的視圖:

# views.py
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from .models import Booking

class BookingListView(ListView):
    model = Booking
    template_name = 'bookings/booking_list.html'
    context_object_name = 'bookings'

class BookingCreateView(CreateView):
    model = Booking
    template_name = 'bookings/booking_create.html'
    fields = ['court', 'date', 'start_time', 'end_time']
    success_url = '/bookings/'

class BookingDetailView(DetailView):
    model = Booking
    template_name = 'bookings/booking_detail.html'

第二步:添加登入要求

接下來,我需要確保只有登入用戶才能訪問預約功能,所以我嘗試使用 LoginRequiredMixin

# views.py
from django.views.generic import ListView, CreateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Booking

class BookingListView(LoginRequiredMixin, ListView):
    model = Booking
    template_name = 'bookings/booking_list.html'
    context_object_name = 'bookings'

    def get_queryset(self):
        return Booking.objects.filter(user=self.request.user)

class BookingCreateView(LoginRequiredMixin, CreateView):
    model = Booking
    template_name = 'bookings/booking_create.html'
    fields = ['court', 'date', 'start_time', 'end_time']
    success_url = '/bookings/'

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

class BookingDetailView(LoginRequiredMixin, DetailView):
    model = Booking
    template_name = 'bookings/booking_detail.html'

踩坑記錄

坑一:繼承順序問題

我第一次實作時,不小心寫成了這樣:

# 錯誤的寫法
class BookingCreateView(CreateView, LoginRequiredMixin):  # 錯誤的順序!
    # ...

結果遇到了奇怪的錯誤:視圖不會重定向到登入頁面,而是直接顯示 HTTP 500 錯誤。

解決方案:
在 Python 的多重繼承中,方法解析順序(Method Resolution Order,MRO)是從左到右的。LoginRequiredMixin 必須放在左側,確保其 dispatch() 方法先於視圖的 dispatch() 方法被調用:

# 正確的寫法
class BookingCreateView(LoginRequiredMixin, CreateView):  # 正確的順序!
    # ...

坑二:找不到登入頁面

當我修正了繼承順序後,系統開始正確地攔截未登入用戶,但出現了新錯誤:

NoReverseMatch at /bookings/create/
Reverse for 'login' not found. 'login' is not a valid view function or pattern name.

或者在某些情況下會看到:

Page not found (404)
Request URL: http://localhost:8000/accounts/login/?next=/bookings/create/

這是因為 LoginRequiredMixin 默認會將未登入用戶重定向到 settings.LOGIN_URL,而 Django 的默認值是 'accounts/login/'。但我的項目中沒有這個 URL!

解決方案:

  1. settings.py 中指定登入頁面的 URL:
# settings.py
LOGIN_URL = 'login'  # 使用 URL 名稱
# 或者
LOGIN_URL = '/users/login/'  # 使用路徑
  1. 確保在 urls.py 中定義了相應的 URL:
# urls.py
from django.urls import path
from django.contrib.auth import views as auth_views

urlpatterns = [
    # ...
    path('users/login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    # ...
]
  1. 創建登入表單模板 users/login.html
<!-- users/login.html -->
{% extends 'base.html' %}

{% block content %}
<div class="login-form">
    <h2>登入</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">登入</button>
    </form>
    <p>還沒有帳號?<a href="{% url 'register' %}">註冊</a></p>
</div>
{% endblock %}

坑三:登入後不會跳轉回原頁面

解決了登入頁面問題後,我發現當用戶登入成功,系統不會自動跳轉回他們原本想訪問的頁面。

解決方案:

確保在登入視圖中處理 next 參數:

# views.py
from django.contrib.auth.views import LoginView
from django.urls import reverse_lazy

class CustomLoginView(LoginView):
    template_name = 'users/login.html'
    redirect_authenticated_user = True

    def get_success_url(self):
        next_url = self.request.GET.get('next')
        if next_url:
            return next_url
        return reverse_lazy('home')  # 默認重定向到首頁

然後在 urls.py 中使用這個自定義視圖:

# urls.py
from .views import CustomLoginView

urlpatterns = [
    # ...
    path('users/login/', CustomLoginView.as_view(), name='login'),
    # ...
]

坑四:自定義重定向 URL

有時候我們需要在不同視圖中指定不同的登入重定向 URL:

class BookingCreateView(LoginRequiredMixin, CreateView):
    login_url = '/custom-login/'  # 自定義登入頁面
    redirect_field_name = 'redirect_to'  # 自定義重定向參數名(默認是 'next')
    # ...

進階技巧:使用 UserPassesTestMixin

除了基本的登入限制外,有時我需要更精細的權限控制,例如確保用戶只能查看和修改自己的預約。這時可以結合使用 UserPassesTestMixin

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin

class BookingUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Booking
    template_name = 'bookings/booking_update.html'
    fields = ['date', 'start_time', 'end_time']

    def test_func(self):
        booking = self.get_object()
        return booking.user == self.request.user  # 只允許預約的創建者修改

    def handle_no_permission(self):
        # 自定義權限拒絕時的行為
        return HttpResponseForbidden("您沒有權限修改這個預約")

結論

Django 的 LoginRequiredMixin 是實現用戶身份驗證的強大工具,但正確使用它需要注意幾個關鍵點:

  1. 繼承順序至關重要 - LoginRequiredMixin 必須放在最左側
  2. 確保正確配置了 LOGIN_URL 並創建了相應的登入視圖和模板
  3. 處理登入後的重定向邏輯,提升用戶體驗
  4. 根據需要使用更高級的權限控制,如 UserPassesTestMixin

通過這些步驟,我成功實現了球館預約系統的用戶身份驗證功能,確保只有登入用戶才能預約場地和管理自己的預約記錄。


參考資料:
- Django 官方文檔:LoginRequiredMixin
- Django 官方文檔:Authentication in Web requests
- Django 官方文檔:Class-based views