使用者驗證¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解 Django 內建的使用者驗證系統
- 實作登入功能
- 實作登出功能
- 實作註冊功能
- 實作密碼變更功能
- 實作密碼重設功能
- 使用
@login_required保護 views
為什麼需要使用者驗證?¶
在前面的章節中,我們建立了文章的建立、編輯、刪除功能。但目前有個問題:
這在實際應用中是不合理的。我們需要:
這就是「使用者驗證」要解決的問題。
Django 內建的使用者驗證系統¶
Django 提供了完整的使用者驗證系統,包含:
User Model¶
Django 內建的 User 模型包含以下欄位:
| 欄位 | 說明 |
|---|---|
username |
使用者名稱(唯一) |
password |
密碼(加密儲存) |
email |
電子郵件 |
first_name |
名字 |
last_name |
姓氏 |
is_active |
是否啟用 |
is_staff |
是否為管理員 |
is_superuser |
是否為超級使用者 |
date_joined |
註冊日期 |
last_login |
最後登入時間 |
權限系統¶
Django 的權限系統包含:
-
Permissions(權限):細粒度的權限控制
- 預設每個 model 有 4 個權限:
add、change、delete、view - 可以自訂額外的權限
- 預設每個 model 有 4 個權限:
-
Groups(群組):將使用者分組管理
- 一個群組可以包含多個權限
- 一個使用者可以屬於多個群組
-
User Permissions(使用者權限):
- 可以直接給使用者特定權限
- 也可以透過群組繼承權限
在本章節中
我們會專注在「使用者驗證」(Authentication),先讓使用者可以登入、登出、註冊等。
「授權」(Authorization,權限控制)會在後續章節詳細說明。
實作登入功能¶
Django 提供了內建的 LoginView,我們只需要設定 URL 和模板即可。
步驟 1:設定 URL¶
驗證功能是全專案共用的,所以我們將它設定在專案的主要 URL 設定中。
編輯 core/urls.py:
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
auth_urlpatterns = [ # (1)!
path(
"login/", # (2)!
auth_views.LoginView.as_view(template_name="registration/login.html"), # (3)!
name="login",
),
]
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))), # (4)!
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
- 獨立宣告驗證相關的 URL patterns
- URL 路徑為
login/(相對於auth/前綴) - 使用 Django 內建的
LoginView,模板放在registration資料夾(Django 慣例) - 透過
include引入,加上/auth前綴,namespace 為auth
程式碼重點
- 匯入 auth views:
from django.contrib.auth import views as auth_views - 獨立宣告:
auth_urlpatterns = [...]讓程式碼更清晰 - 使用 include:
path("auth/", include((auth_urlpatterns, "auth"))) - URL 前綴:所有驗證 URL 都會有
/auth/前綴(如/auth/login/) - namespace:使用
authnamespace,引用時用auth:login - 模板路徑:使用
registration/資料夾,這是 Django 的慣例 - 全專案共用:所有 app 都可以使用這些驗證功能
步驟 2:建立登入模板¶
建立 templates/registration/login.html:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
登入 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">登入</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登入</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
步驟 3:設定登入後的重導向¶
編輯 core/settings.py,在檔案最後加入:
- 登入成功後重導向到文章列表頁
- 設定登入頁面的 URL name(使用 auth namespace)
步驟 4:建立測試帳號¶
使用 Django shell 建立測試帳號:
在 Python shell 中:
from django.contrib.auth.models import User
# 建立使用者
user = User.objects.create_user(
username="test-user",
email="test@example.com",
password="test-pass123"
)
print(f"使用者 {user.username} 建立成功!")
測試登入功能¶
啟動開發伺服器:
訪問 http://127.0.0.1:8000/auth/login/:
- 輸入帳號:
test-user - 輸入密碼:
test-pass123 - 點擊「登入」
- 成功登入後會重導向到文章列表頁
登入成功!
現在你已經可以使用 Django 內建的登入功能了!
實作登出功能¶
Django 提供了內建的 LogoutView,讓我們加入登出功能。
使用 POST 方法登出
從 Django 3.0 開始,LogoutView 預設只接受 POST 請求,以防止 CSRF 攻擊。這意味著我們需要使用表單來提交登出請求,而不是簡單的連結。
步驟 1:設定 URL¶
編輯 core/urls.py,在 auth_urlpatterns 中加入登出 URL:
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/", # (1)!
auth_views.LogoutView.as_view(),
name="logout",
),
]
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
- 加入登出 URL,使用 Django 內建的
LogoutView
步驟 2:設定登出後的重導向¶
編輯 core/settings.py,加入登出重導向設定:
- 登出後重導向到登入頁面
步驟 3:在導覽列加入登入/登出連結¶
編輯 templates/base.html:
{% load django_bootstrap5 %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Django Playground" />
<meta name="keywords" content="Django, Playground" />
<title>
{% block title %}
Django 大冒險
{% endblock title %}
</title>
{% bootstrap_css %}
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
{% block extra_head %}
{% endblock extra_head %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">Django 大冒險</span>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">文章列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">管理後台</a>
</li>
{% if user.is_authenticated %} <!-- (1)! -->
<li class="nav-item">
<form method="post" action="{% url 'auth:logout' %}" class="d-inline"> <!-- (2)! -->
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link">登出 ({{ user.username }})</button> <!-- (3)! -->
</form>
</li>
{% else %} <!-- (4)! -->
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:login' %}">登入</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container my-4">
{% bootstrap_messages %}
{% block content %}
{% endblock content %}
</main>
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">Django Playground</p>
</div>
</footer>
{% bootstrap_javascript %}
{% block extra_scripts %}
{% endblock extra_scripts %}
</body>
</html>
- 檢查使用者是否已登入
- 使用 POST 表單提交登出請求(Django 3.0+ 要求使用 POST 以防止 CSRF 攻擊)
- 在登出按鈕上顯示當前登入使用者名稱
- 未登入時顯示登入連結
登出按鈕樣式
我們使用 btn btn-link class 讓 <button> 看起來像連結,同時使用 nav-link class 保持導覽列的樣式一致。d-inline class 讓表單以 inline 方式顯示。
user 變數
在 Django 模板中,user 變數會自動提供:
user.is_authenticated:檢查是否已登入user.username:使用者名稱user.email:電子郵件- 其他 User model 的欄位
測試登出功能¶
- 確保你已登入
- 點擊導覽列的「登出 (username)」按鈕
- 成功登出後會重導向到登入頁面
- 登入頁面會顯示登入表單
登出功能完成!
現在你已經可以使用登入和登出功能了!
實作註冊功能¶
步驟 1:使用內建註冊表單¶
UserCreationForm 是 Django 內建的表單,包含:
username:使用者名稱password1:密碼password2:確認密碼
它會自動:
- 檢查兩次密碼是否一致
- 驗證密碼強度
- 加密密碼
步驟 2:建立註冊 View¶
建立 core/views.py:
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import redirect, render
def register(request): # (1)!
form = UserCreationForm(request.POST or None) # (2)!
if form.is_valid():
user = form.save() # (3)!
login(request, user) # (4)!
messages.success(request, f"歡迎加入, {user.username}!")
return redirect("blog:article_list")
return render(request, "registration/register.html", {"form": form})
- 建立註冊 view
- 使用我們內建的 UserCreationForm
- 儲存新使用者
- 自動登入新使用者
程式碼重點
- 匯入 login 函式:
from django.contrib.auth import login-login()用於手動登入使用者 - 註冊後自動登入:
login(request, user)- 讓新註冊的使用者自動登入 - 重導向到首頁:
return redirect("blog:article_list")
這邊為了方便,我們將註冊放在 core/ 中,但在大型專案中可以建立獨立的 app (user 或 authentication) 並放到裡面。
步驟 3:設定 URL¶
編輯 core/urls.py,在 auth_urlpatterns 中加入註冊 URL:
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from core import views # (1)!
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/",
auth_views.LogoutView.as_view(),
name="logout",
),
path("register/", views.register, name="register"), # (2)!
]
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
- 匯入 core.views 以使用自訂的 register view
- 加入註冊 URL
步驟 4:建立註冊模板¶
建立 templates/registration/register.html:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
註冊 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">註冊</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">註冊</button>
</div>
</form>
<hr />
<p class="text-center mb-0">
已經有帳號?<a href="{% url 'auth:login' %}">立即登入</a>
</p>
</div>
</div>
</div>
</div>
{% endblock content %}
步驟 5:更新登入模板¶
回到 templates/registration/login.html,加入註冊連結:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
登入 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">登入</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登入</button>
</div>
</form>
<hr />
<p class="text-center mb-0">
還沒有帳號?<a href="{% url 'auth:register' %}">立即註冊</a> <!-- (1)! -->
</p>
</div>
</div>
</div>
</div>
{% endblock content %}
- 加入註冊連結,方便使用者快速切換到註冊頁面
步驟 6:更新導覽列¶
回到 templates/base.html,在未登入時加入註冊連結:
{% load django_bootstrap5 %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Django Playground" />
<meta name="keywords" content="Django, Playground" />
<title>
{% block title %}
Django 大冒險
{% endblock title %}
</title>
{% bootstrap_css %}
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
{% block extra_head %}
{% endblock extra_head %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">Django 大冒險</span>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">文章列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">管理後台</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<form method="post" action="{% url 'auth:logout' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link">登出 ({{ user.username }})</button>
</form>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:login' %}">登入</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:register' %}">註冊</a> <!-- (1)! -->
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container my-4">
{% bootstrap_messages %}
{% block content %}
{% endblock content %}
</main>
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">Django Playground</p>
</div>
</footer>
{% bootstrap_javascript %}
{% block extra_scripts %}
{% endblock extra_scripts %}
</body>
</html>
- 未登入時顯示註冊連結
測試註冊功能¶
- 先登出(如果已登入)
- 點擊導覽列的「註冊」連結
- 填寫註冊表單:
- 使用者名稱:
user001
- 使用者名稱:
- 點擊「註冊」
- 註冊成功後會自動登入並重導向到文章列表頁
- 導覽列的登出按鈕會顯示「登出 (user001)」
註冊成功!
現在使用者可以自行註冊帳號了!
實作密碼變更功能¶
Django 提供了內建的密碼變更 views:PasswordChangeView 和 PasswordChangeDoneView。
步驟 1:設定 URL¶
編輯 core/urls.py,在 auth_urlpatterns 中加入密碼變更 URL:
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from core import views
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/",
auth_views.LogoutView.as_view(),
name="logout",
),
path("register/", views.register, name="register"),
path(
"password-change/", # (1)!
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change.html",
success_url=reverse_lazy("auth:password_change_done"),
),
name="password_change",
),
path(
"password-change/done/", # (2)!
auth_views.PasswordChangeDoneView.as_view(
template_name="registration/password_change_done.html"
),
name="password_change_done",
),
]
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
- 密碼變更頁面
- 密碼變更成功頁面
步驟 2:建立密碼變更模板¶
建立 templates/registration/password_change.html:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
變更密碼 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">變更密碼</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %} <!-- (1)! -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">變更密碼</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
- PasswordChangeForm 包含三個欄位:舊密碼、新密碼、確認新密碼
建立 templates/registration/password_change_done.html:
{% extends "base.html" %}
{% block title %}
密碼變更成功 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-success">
<h4 class="alert-heading">密碼變更成功!</h4>
<p>你的密碼已經成功變更。</p>
<hr />
<p class="mb-0">
<a href="{% url 'blog:article_list' %}" class="btn btn-success">回到首頁</a>
</p>
</div>
</div>
</div>
{% endblock content %}
步驟 3:更新導覽列¶
回到 templates/base.html,在登入時加入變更密碼連結:
{% load django_bootstrap5 %}
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Django Playground" />
<meta name="keywords" content="Django, Playground" />
<title>
{% block title %}
Django 大冒險
{% endblock title %}
</title>
{% bootstrap_css %}
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
{% block extra_head %}
{% endblock extra_head %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">Django 大冒險</span>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">文章列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">管理後台</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:password_change' %}">變更密碼</a> <!-- (1)! -->
</li>
<li class="nav-item">
<form method="post" action="{% url 'auth:logout' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link">登出 ({{ user.username }})</button>
</form>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:login' %}">登入</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'auth:register' %}">註冊</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container my-4">
{% bootstrap_messages %}
{% block content %}
{% endblock content %}
</main>
<footer class="bg-light py-4 mt-5">
<div class="container text-center">
<p class="text-muted mb-0">Django Playground</p>
</div>
</footer>
{% bootstrap_javascript %}
{% block extra_scripts %}
{% endblock extra_scripts %}
</body>
</html>
- 登入時顯示變更密碼連結
測試密碼變更功能¶
- 確保你已登入
- 點擊導覽列的「變更密碼」連結
- 填寫表單
- 點擊「變更密碼」
- 成功後會顯示成功頁面
實作密碼重設功能¶
密碼重設功能讓使用者在忘記密碼時,可以透過電子郵件重設密碼。
Django 提供了完整的密碼重設流程 views:
PasswordResetView:輸入 emailPasswordResetDoneView:顯示已寄出訊息PasswordResetConfirmView:設定新密碼PasswordResetCompleteView:顯示完成訊息
步驟 1:設定 Email Backend¶
在開發環境中,我們使用 file-based email backend,email 會儲存在檔案中而不是真的寄出。
編輯 core/settings.py:
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" # (1)!
EMAIL_FILE_PATH = BASE_DIR / "sent_emails" # (2)!
- 使用 file-based email backend
- Email 會儲存在專案根目錄的
sent_emails資料夾
為什麼使用 file-based backend?
在開發環境中:
- 不需要設定真實的 SMTP 伺服器
- Email 會儲存在本機檔案中,方便測試
- 可以直接查看 Email 內容
在生產環境中,需要改用真實的 SMTP 設定。
步驟 2:設定 URL¶
編輯 core/urls.py,在 auth_urlpatterns 中加入密碼重設 URL:
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from core import views
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/",
auth_views.LogoutView.as_view(),
name="logout",
),
path("register/", views.register, name="register"),
path(
"password-change/",
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change.html",
success_url=reverse_lazy("auth:password_change_done"),
),
name="password_change",
),
path(
"password-change/done/",
auth_views.PasswordChangeDoneView.as_view(
template_name="registration/password_change_done.html"
),
name="password_change_done",
),
path(
"password-reset/", # (1)!
auth_views.PasswordResetView.as_view(
template_name="registration/password_reset.html",
success_url=reverse_lazy("auth:password_reset_done"),
),
name="password_reset",
),
path(
"password-reset/done/", # (2)!
auth_views.PasswordResetDoneView.as_view(
template_name="registration/password_reset_done.html"
),
name="password_reset_done",
),
path(
"password-reset/<uidb64>/<token>/", # (3)!
auth_views.PasswordResetConfirmView.as_view(
template_name="registration/password_reset_confirm.html",
success_url=reverse_lazy("auth:password_reset_complete"),
),
name="password_reset_confirm",
),
path(
"password-reset/complete/", # (4)!
auth_views.PasswordResetCompleteView.as_view(
template_name="registration/password_reset_complete.html"
),
name="password_reset_complete",
),
]
urlpatterns = [
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
- 密碼重設請求頁面(輸入 email)
- 密碼重設請求已送出頁面
- 密碼重設確認頁面(設定新密碼)
- 密碼重設完成頁面
密碼重設流程
sequenceDiagram
participant User as 使用者
participant Web as 網站
participant Email as Email 系統
User->>Web: 1. 輸入 email
Web->>Email: 2. 寄送重設連結
Web->>User: 3. 顯示「已寄出」頁面
User->>Email: 4. 查看 email
Email->>User: 5. 點擊重設連結
User->>Web: 6. 設定新密碼
Web->>User: 7. 顯示「重設完成」頁面
步驟 3:建立密碼重設模板¶
建立 templates/registration/password_reset.html:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
忘記密碼 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">忘記密碼</h2>
</div>
<div class="card-body">
<p>請輸入你的電子郵件地址,我們會寄送密碼重設連結給你。</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">送出</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
建立 templates/registration/password_reset_done.html:
{% extends "base.html" %}
{% block title %}
密碼重設信已寄出 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-info">
<h4 class="alert-heading">密碼重設信已寄出</h4>
<p>我們已經將密碼重設連結寄送到你的電子郵件地址。</p>
<p>請檢查你的信箱,並點擊連結來重設密碼。</p>
<hr />
<p class="mb-0 text-muted">
<small>
如果沒有收到信件,請檢查垃圾郵件匣。
</small>
</p>
</div>
</div>
</div>
{% endblock content %}
建立 templates/registration/password_reset_confirm.html:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
設定新密碼 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">設定新密碼</h2>
</div>
<div class="card-body">
{% if validlink %} <!-- (1)! -->
<p>請輸入你的新密碼。</p>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">設定新密碼</button>
</div>
</form>
{% else %} <!-- (2)! -->
<div class="alert alert-danger">
<h4 class="alert-heading">連結已失效</h4>
<p>這個密碼重設連結已經失效或已被使用。</p>
<hr />
<p class="mb-0">
<a href="{% url 'auth:password_reset' %}" class="btn btn-danger">重新申請</a>
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock content %}
- 連結有效時顯示表單
- 連結失效時顯示錯誤訊息
建立 templates/registration/password_reset_complete.html:
{% extends "base.html" %}
{% block title %}
密碼重設完成 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-success">
<h4 class="alert-heading">密碼重設完成!</h4>
<p>你的密碼已經成功重設。</p>
<hr />
<p class="mb-0">
<a href="{% url 'auth:login' %}" class="btn btn-success">立即登入</a>
</p>
</div>
</div>
</div>
{% endblock content %}
步驟 4:更新登入模板¶
回到 templates/registration/login.html,加入「忘記密碼?」連結:
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% block title %}
登入 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2 class="mb-0">登入</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登入</button>
</div>
</form>
<hr />
<p class="text-center">
<a href="{% url 'auth:password_reset' %}">忘記密碼?</a> <!-- (1)! -->
</p>
<p class="text-center mb-0">
還沒有帳號?<a href="{% url 'auth:register' %}">立即註冊</a>
</p>
</div>
</div>
</div>
</div>
{% endblock content %}
- 加入密碼重設連結,方便使用者在忘記密碼時重設
步驟 5:建立 Email 樣板¶
建立檔案 templates/registration/password_reset_email.html 這個檔案會被用來當作忘記密碼信件的內容
Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol }}://{{ domain }}{% url 'auth:password_reset_confirm' uidb64=uid token=token %}
測試密碼重設功能¶
首先,我們需要為測試帳號設定電子郵件。使用 Django shell:
from django.contrib.auth.models import User
user = User.objects.get(username="test-user")
user.email = "test@example.com"
user.save()
print(f"使用者 {user.username} 的 email 已設定為 {user.email}")
為什麼需要設定 email?
密碼重設功能需要透過 email 寄送重設連結,所以使用者必須有 email。
接下來測試密碼重設功能:
- 登出(如果已登入)
- 訪問登入頁面,點擊「忘記密碼?」
- 輸入電子郵件地址:
test@example.com - 點擊「送出」
- 看到「密碼重設信已寄出」頁面
- 到專案根目錄的
sent_emails資料夾查看 email - 打開 email 檔案,複製重設連結
- 在瀏覽器中訪問該連結
- 設定新密碼
- 看到「密碼重設完成」頁面
- 使用新密碼登入
查看 Email 內容
在 sent_emails 資料夾中會有一個文字檔,內容類似:
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: =?utf-8?b?5ZyoIGxvY2FsaG9zdDo4MDAwIOmAsuihjOWvhueivOmHjee9rg==?=
From: webmaster@localhost
To: admin@example.com
Date: Fri, 19 Dec 2025 08:52:13 -0000
Message-ID: <176673913372.70555.6877219139717744732@arthur-mbp14.local>
Someone asked for password reset for email test@example.com. Follow the link below:
http://localhost:8000/auth/password-reset/...
-------------------------------------------------------------------------------
複製 http://127.0.0.1:8000/auth/password-reset/... 這個連結。
保護 Views¶
現在我們要確保只有登入的使用者才能建立、編輯、刪除文章。
使用 @login_required 裝飾器¶
Django 提供了 @login_required 裝飾器來保護需要登入才能訪問的 views。
編輯 blog/views.py:
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from blog.filters import ArticleFilter
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
filter_ = ArticleFilter(
request.GET or None,
queryset=Article.objects.select_related("author").prefetch_related("tags"),
)
return render(request, "blog/article_list.html", {"filter": filter_})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
@login_required
def article_create(request):
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功建立。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_create.html", {"form": form})
@login_required
def article_edit(request, article_id):
article = get_object_or_404(Article, id=article_id)
form = ArticleForm(request.POST or None, instance=article)
if form.is_valid():
article = form.save()
messages.success(request, f"文章「{article.title}」已成功更新。")
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_edit.html", {"form": form, "article": article})
@login_required
def article_delete(request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
article.delete()
messages.success(request, f"文章「{article.title}」已成功刪除。")
return redirect("blog:article_list")
return render(request, "blog/article_delete.html", {"article": article})
- 建立文章需要登入
- 編輯文章需要登入
- 刪除文章需要登入
@login_required 的作用
當未登入的使用者訪問這些頁面時:
- 自動重導向到登入頁面
- 登入成功後會自動導回原本要訪問的頁面(使用
next參數)
測試保護功能¶
- 登出(如果已登入)
- 訪問 http://127.0.0.1:8000/blog/articles/create/
- 會自動重導向到登入頁面
- 登入後會自動導回文章建立頁面
Views 已受保護
現在只有登入的使用者才能:
- ✅ 建立文章
- ✅ 編輯文章
- ✅ 刪除文章
未登入的使用者只能瀏覽文章列表和詳情頁。
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解 Django 內建的使用者驗證系統
- 實作登入功能
- 實作登出功能
- 實作註冊功能
- 實作密碼變更功能
- 實作密碼重設功能
- 使用
@login_required保護 views
你學會了:
-
Django 的 User Model:
- 內建的使用者系統
- 使用者欄位和屬性
- 權限系統概念
-
登入與登出:
- 使用
LoginView和LogoutView - 自訂登入模板
- 設定重導向 URL
- 使用
-
使用者註冊:
- 繼承
UserCreationForm - 自動登入新使用者
- 密碼驗證和加密
- 繼承
-
密碼管理:
- 密碼變更功能
- 密碼重設流程(4 個步驟)
- File-based email backend
-
保護 Views:
- 使用
@login_required裝飾器 - 自動重導向功能(使用
next參數)
- 使用
-
全專案共用的設計:
- 在
core/urls.py中獨立宣告auth_urlpatterns - 使用
registration/資料夾存放模板 - 所有 app 都可以使用這些驗證功能
- 在