用戶賬戶
Web應用程序的核心是讓任何用戶都能夠註冊賬戶並能夠使用它。在本章中,創建一些表單,讓用戶能夠添加主題和條目,以及編輯既有的條目。還將學習Django如何防範對基於表單的網頁發起的常見攻擊,
目標:實現一個用戶身份驗證系統。創建一個註冊頁面,供用戶創建賬戶,並讓有些頁面只能供已登錄的用戶訪問。接下來,修改一些視圖函數, 使得用戶只能看到自己的數據,以及確保用戶數據的安全。
1、讓用戶能夠輸入數據
【1】添加新主題
讓用戶輸入並提交信息的頁面都是表單。用戶輸入信息時,需要進行驗證,確認提供的信息是正確的數據類型。再對這些有效信息進行處理,並將其保存到數據庫的合適地方。這些工作很多都是由Django自動完成的。 在Django中,創建表單的最簡單方式是使用ModelForm,它根據我們在上一章定義的模型中的信息自動創建表單。創建一個名爲forms.py的文件,將其存儲到models.py所在的目錄中,並在其中編寫表單:
from django import forms
from .models import Topic
class TopicForm(forms.ModelForm): # TopicForm繼承forms.ModelForm
class Meta:
model = Topic # 根據模型Topic創建表單model
fields = ['text'] # 表單只包含text字段
labels = {'text': ''} # 不要爲labels生成標籤
最簡單的ModelForm 版本只包含一個內嵌的Meta 類,它告訴Django根據哪個模型創建表單,以及在表單中包含哪些字段。
這個新網頁的URL應簡短而具有描述性,因此當用戶要添加新主題時,切換到http://localhost:8000/new_topic/。下面是網頁new_topic 的URL模式,將其添加到 learning_logs/urls.py中:
from django.conf.urls import url
from . import views
urlpatterns = [
# skip
# 用於添加新主題的網頁
url(r'^new_topic/$', views.new_topic, name='new_topic'),
]
編寫視圖new_topic:剛進入new_topic,它顯示一個空表單;對提交的表單數據進行處理,並將用戶重定位到網頁topics:
views.py
from django.shortcuts import render
from .models import Topic
from django.http import HttpResponseRedirect
from django.urls import reverse
from .forms import TopicForm
# Create your views here.
#skip
def topic(request, topic_id):
# skip
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。
創建Web應用程序時,將用到的兩種主要請求類型是GET請求和POST請求。對於只是從服務器讀取數據的頁面,使用GET請求;在用戶需要通過表單提交信息時,通常使用POST 請求。處理所有表單時,都將指定使用POST方法。
函數new_topic() 將請求對象作爲參數。用戶初次請求該網頁時,其瀏覽器將發送GET請求;用戶填寫並提交表單時,其瀏覽器將發送POST請求。根據請求的類型,可以確定用戶請求的是空表單(GET請求)還是要求對填寫好的表單進行處理(POST請求)。創建一個TopicForm 實例,將其存儲在變量form 中,再通過上下文字典將這個表單發送給模板。由於實例化TopicForm 時沒有指定任何實參,Django將創建一個可供用戶填寫的空表單。
函數is_valid() 覈實用戶填寫了所有必不可少的字段(表單字段默認都是必不可少的),且輸入的數據與要求的字段類型一致(例如,字段text 少於200個字符,這是我們在上一章中的models.py中指定的)。如果所有字段都有效,調用save() ,將表單中的數據寫入數據庫。保存數據後,就可離開這個頁面了。
創建新模板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 %}
Django使用模板標籤{% csrf_token %} 來防止攻擊者利用表單來獲得對服務器未經授權的訪問(這種攻擊被稱爲跨站請求僞造 )。只需包含模板變量{{ form.as_p }} ,就可讓Django自動創建顯示錶單所需的全部字段。修飾符as_p 讓Django以段落格式渲 染所有表單元素,這是一種整潔地顯示錶單的簡單方式。 Django不會爲表單創建提交按鈕,因此定義了一個提交的按鈕。
在頁面topics 中添加一個到頁面new_topic 的鏈接:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
# skip
</ul>
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}
運行程序顯示了生成的表單,並使用這個表單來添加幾個新主題:
【2】添加新條目
再次定義URL,編寫視圖函數和模板,並鏈接到添加新條目的網頁。在forms.py中再添加一個 類。
創建一個與模型Entry 相關聯的表單,但這個表單的定製程度比TopicForm 要高些:
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm): # TopicForm繼承forms.ModelForm
# skip
class EntryFrom(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
labels = {'text': ''}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
定義了屬性widgets 。小部件 (widget)是一個HTML表單元素,如單行文本框、多行文本區域或下拉列表。通過設置屬性widgets ,可覆蓋Django選擇的默認小 部件。通過讓Django使用forms.Textarea ,定製了字段'text' 的輸入小部件,將文本區域的寬度設置爲80列,而不是默認的40列。這給用戶提供了足夠的空間,可以 編寫有意義的條目。
在用於添加新條目的頁面的URL模式中,需要包含實參topic_id ,因爲條目必須與特定的主題相關聯。該URL模式如下,將它添加到了learning_logs/urls.py中:
urlpatterns = [
# skip
# 添加新條目
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() 。
添加視圖函數new_entry() :
# skip
from .forms import TopicForm,EntryFrom
# skip
def new_entry(request, topic_id): # 形參topic_id ,用於存儲從URL中獲得的值
# 在特定的主題中添加新條目
topic = Topic.objects.get(id=topic_id)
if request.method != 'POST':
# 未提交數據:創建一個新表單
form = EntryFrom()
else:
# POST提交數據,對數據進行處理
form = EntryFrom(data=request.POST)
print(form)
if form.is_valid():
new_entry = form.save(commit=False) # 實參commit=False,讓Django創建一個新的條目對象,並將其存儲到new_entry 中,但不將它保存到數據庫中。
new_entry.topic = topic # 將new_entry 的屬性topic 設置爲在這個函數開頭從數據庫中獲取的主題
new_entry.save()
return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id])) # 列表args ,其中包含要包含在URL中的所有實參
context = {'topic':topic,'form': form}
return render(request, 'learning_logs/new_entry.html', context)
添加模板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'> <!-- topic_id 值,讓視圖函數能夠將新條目關聯到正確的主題-->
{% csrf_token %}
{{ form.as_p }}
<button name="submit" >add entry</button>
</form>
{% endblock content %}
在顯示特定主題的頁面topic.html中添加到頁面new_entry 的鏈接。
【3】編輯條目
來創建一個頁面,讓用戶能夠編輯既有的條目。
.URL模式edit_entry :該頁面的URL需要傳遞要編輯的條目的ID。添加learning_logs/urls.py如下:
# 用於編輯條目的頁面
url(r'^edit_entry/(?P<entry_id>\d+)/$',views.edit_entry, name='edit_entry')
在views.py添加視圖函數edit_entry():
def edit_entry(request, entry_id):
# 編輯既有條目
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
# 初次請求,使用當前條目填充表單
form = EntryFrom(instance=entry)
else:
# POST提交的數據,對數據進行處理
form = EntryFrom(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)
實參instance=entry 創建一個EntryForm 實例,這個實參讓Django創建一個表單,並使用既有條目對象中的信息填充它。用戶將看到既有的數據,並能夠編輯它們。 處理POST請求時,傳遞實參instance=entry 和data=request.POST ,讓Django根據既有條目對象創建一個表單實例。
打開網頁:
模板edit_entry:
{% 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 %}
鏈接到頁面edit_entry:
在topic.html中添加到頁面edit_entry的鏈接:
{% extends 'learning_logs/base.html' %}
{% block content %}
# skip
{% 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>
# skip
{% endblock content %}
2、創建用戶賬戶
目標:將建立一個用戶註冊和身份驗證系統,讓用戶能夠註冊賬戶,進而登錄和註銷。我們將創建一個新的應用程序,其中包含與處理用戶賬戶相關的所有功能。對模型Topic 稍做修改,讓每個主題都歸屬於特定用戶。
【1】應用程序users
先使用命令startapp 來創建一個名爲users 的應用程序:
將應用程序users添加到settings.py中
INSTALLED_APPS = [
#skip
# 我的應用程序
'learning_logs',
'users'
]
包含應用程序users的URL:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('users/', include(('users.urls', "users" ),namespace='users')),
path('', include(('learning_logs.urls', "learning_logs"),namespace="learning_logs")),
]
登錄頁面:
使用Django提供的默認登錄視圖,URL模式會稍有不同。在目錄learning_log/users/中,新建一個名爲urls.py的文件,並在其中添加代碼:
# 爲應用程序users 定義URL模式
from django.urls import path
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
# 登錄頁面
path('login/',LoginView.as_view(template_name='users/login.html'),name="login"),
]
創建頁面login.html :learning_log/users/templates/users中
{% 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中添加到登錄頁面的鏈接。用戶已登錄時,不想顯示這個鏈接,因此將它嵌套在一個{% if %} 標籤中:
<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 。
使用登錄頁面:
登錄一下,看看登錄頁面是否管用。訪問http://localhost:8000/admin/,如果依然是以管理員的身份登錄的,點擊LOG OUT註銷。
註銷後,訪問http://localhost:8000/users/login/:
輸入在前面設置的用戶名和密碼:
註銷:
讓用戶單擊一個鏈接註銷並返回到主頁。爲註銷鏈接定義一個URL模式,編寫一個視圖函數,並在base.html中添加一個註銷鏈接。
註銷URL:
更新users/urls.py:
# 註銷
path('logout/', views.logout_view, name='logout'),
視圖函數:logout_view()
函數logout_view() :導入Django函數logout() ,並調用它,再重定向到主頁。
users/views.py
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout
def logout_view(request):
# 註銷用戶
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
鏈接到註銷視圖:
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 }}.
<a href="{% url 'users:logout' %}">log out</a>
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
僅當用戶登錄後才能看到它:
【4】註冊頁面
使用Django提供的表單UserCreationForm ,但編寫自己的視圖函數和模板。
urls.py:
# 註冊頁面
path('register/', views.register, name='register'),
view.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.forms import UserCreationForm
def logout_view(request):
# skip
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)
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 %}
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 }}.
<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 %}
</p>
{% block content %}{% endblock content %}
運行網頁:
3、讓用戶擁有自己的數據
創建一個系統,確定各項數據所屬的用戶,再限制對頁面的訪問,讓用戶只能使用自己的數據。 在本節中,修改模型Topic ,讓每個主題都歸屬於特定用戶。這也將影響條目,因爲每個條目都屬於特定的主題。
【1】使用@login_require限制訪問
裝飾器 (decorator)是放在函數定義前面的指 令,Python在函數運行前,根據它來修改函數代碼的行爲。
限制對topics頁面的訪問:
learning_logs/views.py
# skip
from django.contrib.auth.decorators import login_required
# skip
@login_required()
def topics(request):
# skip
# skip
login_required() 的代碼檢查用戶是否已登錄,僅當用戶已登錄時,Django才運行topics() 的代碼。如果用戶未登錄,就重定向到登錄頁面。 爲實現這種重定向,修改settings.py,讓Django知道到哪裏去查找登錄頁面。請在settings.py末尾添加如下代碼:
# 我的設置
LOGIN_URL = '/users/login/'
全面限制對項目的訪問:
在項目中,不限制對主頁、註冊頁面和註銷頁面的訪問,並限制對其他所有頁面的訪問。
在learning_logs/views.py中,對除index() 外的每個視圖都應用了裝飾器@login_required 。
在未登錄的情況下嘗試訪問這些頁面,將被重定向到登錄頁面。另外,還不能單擊到new_topic 等頁面的鏈接。但如果你輸入URL http://localhost:8000/new_topic/,將重定向到登錄頁面。
【2】將數據關聯到用戶:
要將數據關聯到提交它們的用戶。只需將最高層的數據關聯到用戶,這樣更低層的數據將自動關聯到用戶。例如,在項目“學習筆記”中,應用程序的最高層數據是 主題,而所有條目都與特定主題相關聯。只要每個主題都歸屬於特定用戶,就能確定數據庫中每個條目的所有者。
修改模型Topic:
models.py
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Topic(models.Model):
# 用戶學習的主題
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE) # 建立到模型User 的外鍵關係
def __str__(self):
# 返回模型的字符串表示
return self.text
確定當前有哪些用戶:
遷移數據庫時,Django將對數據庫進行修改,使其能夠存儲主題和用戶之間的關聯。爲執行遷移,Django需要知道該將各個既有主題關聯到哪個用戶。最簡單的辦法是,將既有主題都關聯到同一個用戶,如超級用戶。爲此,我需要知道該用戶的ID。
查看已創建的所有用戶的ID。啓動一個Django shell會話,並執行如下命令:
Django詢問要將既有主題關聯到哪個用戶時,指定其中的一個ID值。
遷移數據庫:
爲將所有既有主題都關聯到管理用戶ll_admin,輸入了用戶ID值1。並非必須使用超級用戶,而可使用已創建的任何用戶的ID。接下來,Django使用這個值來遷移數據庫,並生成了遷移文件0003_topic_owner.py,它在模型Topic 中添加字段owner 。 現在可以執行遷移了。爲此,在活動的虛擬環境中執行下面的命令:
Django應用新的遷移,結果ok。 爲驗證遷移符合預期,可在shell會話中像下面這樣做:
【3】只允許用戶訪問自己的主題:
在views.py中,對函數topics() 做如下修改:
@login_required()
def topics(request):
# 顯示所有主題
topics = Topic.objects.filter(owner=request.user).order_by('date_added') # 讓Django只從數據庫中獲 取owner 屬性爲當前用戶的Topic 對象
context = {'topics':topics}
return render(request, 'learning_logs/topics.html',context)
用戶登錄後,request 對象將有一個user 屬性,這個屬性存儲了有關該用戶的信息。
【4】保護用戶的主題:
由於還沒有限制對顯示單個主題的頁面的訪問,因此任何已登錄的用戶都可輸入類似於http://localhost:8000/topics/1/的URL,來訪問顯示相應主題的頁面。
在視圖函數topic() 獲取請求的條目前執行檢查:
# skip
from django.http import HttpResponseRedirect, Http404
# skip
@login_required
def topic(request, topic_id):
# 顯示單個主題及其所有的條目
topic = Topic.objects.get(id=topic_id)
# 確認請求的主題屬於當前用戶
if topic.owner != request.user: #如果請求的主題不歸當前用戶所有,就引發Http404異常
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic':topic,'entries':entries}
return render(request, 'learning_logs/topic.html', context)
# skip
現在查看其他用戶的主題條目,將看到Django發送的消息Page Not Found。
【5】保護頁面edit_entry
頁面edit_entry 的URL爲http://localhost:8000/edit_entry/entry_id / ,其中 entry_id 是一個數字。下面來保護這個頁面,禁止用戶通過輸入類似於前面 ]的URL來訪問其他用戶的條目:
views.py
@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':
# 初次請求,使用當前條目填充表單
form = EntryFrom(instance=entry)
# skip
【6】將新主題關聯到當前用戶
view.py
@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 # 將新主題的owner 屬性設置爲當前用戶
new_topic.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
現在,這個項目允許任何用戶註冊,而每個用戶想添加多少新主題都可以。每個用戶都只能訪問自己的數據,無論是查看數據、輸入新數據還是修改舊數據時都如此。
4、小結
學習了:
- 如何使用表單來讓用戶添加新主題、添加新條目和編輯既有條目;
- 如何實現用戶賬戶。讓老用戶能夠登錄和註銷;
- 如何使用 Django提供的表單UserCreationForm 讓用戶能夠創建新賬戶;
- 建立簡單的用戶身份驗證和註冊系統後,通過使用裝飾器@login_required 禁止未登錄的用戶訪問特定頁面;
- 通過使用外鍵將數據關聯到特定用戶;
- 如何執行要求指定默認數據的數據庫遷移;
- 如何修改視圖函數,讓用戶只能看到屬於他的數據。使用方法filter() 來獲取合適的數據;
- 如何將請求的數據的所有者同當前登錄的用戶進行比較。
下一章:Web應用程序之樣式設置 上一章:Web應用程序之樣式設置