Django 使用者登入與資料操作的最佳實踐

更新日期: 2024 年 12 月 1 日

本篇教學將詳細介紹如何在 Django 專案中實現與使用者關聯的資料操作,包括:

  1. 加入登入限制的裝飾器。
  2. 透過表單處理資料的新增與修改。
  3. 解決使用者 ID 無法傳遞的問題。
  4. 安全地限制用戶僅能操作自己的資料。

登入限制與裝飾器的應用

為確保只有登入的使用者可以訪問相關頁面,我們需要使用 @login_required 裝飾器。

新增 @login_required 到視圖函數

以下為一個新增履歷的視圖函數範例:

from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.contrib import messages

@login_required
def index(request):
    if request.POST:
        form = ResumeForm(request.POST)
        form.save()
        messages.success(request, "新增成功")
        return redirect("resumes:index")

    resumes = Resume.objects.all()
    return render(request, "resumes/index.html", {"resumes": resumes})

@login_required 的功能

  • 確保只有已登入的使用者才能訪問某些特定的視圖(View)。
  • 如果未登入的使用者嘗試訪問這些受保護的視圖,Django 會自動將他們重定向到指定的登入頁面。
  • 這是一個裝飾器,通常用在需要保護的視圖函數上,例如:
from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, "users/profile.html")

當使用者訪問 /profile 頁面時,@login_required 會檢查該使用者是否已登入:

  1. 若已登入:執行視圖函數,返回正常的頁面響應。
  2. 若未登入:重定向至登入頁面,並附帶查詢參數 ?next=/profile,告訴系統完成登入後應跳轉回 /profile

LOGIN_URL 的設定

settings.py 中,LOGIN_URL 指定未登入使用者被引導到的登入頁面路徑:

LOGIN_URL = "/users/login"
作用
  1. 設置全局重定向路徑:未登入的使用者訪問任何被 @login_required 保護的視圖時,Django 會將他們重定向到該路徑。
  2. 提高可維護性:集中管理登入頁面路徑,若登入頁面改名或遷移,只需更新這一處設置。
對應的 settings.py 示例:
# settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "users",  # 假設有用戶相關的應用
    "resumes",  # 假設有履歷相關的應用
]

# 指定未登入使用者重定向到的登入頁面
LOGIN_URL = "/users/login"

# 靜態檔案的路徑配置(如需其他設置)
STATIC_URL = "/static/"

資料操作與 request.user 的結合

問題背景

在許多多用戶應用中,不同的使用者會有各自的數據。例如:

  • 使用者 A 和使用者 B 可以分別管理各自的履歷,數據之間互不干擾。
  • 若沒有正確綁定履歷與當前使用者,所有履歷都會出現在同一列表中,導致數據混亂,甚至出現權限洩漏的風險。

當新增履歷時,應將履歷與當前使用者進行綁定,自動將當前使用者的 ID 傳入資料庫,可以改用以下方式:

更新模型結構

新增 Resume 模型欄位

為了支援使用者與履歷的關聯,我們需要在模型中加入 user 外鍵:

檔案位置:models.py

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

class Resume(models.Model):
    title = models.CharField(max_length=100)
    skill = models.CharField(max_length=300, null=True)
    location = models.CharField(max_length=200, null=True)
    content = models.TextField(null=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)

    def __str__(self):
        return f"{self.title} ({self.skill})"

代碼解析

  1. user 外鍵:關聯到 Django 內建的 User 模型,表示每份簡歷屬於某個用戶。
  2. on_delete=models.CASCADE:當用戶被刪除時,相關的簡歷資料將一併刪除。
  3. null=True:允許初始資料的 user 欄位為空,避免遷移錯誤。

執行資料遷移

在模型修改完成後,執行以下命令生成並應用遷移:

python manage.py makemigrations
python manage.py migrate

注意

若執行遷移時出現初始資料有空值的警告,可調整 user 欄位設為 null=True,如上所示,然後重新執行遷移。

建立關聯:手動設置(方法一)

