Django 使用者登入與資料操作的最佳實踐
更新日期: 2024 年 12 月 1 日
本文為 Django 會員系統建立教學,第 5 篇:
- 設計登入與註冊功能的基礎路由與頁面配置
- Django 使用者密碼加密方式詳解
- 使用 Django 內建功能實現使用者註冊與登入
- 使用 Django 實現安全登出功能:完整指南
- Django 使用者登入與資料操作的最佳實踐 👈 所在位置
- Django: 添加公開留言與履歷列表功能
- 使用 Alpine.js 和 Django 動態管理留言按鈕啟用狀態
- Django 收藏功能的實現:使用 ManyToMany 關係與自定義中介模型
建議閱讀本文前,先閱讀完 Django 與前端框架教學 系列文
本篇教學將詳細介紹如何在 Django 專案中實現與使用者關聯的資料操作,包括:
- 加入登入限制的裝飾器。
- 透過表單處理資料的新增與修改。
- 解決使用者 ID 無法傳遞的問題。
- 安全地限制用戶僅能操作自己的資料。
登入限制與裝飾器的應用
為確保只有登入的使用者可以訪問相關頁面,我們需要使用 @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
會檢查該使用者是否已登入:
- 若已登入:執行視圖函數,返回正常的頁面響應。
- 若未登入:重定向至登入頁面,並附帶查詢參數
?next=/profile
,告訴系統完成登入後應跳轉回/profile
。
LOGIN_URL
的設定
在 settings.py
中,LOGIN_URL
指定未登入使用者被引導到的登入頁面路徑:
LOGIN_URL = "/users/login"
作用
- 設置全局重定向路徑:未登入的使用者訪問任何被
@login_required
保護的視圖時,Django 會將他們重定向到該路徑。 - 提高可維護性:集中管理登入頁面路徑,若登入頁面改名或遷移,只需更新這一處設置。
對應的 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})"
代碼解析
user
外鍵:關聯到 Django 內建的User
模型,表示每份簡歷屬於某個用戶。on_delete=models.CASCADE
:當用戶被刪除時,相關的簡歷資料將一併刪除。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})
流程解釋:
- 登入檢查:
@login_required
裝飾器確保只有已登入的使用者可以進入這個視圖。如果未登入,會自動重定向到設定的登入頁面。 - 表單提交(POST 請求):
if request.POST:
判斷是否有表單數據提交。form = ResumeForm(request.POST)
將提交的數據綁定到ResumeForm
表單。resume = form.save(commit=False)
使用commit=False
表示先不將數據寫入資料庫,這樣我們可以進一步操作數據(如關聯當前使用者)。
- 綁定當前使用者:
resume.user = request.user
綁定resume
的user
字段,將當前登入的使用者(request.user
)設為該履歷的擁有者。這是將履歷與使用者綁定的關鍵步驟。 - 保存數據:
resume.save()
這會將履歷數據保存到資料庫,包括綁定的使用者信息。 - 成功訊息與重定向: 使用
messages.success(request, "新增成功")
顯示成功訊息,並將使用者重定向到履歷列表頁面。 - 顯示履歷列表: 如果沒有提交表單,則展示所有履歷數據。
這段代碼的主要目的是將當前登入的使用者與新增的履歷關聯,然後保存數據。
建立關聯:使用 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
只包含四個字段(title
、skill
、location
、content
),但我們在保存時卻能插入一個額外的 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
只包括title
、skill
、location
、content
,但模型仍然完整地包含所有字段(如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=False
或null=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
)可以通過程式邏輯或預設值進行設置。
- 表單中的
小結
fields
是控制表單顯示的字段,不是對模型字段的限制。- 未列入
fields
的字段可以在後端手動設置或通過instance
預設值處理。 - 使用
instance
可以讓代碼更簡潔,但需注意模型的約束條件。 - 選擇手動設置或自動設置需根據具體業務邏輯來決定。
安全限制:僅操作自己的資料
限制列表顯示
在視圖函數中,篩選出僅屬於當前使用者的資料:
@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=False
或instance
靈活設置額外欄位。 - 安全性:在視圖中篩選資料,確保用戶僅能操作自己的內容。
希望本篇教學能幫助新手更清楚地理解 Django 中與用戶資料相關的最佳實踐!