第 19 章 用戶帳戶
Web應用程式的核心是讓任何用戶都能夠註冊帳戶並能夠使用它,不管用戶身處何方。在本章中,你將創建一些表單,讓使用者能夠添加主題和條目,以及編輯既有的條目。你還將學習Django如何防範對基於表單的網頁發起的常見攻擊,這讓你無需花太多時間考慮確保應用程式安全的問題。
然後,我們將實現一個使用者身份驗證系統。你將創建一個註冊頁面,供使用者創建帳戶,並讓有些頁面只能供已登錄的用戶訪問。接下來,我們將修改一些視圖函數,使得使用者只能看到自己的資料。你將學習如何確保使用者資料的安全。
19.1 讓使用者能夠輸入資料
建立用於創建使用者帳戶的身份驗證系統之前,我們先來添加幾個頁面,讓使用者能夠輸入資料。我們將讓使用者能夠添加新主題、添加新條目以及編輯既有條目。
當前,只有超級使用者能夠通過管理網站輸入資料。我們不想讓用戶與管理網站交互,因此我們將使用Django的表單創建工具來創建讓使用者能夠輸入資料的頁面。
19.1.1 添加新主題
首先來讓使用者能夠添加新主題。創建基於表單的頁面的方法幾乎與前面創建網頁一樣:定義一個URL,編寫一個視圖函數並編寫一個範本。一個主要差別是,需要導入包含表單的模組forms.py。
1. 用於添加主題的表單
讓使用者輸入並提交資訊的頁面都是表單,那怕它看起來不像表單。使用者輸入資訊時,我們需要進行驗證,確認提供的資訊是正確的資料類型,且不是惡意的資訊,如中斷伺服器的代碼。然後,我們再對這些有效資訊進行處理,並將其保存到資料庫的合適地方。這些工作很多都是由Django自動完成的。
在Django中,創建表單的最簡單方式是使用ModelForm,它根據我們在第18章定義的模型中的資訊自動創建表單。創建一個名為forms.py的檔,將其存儲到models.py所在的目錄中,並在其中編寫你的第一個表單:
forms.py
from django import forms
from .models import Topic
❶ class
TopicForm(forms.ModelForm):
class Meta:
❷ model = Topic
❸ fields = ['text']
❹ labels = {'text': ''}
我們首先導入了模組forms 以及要使用的模型Topic 。在❶處,我們定義了一個名為TopicForm 的類,它繼承了forms.ModelForm 。
最簡單的ModelForm 版本只包含一個內嵌的Meta 類,它告訴Django根據哪個模型創建表單,以及在表單中包含哪些欄位。在❷處,我們根據模型Topic 創建一個表單,該表單只包含欄位text (見❸)。❹處的代碼讓Django不要為欄位text 生成標籤。
2. URL模式new_topic
這個新網頁的URL應簡短而具有描述性,因此當使用者要添加新主題時,我們將切換到http://localhost:8000/new_topic/。下面是網頁new_topic 的URL模式,我們將其添加到learning_logs/urls.py中:
urls.py
--snip--
urlpatterns
= [
--snip--
# 用於添加新主題的網頁
url(r'^new_topic/$', views.new_topic,
name='new_topic'),
]
這個URL模式將請求交給視圖函數new_topic() ,接下來我們將編寫這個函數。
3. 視圖函數new_topic()
函數new_topic() 需要處理兩種情形:剛進入new_topic 網頁(在這種情況下,它應顯示一個空表單);對提交的表單數據進行處理,並將用戶重定向到網頁topics :
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from .models import Topic
from .forms import TopicForm
--snip--
def new_topic(request):
"""添加新主題"""
❶ if request.method != 'POST':
# 未提交資料:創建一個新表單
❷ form = TopicForm()
else:
# POST提交的資料,對資料進行處理
❸ form = TopicForm(request.POST)
❹ if form.is_valid():
❺ form.save()
❻ return
HttpResponseRedirect(reverse('learning_logs:topics'))
❼ context = {'form': form}
return render(request,
'learning_logs/new_topic.html', context)
我們導入了HttpResponseRedirect 類,使用者提交主題後我們將使用這個類將用戶重定向到網頁topics 。函數reverse() 根據指定的URL模型確定URL,這意味著Django將在頁面被請求時生成URL。我們還導入了剛才創建的表單TopicForm 。
4. GET請求和POST請求
創建Web應用程式時,將用到的兩種主要請求類型是GET請求和POST請求。對於只是從伺服器讀取資料的頁面,使用GET請求;在使用者需要通過表單提交資訊時,通常使用POST請求。處理所有表單時,我們都將指定使用POST方法。還有一些其他類型的請求,但這個專案沒有使用。
函數new_topic() 將請求對象作為參數。用戶初次請求該網頁時,其流覽器將發送GET請求;用戶填寫並提交表單時,其流覽器將發送POST請求。根據請求的類型,我們可以確定用戶請求的是空表單(GET請求)還是要求對填寫好的表單進行處理(POST請求)。
❶處的測試確定請求方法是GET還是POST。如果請求方法不是POST,請求就可能是GET,因此我們需要返回一個空表單(即便請求是其他類型的,返回一個空表單也不會有任何問題)。我們創建一個TopicForm 實例(見❷),將其存儲在變數form 中,再通過上下文字典將這個表單發送給範本(見❼)。由於產生實體TopicForm 時我們沒有指定任何實參,Django將創建一個可供用戶填寫的空表單。
如果請求方法為POST,將執行else 代碼塊,對提交的表單數據進行處理。我們使用使用者輸入的資料(它們存儲在request.POST 中)創建一個TopicForm 實例(見❸),這樣物件form 將包含使用者提交的資訊。
要將提交的資訊保存到資料庫,必須先通過檢查確定它們是有效的(見❹)。函數is_valid() 核實使用者填寫了所有必不可少的欄位(表單字段默認都是必不可少的),且輸入的資料與要求的欄位類型一致(例如,欄位text 少於200個字元,這是我們在第18章中的models.py中指定的)。這種自動驗證避免了我們去做大量的工作。如果所有欄位都有效,我們就可調用save() (見❺),將表單中的資料寫入資料庫。保存資料後,就可離開這個頁面了。我們使用reverse() 獲取頁面topics 的URL,並將其傳遞給HttpResponseRedirect() (見❻),後者將用戶的流覽器重定向到頁面topics 。在頁面topics 中,使用者將在主題清單中看到他剛輸入的主題。
5. 範本new_topic
下麵來創建新範本new_topic.html,用於顯示我們剛創建的表單:
new_topic.html
{% extends
"learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
❶ <form action="{% url 'learning_logs:new_topic'
%}" method='post'>
❷ {% csrf_token %}
❸ {{ form.as_p }}
❹ <button name="submit">add
topic</button>
</form>
{% endblock content %}
這個範本繼承了base.html,因此其基本結構與專案“學習筆記”的其他頁面相同。在❶處,我們定義了一個HTML表單。實參action 告訴伺服器將提交的表單數據發送到哪裡,這裡我們將它發回給視圖函數new_topic() 。實參method 讓流覽器以POST請求的方式提交資料。
Django使用範本標籤{% csrf_token %} (見❷)來防止攻擊者利用表單來獲得對伺服器未經授權的訪問(這種攻擊被稱為跨站請求偽造 )。在❸處,我們顯示表單,從中可知Django使得完成顯示表單等任務有多簡單:我們只需包含範本變數{{
form.as_p }} ,就可讓Django自動創建顯示表單所需的全部欄位。修飾符as_p 讓Django以段落格式渲染所有表單元素,這是一種整潔地顯示表單的簡單方式。
Django不會為表單創建提交按鈕,因此我們在❹處定義了一個這樣的按鈕。
6. 連結到頁面new_topic
接下來,我們在頁面topics 中添加一個到頁面new_topic 的連結:
topics.html
{%
extends "learning_logs/base.html" %}
{%
block content %}
<p>Topics</p>
<ul>
--snip--
</ul>
<a href="{% url
'learning_logs:new_topic' %}">Add a new topic:</a>
{%
endblock content %}
這個連結放在了既有主題清單的後面。圖19-1顯示了生成的表單。請使用這個表單來添加幾個新主題。
19.1.2 添加新條目
現在使用者可以添加新主題了,但他們還想添加新條目。我們將再次定義URL,編寫視圖函數和範本,並連結到添加新條目的網頁。但在此之前,我們需要在forms.py中再添加一個類。
1. 用於添加新條目的表單
我們需要創建一個與模型Entry 相關聯的表單,但這個表單的定制程度比TopicForm 要高些:
forms.py
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
--snip--
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
❶ labels = {'text': ''}
❷ widgets = {'text':
forms.Textarea(attrs={'cols': 80})}
我們首先修改了import 語句,使其除導入Topic 外,還導入Entry 。新類EntryForm 繼承了forms.ModelForm ,它包含的Meta 類指出了表單基於的模型以及要在表單中包含哪些欄位。這裡也給欄位'text' 指定了一個空標籤(見❶)。
在❷處,我們定義了屬性widgets 。小部件 (widget)是一個HTML表單元素,如單行文字方塊、多行文本區域或下拉清單。通過設置屬性widgets ,可覆蓋Django選擇的默認小部件。通過讓Django使用forms.Textarea ,我們定制了欄位'text' 的輸入小部件,將文本區域的寬度設置為80列,而不是默認的40列。這給用戶提供了足夠的空間,可以編寫有意義的條目。
2. URL模式new_entry
在用於添加新條目的頁面的URL模式中,需要包含實參topic_id ,因為條目必須與特定的主題相關聯。該URL模式如下,我們將它添加到了learning_logs/urls.py中:
urls.py
--snip--
urlpatterns
= [
--snip--
# 用於添加新條目的頁面
url(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry,
name='new_entry'),
]
這個URL模式與形式為http://localhost:8000/new_entry/id / 的URL匹配,其中 id 是一個與主題ID匹配的數位。代碼(?P<topic_id>\d+) 捕獲一個數字值,並將其存儲在變數topic_id 中。請求的URL與這個模式匹配時,Django將請求和主題ID發送給函數new_entry() 。
3. 視圖函數new_entry()
視圖函數new_entry() 與函數new_topic() 很像:
views.py
from django.shortcuts import render
--snip--
from .models import Topic
from .forms import TopicForm, EntryForm
--snip--
def new_entry(request, topic_id):
"""在特定的主題中添加新條目"""
❶ topic = Topic.objects.get(id=topic_id)
❷ if request.method != 'POST':
# 未提交資料,創建一個空表單
❸ form = EntryForm()
else:
# POST提交的資料,對資料進行處理
❹ form = EntryForm(data=request.POST)
if form.is_valid():
❺ new_entry = form.save(commit=False)
❻ new_entry.topic = topic
new_entry.save()
❼ return
HttpResponseRedirect(reverse('learning_logs:topic',
args=[topic_id]))
context = {'topic': topic, 'form': form}
return render(request,
'learning_logs/new_entry.html', context)
我們修改了import 語句,在其中包含了剛創建的EntryForm 。new_entry() 的定義包含形參topic_id ,用於存儲從URL中獲得的值。渲染頁面以及處理表單數據時,都需要知道針對的是哪個主題,因此我們使用topic_id 來獲得正確的主題(見❶)。
在❷處,我們檢查請求方法是POST還是GET。如果是GET請求,將執行if 代碼塊:創建一個空的EntryForm 實例(見❸)。如果請求方法為POST,我們就對資料進行處理:創建一個EntryForm 實例,使用request 物件中的POST資料來填充它(見❹);再檢查表單是否有效,如果有效,就設置條目物件的屬性topic ,再將條目物件保存到資料庫。
調用save() 時,我們傳遞了實參commit=False (見❺),讓Django創建一個新的條目物件,並將其存儲到new_entry 中,但不將它保存到資料庫中。我們將new_entry 的屬性topic 設置為在這個函數開頭從資料庫中獲取的主題(見❻),然後調用save() ,且不指定任何實參。這將把條目保存到資料庫,並將其與正確的主題相關聯。
在❼處,我們將使用者重定向到顯示相關主題的頁面。調用reverse() 時,需要提供兩個實參:要根據它來生成URL的URL模式的名稱;列表args ,其中包含要包含在URL中的所有實參。在這裡,清單args 只有一個元素——topic_id 。接下來,調用HttpResponseRedirect() 將使用者重定向到顯示新增條目所屬主題的頁面,使用者將在該頁面的條目清單中看到新添加的條目。
4. 範本new_entry
從下面的代碼可知,範本new_entry 類似於範本new_topic :
new_entry.html
{% extends
"learning_logs/base.html" %}
{% block content %}
❶ <p><a href="{% url
'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Add a new entry:</p>
❷ <form action="{% url 'learning_logs:new_entry'
topic.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name='submit'>add
entry</button>
</form>
{% endblock content %}
我們在頁面頂端顯示了主題(見❶),讓用戶知道他是在哪個主題中添加條目;該主題名也是一個連結,可用於返回到該主題的主頁面。
表單的實參action 包含URL中的topic_id 值,讓視圖函數能夠將新條目關聯到正確的主題(見❷)。除此之外,這個範本與範本new_topic.html完全相同。
5. 連結到頁面new_entry
接下來,我們需要在顯示特定主題的頁面中添加到頁面new_entry 的連結:
topic.html
{%
extends "learning_logs/base.html" %}
{%
block content %}
<p>Topic: {{ topic }}</p>
<p>Entries:</p>
<p>
<a href="{% url
'learning_logs:new_entry' topic.id %}">add new entry</a>
</p>
<ul>
--snip—
</ul>
{%
endblock content %}
我們在顯示條目前添加連結,因為在這種頁面中,執行的最常見的操作是添加新條目。圖19-2顯示了頁面new_entry 。現在使用者可以添加新主題,還可以在每個主題中添加任意數量的條目。請在一些既有主題中添加一些新條目,嘗試使用一下頁面new_entry 。
19.1.3 編輯條目
下面來創建一個頁面,讓使用者能夠編輯既有的條目。
1. URL模式edit_entry
這個頁面的URL需要傳遞要編輯的條目的ID。修改後的learning_logs/urls.py如下:
urls.py
--snip--
urlpatterns
= [
--snip--
# 用於編輯條目的頁面
url(r'^edit_entry/(?P<entry_id>\d+)/$', views.edit_entry,
name='edit_entry'),
]
在URL(如http://localhost:8000/edit_entry/1/)中傳遞的ID存儲在形參entry_id 中。這個URL模式將預期匹配的請求發送給視圖函數edit_entry() 。
2. 視圖函數edit_entry()
頁面edit_entry 收到GET請求時,edit_entry() 將返回一個表單,讓用戶能夠對條目進行編輯。該頁面收到POST請求(條目文本經過修訂)時,它將修改後的文本保存到資料庫中:
views.py
from django.shortcuts import render
--snip--
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
--snip--
def edit_entry(request, entry_id):
"""編輯既有條目"""
❶ entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
# 初次請求,使用當前條目填充表單
❷ form = EntryForm(instance=entry)
else:
# POST提交的資料,對資料進行處理
❸ form = EntryForm(instance=entry,
data=request.POST)
if form.is_valid():
❹ form.save()
❺ return
HttpResponseRedirect(reverse('learning_logs:topic',
args=[topic.id]))
context = {'entry': entry, 'topic':
topic, 'form': form}
return render(request,
'learning_logs/edit_entry.html', context)
我們首先需要導入模型Entry 。在❶處,我們獲取使用者要修改的條目物件,以及與該條目相關聯的主題。在請求方法為GET時將執行的if 代碼塊中,我們使用實參instance=entry 創建一個EntryForm 實例(見❷)。這個實參讓Django創建一個表單,並使用既有條目物件中的資訊填充它。使用者將看到既有的資料,並能夠編輯它們。
處理POST請求時,我們傳遞實參instance=entry 和data=request.POST (見❸),讓Django根據既有條目物件創建一個表單實例,並根據request.POST 中的相關資料對其進行修改。然後,我們檢查表單是否有效,如果有效,就調用save() ,且不指定任何實參(見❹)。接下來,我們重定向到顯示條目所屬主題的頁面(見❺),使用者將在其中看到其編輯的條目的新版本。
3. 範本edit_entry
下麵是範本edit_entry.html,它與範本new_entry.html類似:
edit_entry.html
{% extends "learning_logs/base.html"
%}
{% block content %}
<p><a href="{% url
'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Edit entry:</p>
❶ <form action="{% url 'learning_logs:edit_entry'
entry.id %}" method='post'>
{%
csrf_token %}
{{ form.as_p }}
❷ <button name="submit">save
changes</button>
</form>
{% endblock content %}
在❶處,實參action 將表單發回給函數edit_entry() 進行處理。在標籤{% url %} 中,我們將條目ID作為一個實參,讓視圖物件能夠修改正確的條目物件。我們將提交按鈕命名為save changes,以提醒用戶:按一下該按鈕將保存所做的編輯,而不是創建一個新條目(見❷)。
4. 連結到頁面edit_entry
現在,在顯示特定主題的頁面中,需要給每個條目添加到頁面edit_entry 的連結:
topic.html
--snip--
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y
H:i' }}</p>
<p>{{ entry.text|linebreaks
}}</p>
<p>
<a href="{% url
'learning_logs:edit_entry' entry.id %}">edit entry</a>
</p>
</li>
--snip--
我們將編輯連結放在每個條目的日期和文本後面。在迴圈中,我們使用範本標籤{%
url %} 根據URL模式edit_entry 和當前條目的ID屬性(entry.id )來確定URL。連結文本為"edit entry" ,它出現在頁面中每個條目的後面。圖19-3顯示了包含這些連結時,顯示特定主題的頁面是什麼樣的。
至此,“學習筆記”已具備了需要的大部分功能。使用者可添加主題和條目,還可根據需要查看任何一組條目。在下一節,我們將實現一個使用者註冊系統,讓任何人都可向“學習筆記”申請帳戶,並創建自己的主題和條目。
動手試一試
19-1 博客 :新建一個Django項目,將其命名為Blog。在這個專案中,創建一個名為blogs的應用程式,並在其中創建一個名為BlogPost 的模型。這個模型應包含title 、text 和date_added 等欄位。為這個專案創建一個超級用戶,並使用管理網站創建幾個簡短的帖子。創建一個主頁,在其中按時間順序顯示所有的帖子。
創建兩個表單,其中一個用於發佈新帖子,另一個用於編輯既有的帖子。
嘗試填寫這些表單,確認它們能夠正確地工作。
19.2 創建用戶帳戶
在這一節,我們將建立一個使用者註冊和身份驗證系統,讓使用者能夠註冊帳戶,進而登錄和註銷。我們將創建一個新的應用程式,其中包含與處理用戶帳戶相關的所有功能。我們還將對模型Topic 稍做修改,讓每個主題都歸屬於特定使用者。
19.2.1 應用程式users
我們首先使用命令startapp 來創建一個名為users 的應用程式:
(ll_env)learning_log$ python manage.py
startapp users
(ll_env)learning_log$ ls
❶ db.sqlite3 learning_log
learning_logs ll_env manage.py
users
(ll_env)learning_log$ ls users
❷ admin.py
__init__.py migrations models.py tests.py views.py
這個命令新建一個名為users的目錄(見❶),其結構與應用程式learning_logs 相同(見❷)。
1. 將應用程式users 添加到settings.py中
在settings.py中,我們需要將這個新的應用程式添加到INSTALLED_APPS 中,如下所示:
settings.py
--snip--
INSTALLED_APPS
= (
--snip--
# 我的應用程式
'learning_logs',
'users',
)
--snip--
這樣,Django將把應用程式users 包含到專案中。
2. 包含應用程式users 的URL
接下來,我們需要修改專案根目錄中的urls.py,使其包含我們將為應用程式users 定義的URL:
urls.py
from
django.conf.urls import include, url
from
django.contrib import admin
urlpatterns
= [
url(r'^admin/', include(admin.site.urls)),
url(r'^users/', include('users.urls',
namespace='users')),
url(r'', include('learning_logs.urls',
namespace='learning_logs')),
]
我們添加了一行代碼,以包含應用程式users 中的檔urls.py。這行代碼與任何以單詞users打頭的URL(如http://localhost:8000/users/login/)都匹配。我們還創建了命名空間'users' ,以便將應用程式learning_logs 的URL同應用程式users 的URL區分開來。
19.2.2 登錄頁面
我們首先來實現登錄頁面的功能。為此,我們將使用Django提供的默認登錄視圖,因此URL模式會稍有不同。在目錄learning_log/users/中,新建一個名為urls.py的檔,並在其中添加如下代碼:
urls.py
"""為應用程式users定義URL模式"""
from django.conf.urls import url
❶ from
django.contrib.auth.views import login
from . import views
urlpatterns = [
# 登錄頁面
❷ url(r'^login/$', login, {'template_name':
'users/login.html'},
name='login'),
]
我們首先導入了默認視圖login (見❶)。登錄頁面的URL模式與URL http://localhost:8000/users/login/匹配(見❷)。這個URL中的單詞users讓Django在users/urls.py中查找,而單詞login讓它將請求發送給Django默認視圖login (請注意,視圖實參為login ,而不是views.login )。鑒於我們沒有編寫自己的視圖函數,我們傳遞了一個字典,告訴Django去哪裡查找我們將編寫的範本。這個範本包含在應用程式users 而不是learning_logs 中。
1. 範本login.html
使用者請求登錄頁面時,Django將使用其默認視圖login ,但我們依然需要為這個頁面提供範本。為此,在目錄learning_log/users/中,創建一個名為templates的目錄,並在其中創建一個名為users的目錄。以下是範本login.html,你應將其存儲到目錄learning_log/users/templates/users/中:
login.html
{% extends
"learning_logs/base.html" %}
{% block content %}
❶ {% if form.errors %}
<p>Your username and password didn't
match. Please try again.</p>
{% endif %}
❷ <form method="post"
action="{% url 'users:login' %}">
{% csrf_token %}
❸ {{ form.as_p }}
❹ <button name="submit">log
in</button>
❺ <input type="hidden"
name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
這個範本繼承了base.html,旨在確保登錄頁面的外觀與網站的其他頁面相同。請注意,一個應用程式中的範本可繼承另一個應用程式中的範本。
如果表單的errors 屬性被設置,我們就顯示一條錯誤消息(見❶),指出輸入的用戶名—密碼對與資料庫中存儲的任何用戶名—密碼對都不匹配。
我們要讓登錄視圖處理表單,因此將實參action 設置為登錄頁面的URL(見❷)。登錄視圖將一個表單發送給範本,在範本中,我們顯示這個表單(見❸)並添加一個提交按鈕(見❹)。在❺處,我們包含了一個隱藏的表單元素——'next' ,其中的實參value 告訴Django在用戶成功登錄後將其重定向到什麼地方——在這裡是主頁。
2. 連結到登錄頁面
下麵在base.html中添加到登錄頁面的連結,讓所有頁面都包含它。使用者已登錄時,我們不想顯示這個連結,因此將它嵌套在一個{% if
%} 標籤中:
base.html
<p>
<a href="{% url
'learning_logs:index' %}">Learning Log</a> -
<a href="{% url
'learning_logs:topics' %}">Topics</a> -
❶ {% if user.is_authenticated %}
❷ Hello, {{ user.username }}.
{% else %}
❸ <a href="{% url 'users:login'
%}">log in</a>
{%
endif %}
</p>
{% block content %}{% endblock content %}
在Django身份驗證系統中,每個範本都可使用變數user ,這個變數有一個is_authenticated 屬性:如果使用者已登錄,該屬性將為True ,否則為False 。這讓你能夠向已通過身份驗證的使用者顯示一條消息,而向未通過身份驗證的使用者顯示另一條消息。
在這裡,我們向已登錄的使用者顯示一條問候語(見❶)。對於已通過身份驗證的使用者,還設置了屬性username ,我們使用這個屬性來個性化問候語,讓用戶知道他已登錄(見❷)。在❸處,對於還未通過身份驗證的使用者,我們再顯示一個到登錄頁面的連結。
3. 使用登錄頁面
前面建立了一個用戶帳戶,下面來登錄一下,看看登錄頁面是否管用。請訪問http://localhost:8000/admin/,如果你依然是以管理員的身份登錄的,請在頁眉上找到登出連結並按一下它。
註銷後,訪問http://localhost:8000/users/login/,你將看到類似於圖19-4所示的登錄頁面。輸入你在前面設置的用戶名和密碼,將進入頁面index。。在這個主頁的頁眉中,顯示了一條個性化問候語,其中包含你的用戶名。
19.2.3 註銷
現在需要提供一個讓使用者登出的途徑。我們不創建用於登出的頁面,而讓使用者只需按一下一個連結就能登出並返回到主頁。為此,我們將為登出連結定義一個URL模式,編寫一個視圖函數,並在base.html中添加一個登出連結。
1. 註銷URL
下面的代碼為登出定義了URL模式,該模式與URL http://locallwst:8000/users/logout/匹配。修改後的users/urls.py如下:
urls.py
--snip--
urlpatterns
= [
# 登錄頁面
--snip--
# 註銷
url(r'^logout/$', views.logout_view,
name='logout'),
]
這個URL模式將請求發送給函數logout_view() 。這樣給這個函數命名,旨在將其與我們將在其中調用的函數logout() 區分開來(請確保你修改的是users/urls.py,而不是learning_log/ urls.py)。
2. 視圖函數logout_view()
函數logout_view() 很簡單:只是導入Django函數logout() ,並調用它,再重定向到主頁。請打開users/views.py,並輸入下面的代碼:
views.py
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
❶ from
django.contrib.auth import logout
def logout_view(request):
"""註銷用戶"""
❷ logout(request)
❸ return
HttpResponseRedirect(reverse('learning_logs:index'))
我們從django.contrib.auth中導入了函數logout() (見❶)。在❷處,我們調用了函數logout() ,它要求將request 對象作為實參。然後,我們重定向到主頁(見❸)。
3. 連結到登出視圖
現在我們需要添加一個登出連結。我們在base.html中添加這種連結,讓每個頁面都包含它;我們將它放在標籤{% if
user.is_authenticated %} 中,使得僅當用戶登錄後才能看到它:
base.html
--snip—
{% if user.is_authenticated %}
Hello, {{ user.username }}.
<a href="{% url 'users:logout'
%}">log out</a>
{% else %}
<a href="{% url 'users:login'
%}">log in</a>
{% endif %}
--snip--
圖19-5顯示了使用者登錄後看到的主頁。這裡的重點是創建能夠正確工作的網站,因此幾乎沒有設置任何樣式。確定所需的功能都能正確運行後,我們將設置這個網站的樣式,使其看起來更專業。
19.2.4 註冊頁面
下面來創建一個讓新使用者能夠註冊的頁面。我們將使用Django提供的表單UserCreationForm ,但編寫自己的視圖函數和範本。
1. 註冊頁面的URL模式
下面的代碼定義了註冊頁面的URL模式,它也包含在users/urls.py中:
urls.py
--snip--
urlpatterns
= [
# 登錄頁面
--snip--
# 註冊頁面
url(r'^register/$', views.register,
name='register'),
]
這個模式與URL http://localhost:8000/users/register/匹配,並將請求發送給我們即將編寫的函數register() 。
2. 視圖函數register()
在註冊頁面首次被請求時,視圖函數register() 需要顯示一個空的註冊表單,並在用戶提交填寫好的註冊表單時對其進行處理。如果註冊成功,這個函數還需讓使用者自動登錄。請在users/views.py中添加如下代碼:
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.contrib.auth import login,
logout, authenticate
from django.contrib.auth.forms import
UserCreationForm
def logout_view(request):
--snip--
def register(request):
"""註冊新用戶"""
if
request.method != 'POST':
# 顯示空的註冊表單
❶ form = UserCreationForm()
else:
# 處理填寫好的表單
❷ form = UserCreationForm(data=request.POST)
❸ if form.is_valid():
❹ new_user = form.save()
# 讓用戶自動登錄,再重定向到主頁
❺ authenticated_user =
authenticate(username=new_user.username,
password=request.POST['password1'])
❻ login(request, authenticated_user)
❼ return
HttpResponseRedirect(reverse('learning_logs:index'))
context = {'form': form}
return render(request,
'users/register.html', context)
我們首先導入了函數render() ,然後導入了函數login() 和authenticate() ,以便在用戶正確地填寫了註冊資訊時讓其自動登錄。我們還導入了默認表單UserCreationForm 。在函數register() 中,我們檢查要回應的是否是POST請求。如果不是,就創建一個UserCreationForm 實例,且不給它提供任何初始資料(見❶)。
如果回應的是POST請求,我們就根據提交的資料創建一個UserCreationForm 實例(見❷),並檢查這些資料是否有效:就這裡而言,是用戶名未包含非法字元,輸入的兩個密碼相同,以及使用者沒有試圖做惡意的事情。
如果提交的資料有效,我們就調用表單的方法save() ,將用戶名和密碼的散列值保存到資料庫中(見❹)。方法save() 返回新創建的使用者物件,我們將其存儲在new_user 中。
保存使用者的資訊後,我們讓使用者自動登錄,這包含兩個步驟。首先,我們調用authenticate() ,並將實參new_user.username 和密碼傳遞給它(見❺)。使用者註冊時,被要求輸入密碼兩次;由於表單是有效的,我們知道輸入的這兩個密碼是相同的,因此可以使用其中任何一個。在這裡,我們從表單的POST資料中獲取與鍵'password1' 相關聯的值。如果用戶名和密碼無誤,方法authenticate() 將返回一個通過了身份驗證的使用者物件,而我們將其存儲在authenticated_user 中。接下來,我們調用函數login() ,並將物件request 和authenticated_user 傳遞給它(見❻),這將為新用戶創建有效的會話。最後,我們將用戶重定向到主頁(見❼),其頁眉中顯示了一條個性化的問候語,讓用戶知道註冊成功了。
3. 註冊範本
註冊頁面的範本與登錄頁面的範本類似,請務必將其保存到login.html所在的目錄中:
register.html
{%
extends "learning_logs/base.html" %}
{%
block content %}
<form method="post"
action="{% url 'users:register' %}">
{% csrf_token %}
{{ form.as_p }}
<button
name="submit">register</button>
<input type="hidden"
name="next" value="{% url 'learning_logs:index' %}" />
</form>
{%
endblock content %}
這裡也使用了方法as_p ,讓Django在表單中正確地顯示所有的欄位,包括錯誤消息——如果使用者沒有正確地填寫表單。
4. 連結到註冊頁面
接下來,我們添加這樣的代碼,即在使用者沒有登錄時顯示到註冊頁面的連結:
base.html
--snip--
{% if user.is_authenticated %}
Hello, {{ user.username }}.
<a href="{% url 'users:logout'
%}">log out</a>
{% else %}
<a href="{% url 'users:register'
%}">register</a> -
<a href="{% url 'users:login'
%}">log in</a>
{% endif %}
--snip--
現在,已登錄的用戶看到的是個性化的問候語和登出連結,而未登錄的使用者看到的是註冊連結和登錄連結。請嘗試使用註冊頁面創建幾個用戶名各不相同的用戶帳戶。
在下一節,我們將對一些頁面進行限制,僅讓已登錄的用戶訪問它們,我們還將確保每個主題都屬於特定使用者。
注意 這裡的註冊系統允許使用者創建任意數量的帳戶。有些系統要求使用者確認其身份:發送一封確認郵件,用戶回復後其帳戶才生效。通過這樣做,系統生成的垃圾帳戶將比這裡使用的簡單系統少。然而,學習創建應用程式時,完全可以像這裡所做的那樣,使用簡單的使用者註冊系統。
動手試一試
19-2 博客帳戶 :在你為完成練習19-1而開發的專案Blog中,添加一個使用者身份驗證和註冊系統。讓已登錄的使用者在螢幕上看到其用戶名,並讓未註冊的使用者看到一個到註冊頁面的連結。
19.3 讓使用者擁有自己的資料
使用者應該能夠輸入其專有的資料,因此我們將創建一個系統,確定各項資料所屬的使用者,再限制對頁面的訪問,讓使用者只能使用自己的資料。
在本節中,我們將修改模型Topic ,讓每個主題都歸屬於特定使用者。這也將影響條目,因為每個條目都屬於特定的主題。我們先來限制對一些頁面的訪問。
19.3.1 使用@login_required 限制訪問
Django提供了裝飾器@login_required ,讓你能夠輕鬆地實現這樣的目標:對於某些頁面,只允許已登錄的用戶訪問它們。裝飾器 (decorator)是放在函式定義前面的指令,Python在函數運行前,根據它來修改函數代碼的行為。下面來看一個示例。
1. 限制對topics 頁面的訪問
每個主題都歸特定使用者所有,因此應只允許已登錄的使用者請求topics 頁面。為此,在learning_logs/views.py中添加如下代碼:
views.py
--snip--
from
django.core.urlresolvers import reverse
from
django.contrib.auth.decorators import login_required
from .models
import Topic, Entry
--snip--
@login_required
def
topics(request):
"""顯示所有的主題"""
--snip--
我們首先導入了函數login_required() 。我們將login_required() 作為裝飾器用於視圖函數topics() ——在它前面加上符號@ 和login_required ,讓Python在運行topics() 的代碼前先運行login_required() 的代碼。
login_required() 的代碼檢查用戶是否已登錄,僅當用戶已登錄時,Django才運行topics() 的代碼。如果使用者未登錄,就重定向到登錄頁面。
為實現這種重定向,我們需要修改settings.py,讓Django知道到哪裡去查找登錄頁面。請在settings.py末尾添加如下代碼:
settings.py
"""
專案learning_log的Django設置
--snip--
# 我的設置
LOGIN_URL
= '/users/login/'
現在,如果未登錄的用戶請求裝飾器@login_required 的保護頁面,Django將重定向到settings.py中的LOGIN_URL 指定的URL。
要測試這個設置,可註銷並進入主頁。然後,按一下連結Topics,這將重定向到登錄頁面。接下來,使用你的帳戶登錄,並再次按一下主頁中的Topics連結,你將看到topics頁面。
2. 全面限制對專案“學習筆記”的訪問
Django讓你能夠輕鬆地限制對頁面的訪問,但你必須針對要保護哪些頁面做出決定。最好先確定專案的哪些頁面不需要保護,再限制對其他所有頁面的訪問。你可以輕鬆地修改過於嚴格的訪問限制,其風險比不限制對敏感頁面的訪問更低。
在專案“學習筆記”中,我們將不限制對主頁、註冊頁面和登出頁面的訪問,並限制對其他所有頁面的訪問。
在下麵的learning_logs/views.py中,對除index() 外的每個視圖都應用了裝飾器@login_required :
views.py
--snip--
@login_required
def
topics(request):
--snip--
@login_required
def
topic(request, topic_id):
--snip--
@login_required
def
new_topic(request):
--snip--
@login_required
def
new_entry(request, topic_id):
--snip--
@login_required
def
edit_entry(request, entry_id):
--snip--
如果你在未登錄的情況下嘗試訪問這些頁面,將被重定向到登錄頁面。另外,你還不能按一下到new_topic 等頁面的連結。但如果你輸入URL http://localhost:8000/new_topic/,將重定向到登錄頁面。對於所有與私有使用者資料相關的URL,都應限制對它們的訪問。
19.3.2 將資料關聯到使用者
現在,需要將資料關聯到提交它們的用戶。我們只需將最高層的資料關聯到使用者,這樣更低層的資料將自動關聯到使用者。例如,在專案“學習筆記”中,應用程式的最高層資料是主題,而所有條目都與特定主題相關聯。只要每個主題都歸屬於特定使用者,我們就能確定資料庫中每個條目的所有者。
下面來修改模型Topic ,在其中添加一個關聯到用戶的外鍵。這樣做後,我們必須對資料庫進行遷移。最後,我們必須對有些視圖進行修改,使其只顯示與當前登錄的使用者相關聯的資料。
1. 修改模型Topic
對models.py的修改只涉及兩行代碼:
models.py
from
django.db import models
from
django.contrib.auth.models import User
class
Topic(models.Model):
"""使用者要學習的主題"""
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User)
def __str__(self):
"""返回模型的字串表示"""
return self.text
class
Entry(models.Model):
--snip--
我們首先導入了django.contrib.auth 中的模型User ,然後在Topic 中添加了欄位owner ,它建立到模型User 的外鍵關係。
2. 確定當前有哪些用戶
我們遷移資料庫時,Django將對資料庫進行修改,使其能夠存儲主題和使用者之間的關聯。為執行遷移,Django需要知道該將各個既有主題關聯到哪個使用者。最簡單的辦法是,將既有主題都關聯到同一個用戶,如超級用戶。為此,我們需要知道該使用者的ID。
下面來查看已創建的所有用戶的ID。為此,啟動一個Django shell會話,並執行如下命令:
(venv)learning_log$ python manage.py shell
❶
>>> from django.contrib.auth.models import User
❷
>>> User.objects.all()
[<User: ll_admin>, <User: eric>,
<User: willie>]
❸
>>> for user in User.objects.all():
...
print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>
在❶處,我們在shell會話中導入了模型User 。然後,我們查看到目前為止都創建了哪些用戶(見❷)。輸出中列出了三個用戶:ll_admin、eric和willie。
在❸處,我們遍歷用戶列表,並列印每位用戶的用戶名和ID。Django詢問要將既有主題關聯到哪個使用者時,我們將指定其中的一個ID值。
3. 遷移資料庫
知道使用者ID後,就可以遷移資料庫了。
❶
(venv)learning_log$ python manage.py makemigrations learning_logs
❷ You are
trying to add a non-nullable field 'owner' to topic without a default;
we can't do that (the database needs
something to populate existing rows).
❸ Please
select a fix:
1) Provide a one-off default now (will be
set on all existing rows)
2) Quit, and let me add a default in models.py
❹ Select an
option: 1
❺ Please
enter the default value now, as valid Python
The datetime and django.utils.timezone
modules are available, so you can do e.g. timezone.now()
❻
>>> 1
Migrations for 'learning_logs':
0003_topic_owner.py:
-
Add field owner to topic
我們首先執行了命令makemigrations (見❶)。在❷處的輸出中,Django指出我們試圖給既有模型Topic 添加一個必不可少(不可為空)的欄位,而該欄位沒有預設值。在❸處,Django給我們提供了兩種選擇:要麼現在提供預設值,要麼退出並在models.py中添加預設值。在❹處,我們選擇了第一個選項,因此Django讓我們輸入預設值(見❺)。
為將所有既有主題都關聯到管理使用者ll_admin,我輸入了用戶ID值1(見❻)。並非必須使用超級用戶,而可使用已創建的任何用戶的ID。接下來,Django使用這個值來遷移資料庫,並生成了遷移檔0003_topic_owner.py,它在模型Topic 中添加欄位owner 。
現在可以執行遷移了。為此,在活動的虛擬環境中執行下面的命令:
(venv)learning_log$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: messages,
staticfiles
Apply all migrations: learning_logs,
contenttypes, sessions, admin, auth
--snip--
Running migrations:
Rendering model states... DONE
❶ Applying learning_logs.0003_topic_owner...
OK
(venv)learning_log$
Django應用新的遷移,結果一切順利(見❶)。
為驗證遷移符合預期,可在shell會話中像下面這樣做:
❶
>>> from learning_logs.models import Topic
❷
>>> for topic in Topic.objects.all():
...
print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
>>>
我們從learning_logs.models 中導入Topic (見❶),再遍歷所有的既有主題,並列印每個主題及其所屬的使用者(見❷)。正如你看到的,現在每個主題都屬於使用者ll_admin。
注意 你可以重置資料庫而不是遷移它,但如果這樣做,既有的資料都將丟失。一種不錯的做法是,學習如何在遷移資料庫的同時確保使用者資料的完整性。如果你確實想要一個全新的資料庫,可執行命令python
manage.py flush ,這將重建資料庫的結構。如果你這樣做,就必須重新創建超級用戶,且原來的所有資料都將丟失。
19.3.3 只允許使用者訪問自己的主題
當前,不管你以哪個用戶的身份登錄,都能夠看到所有的主題。我們來改變這種情況,只向使用者顯示屬於自己的主題。
在views.py中,對函數topics() 做如下修改:
views.py
--snip--
@login_required
def
topics(request):
"""顯示所有的主題"""
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request,
'learning_logs/topics.html', context)
--snip--
使用者登錄後,request 物件將有一個user 屬性,這個屬性存儲了有關該使用者的資訊。代碼Topic.objects.filter(owner=request.user) 讓Django只從資料庫中獲取owner 屬性為當前使用者的Topic 物件。由於我們沒有修改主題的顯示方式,因此無需對頁面topics的範本做任何修改。
要查看結果,以所有既有主題關聯到的使用者的身份登錄,並訪問topics頁面,你將看到所有的主題。然後,註銷並以另一個用戶的身份登錄,topics頁面將不會列出任何主題。
19.3.4 保護使用者的主題
我們還沒有限制對顯示單個主題的頁面的訪問,因此任何已登錄的用戶都可輸入類似於http://localhost:8000/topics/1/的URL,來訪問顯示相應主題的頁面。
你自己試一試就明白了。以擁有所有主題的使用者的身份登錄,訪問特定的主題,並複製該頁面的URL,或將其中的ID記錄下來。然後,註銷並以另一個用戶的身份登錄,再輸入顯示前述主題的頁面的URL。雖然你是以另一個用戶登錄的,但依然能夠查看該主題中的條目。
為修復這種問題,我們在視圖函數topic() 獲取請求的條目前執行檢查:
views.py
from django.shortcuts import render
❶ from
django.http import HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse
--snip--
@login_required
def topic(request, topic_id):
"""顯示單個主題及其所有的條目"""
topic = Topic.objects.get(id=topic_id)
# 確認請求的主題屬於當前使用者
❷ if topic.owner != request.user:
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries':
entries}
return render(request,
'learning_logs/topic.html', context)
--snip--
伺服器上沒有請求的資源時,標準的做法是返回404回應。在這裡,我們導入了異常Http404 (見❶),並在用戶請求它不能查看的主題時引發這個異常。收到主題請求後,我們在渲染網頁前檢查該主題是否屬於當前登錄的用戶。如果請求的主題不歸當前使用者所有,我們就引發Http404 異常(見❷),讓Django返回一個404錯誤頁面。
現在,如果你試圖查看其他使用者的主題條目,將看到Django發送的消息Page Not Found。在第20章,我們將對這個專案進行配置,讓使用者看到更合適的錯誤頁面。
19.3.5 保護頁面edit_entry
頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / ,其中 entry_id 是一個數字。下面來保護這個頁面,禁止使用者通過輸入類似於前面的URL來訪問其他用戶的條目:
views.py
--snip--
@login_required
def
edit_entry(request, entry_id):
"""編輯既有條目"""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user:
raise Http404
if request.method != 'POST':
# 初次請求,使用當前條目的內容填充表單
--snip--
我們獲取指定的條目以及與之相關聯的主題,然後檢查主題的所有者是否是當前登錄的用戶,如果不是,就引發Http404 異常。
19.3.6 將新主題關聯到當前使用者
當前,用於添加新主題的頁面存在問題,因此它沒有將新主題關聯到特定使用者。如果你嘗試添加新主題,將看到錯誤消息IntegrityError ,指出learning_logs_topic.user_id 不能為NULL 。Django的意思是說,創建新主題時,你必須指定其owner 欄位的值。
由於我們可以通過request 物件獲悉當前使用者,因此存在一個修復這種問題的簡單方案。請添加下面的代碼,將新主題關聯到當前使用者:
views.py
--snip--
@login_required
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 沒有提交的資料,創建一個空表單
form = TopicForm()
else:
# POST提交的資料,對資料進行處理
form = TopicForm(request.POST)
if form.is_valid():
❶ new_topic = form.save(commit=False)
❷ new_topic.owner = request.user
❸ new_topic.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request,
'learning_logs/new_topic.html', context)
--snip--
我們首先調用form.save() ,並傳遞實參commit=False ,這是因為我們先修改新主題,再將其保存到資料庫中(見❶)。接下來,將新主題的owner 屬性設置為當前使用者(見❷)。最後,對剛定義的主題實例調用save() (見❸)。現在主題包含所有必不可少的資料,將被成功地保存。
現在,這個項目允許任何用戶註冊,而每個使用者想添加多少新主題都可以。每個使用者都只能訪問自己的資料,無論是查看資料、輸入新資料還是修改舊資料時都如此。
動手試一試
19-3 重構 :在views.py中,我們在兩個地方核實主題關聯到的使用者為當前登錄的用戶。請將執行這種檢查的代碼放在一個名為check_topic_owner() 的函數中,並在恰當的地方調用這個函數。
19-4 保護頁面new_entry :一個使用者可在另一個使用者的學習筆記中添加條目,方法是輸入這樣的URL,即其中包含輸入另一個使用者的主題的ID。為防範這種攻擊,請在保存新條目前,核實它所屬的主題歸當前使用者所有。
19-5 受保護的博客 :在你創建的專案Blog中,確保每篇博文都與特定用戶相關聯。確保任何用戶都可訪問所有的博文,但只有已登錄的用戶能夠發表博文以及編輯既有博文。在讓用戶能夠編輯其博文的視圖中,在處理表單前確認用戶編輯的是他自己發表的博文。
19.4 小結
在本章中,你學習了如何使用表單來讓使用者添加新主題、添加新條目和編輯既有條目。接下來,你學習了如何實現用戶帳戶。你讓老用戶能夠登錄和註銷,並學習了如何使用Django提供的表單UserCreationForm 讓用戶能夠創建新帳戶。
建立簡單的使用者身份驗證和註冊系統後,你通過使用裝飾器@login_required 禁止未登錄的使用者訪問特定頁面。然後,你通過使用外鍵將資料關聯到特定使用者,還學習了如何執行要求指定預設資料的資料庫遷移。
最後,你學習了如何修改視圖函數,讓使用者只能看到屬於他的資料。你使用方法filter() 來獲取合適的資料,並學習了如何將請求的資料的所有者同當前登錄的用戶進行比較。
該讓哪些資料可隨便訪問,該對哪些資料進行保護呢?這可能並非總是那麼顯而易見,但通過不斷地練習就能掌握這種技能。在本章中,我們就該如何保護使用者資料所做的決策表明,與人合作開發項目是個不錯的主意:有人對項目進行檢查的話,更容易發現其薄弱環節。
至此,我們創建了一個功能齊備的項目,它運行在本地電腦上。在本書的最後一章,我們將設置這個專案的樣式,使其更漂亮;我們還將把它部署到一台伺服器上,讓任何人都可通過互聯網註冊並創建帳戶。






0 留言:
發佈留言