透過 commit=False,我們可以暫存表單資料並手動為 user 字段賦值:

@login_required
def index(request):
    if request.POST:
        form = ResumeForm(request.POST)
        resume = form.save(commit=False)  # 保存數據,但不立即提交到資料庫
        resume.user = request.user        # 綁定當前使用者
        resume.save()                     # 提交數據到資料庫
        messages.success(request, "新增成功")
        return redirect("resumes:index")

    resumes = Resume.objects.all()
    return render(request, "resumes/index.html", {"resumes": resumes})

流程解釋:

  1. 登入檢查: @login_required 裝飾器確保只有已登入的使用者可以進入這個視圖。如果未登入,會自動重定向到設定的登入頁面。
  2. 表單提交(POST 請求):
    • if request.POST: 判斷是否有表單數據提交。
    • form = ResumeForm(request.POST) 將提交的數據綁定到 ResumeForm 表單。
    • resume = form.save(commit=False) 使用 commit=False 表示先不將數據寫入資料庫,這樣我們可以進一步操作數據(如關聯當前使用者)。
  3. 綁定當前使用者: resume.user = request.user 綁定 resumeuser 字段,將當前登入的使用者(request.user)設為該履歷的擁有者。這是將履歷與使用者綁定的關鍵步驟。
  4. 保存數據: resume.save() 這會將履歷數據保存到資料庫,包括綁定的使用者信息。
  5. 成功訊息與重定向: 使用 messages.success(request, "新增成功") 顯示成功訊息,並將使用者重定向到履歷列表頁面。
  6. 顯示履歷列表: 如果沒有提交表單,則展示所有履歷數據。

這段代碼的主要目的是將當前登入的使用者與新增的履歷關聯,然後保存數據。

建立關聯:使用 instance 預設值方法二

我們可以在初始化表單時使用 instance 設定 user

@login_required
def index(request):
    if request.POST:
        form = ResumeForm(
            request.POST,
            instance=Resume(user=request.user)  # 預設將 user 設為當前使用者
        )
        form.save()
        messages.success(request, "新增成功")
        return redirect("resumes:index")

    resumes = Resume.objects.all()
    return render(request, "resumes/index.html", {"resumes": resumes})

登入檢查: 同樣使用 @login_required 裝飾器來確保該視圖只有在使用者登入後才能訪問。

表單提交(POST 請求):

  • if request.POST: 判斷是否有表單數據提交。
  • form = ResumeForm(request.POST, instance=Resume(user=request.user)) 這裡的 instance=Resume(user=request.user) 表示在初始化表單時,將 user 字段預設為當前登入的使用者。這樣即使表單沒有明確填寫 user 字段,它也會自動從當前使用者賦值。

保存數據: 使用 form.save() 保存表單數據,這會自動將所有字段(包括 user)保存到資料庫中。

成功訊息與重定向: 顯示成功訊息 messages.success(request, "新增成功") 並重定向到履歷列表頁面。

顯示履歷列表: 如果沒有提交表單,則展示所有履歷數據。

補充討論:

在以下表單定義中,fields 只包含四個字段(titleskilllocationcontent),但我們在保存時卻能插入一個額外的 user 字段,這是為什麼?

from django.forms import ModelForm
from .models import Resume

class ResumeForm(ModelForm):
    class Meta:
        model = Resume
        fields = ["title", "skill", "location", "content"]
解答與詳解:
1. fields 的作用
  • 定義表單字段fields 是為了告訴 Django 在生成的表單中,哪些模型字段應該被顯示並供用戶填寫。
  • 不影響模型的完整性fields 不會限制模型字段的最終值,只是決定表單需要處理的字段。
    • 在上述例子中,fields 只包括 titleskilllocationcontent,但模型仍然完整地包含所有字段(如 user)。
    • 未列入 fields 的字段仍可以通過程式邏輯手動設置。
2. 如何插入未包含在 fields 的字段
  • 手動設置
    • 即使 user 不在 fields 中,我們仍然可以在保存表單時手動設置它:
resume = form.save(commit=False)  # 暫不保存到數據庫
resume.user = request.user        # 手動設置 user 字段
resume.save()                     # 最後保存到數據庫
  • 自動設置(使用 instance 預設值)
    • 我們可以在初始化表單時,通過 instance 預設字段值,減少手動設置:
form = ResumeForm(
    request.POST,
    instance=Resume(user=request.user)  # 預先設置 user 字段
)
form.save()  # 表單保存時會自動包括 user
3. 使用 instance 預設值的詳解
  • 作用instance 參數允許我們在初始化表單時提供一個基礎對象。
    • 在這裡,instance=Resume(user=request.user) 會創建一個 Resume 對象,並預設 user 字段為當前用戶。
    • 表單只處理 fields 中列出的字段,但保存時會保留 user 字段的值。
  • 好處
    • 簡潔性:避免手動設置字段(如 resume.user = request.user),邏輯集中處理。
    • 自動化:預設值會自動被表單保存,減少額外的操作。
  • 注意事項
    • 模型約束
      • 如果模型的其他字段設有約束條件(如 blank=Falsenull=False),未設置值時會導致錯誤。
      • 在初始化時,應確保這些字段能正確被賦值。
    • 數據庫唯一性
      • 如果模型中的字段設置了 unique=True(如 user),需注意避免初始化值違反唯一性約束。
4. 手動設置 vs 自動設置
  • 手動設置
    • 適合需要額外控制保存邏輯的場景。
form = ResumeForm(request.POST)
resume = form.save(commit=False)
resume.user = request.user  # 手動設置 user 字段
resume.save()
  • 自動設置(使用 instance
    • 適合邏輯簡單、字段初始化時能確保正確的場景。
form = ResumeForm(
    request.POST,
    instance=Resume(user=request.user)  # 初始化時設置 user
)
form.save()
關於「四個欄位先預帶,不一定只有四個」
  • 解釋
    • 表單中的 fields 只決定用戶在前端可以填寫的字段,但保存的對象是完整的模型。
    • 因此,除了 fields 中的字段,其他字段(如 user)可以通過程式邏輯或預設值進行設置。

小結

  1. fields 是控制表單顯示的字段,不是對模型字段的限制。
  2. 未列入 fields 的字段可以在後端手動設置或通過 instance 預設值處理。
  3. 使用 instance 可以讓代碼更簡潔,但需注意模型的約束條件。
  4. 選擇手動設置或自動設置需根據具體業務邏輯來決定。

安全限制:僅操作自己的資料

限制列表顯示

在視圖函數中,篩選出僅屬於當前使用者的資料:

@login_required
def index(request):
    resumes = Resume.objects.filter(user=request.user).order_by("-id")  # 僅顯示當前用戶的資料
    return render(request, "resumes/index.html", {"resumes": resumes})

修改、刪除僅針對自己的資料

修改和刪除操作同樣需要確保僅影響到當前使用者的資料:

修改範例

@login_required
def edit(request, id):
    resume = get_object_or_404(Resume, id=id, user=request.user)  # 限制只能操作自己的資料
    form = ResumeForm(instance=resume)

    return render(
        request,
        "resumes/edit.html",
        {"resume": resume, "form": form},
    )

刪除範例

@login_required
def delete(request, id):
    resume = get_object_or_404(Resume, id=id, user=request.user)  # 限制只能刪除自己的資料

    if request.POST:
        resume.delete()
        messages.success(request, "刪除成功")
        return redirect("resumes:index")

    return render(request, "resumes/delete.html", {"resume": resume})

小結

  • @login_required:保護資料操作,僅允許登入用戶訪問。
  • 表單數據處理:使用 commit=Falseinstance 靈活設置額外欄位。
  • 安全性:在視圖中篩選資料,確保用戶僅能操作自己的內容。

希望本篇教學能幫助新手更清楚地理解 Django 中與用戶資料相關的最佳實踐!

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *