4 創(chuàng)建一個社交網(wǎng)站
在上一章中,你學(xué)習(xí)了如何創(chuàng)建站點地圖和訂閱,并且為博客應(yīng)用構(gòu)建了一個搜索引擎。在這一章中,你會開發(fā)一個社交應(yīng)用。你會為用戶創(chuàng)建登錄,登出,編輯和修改密碼的功能。你會學(xué)習(xí)如何為用戶創(chuàng)建自定義的個人資料,并在網(wǎng)站中添加社交認(rèn)證。
本章會涉及以下知識點:
- 使用認(rèn)證框架
- 創(chuàng)建用戶注冊視圖
- 用自定義個人資料模型擴展
User模型 - 用
python-social-auth添加社交認(rèn)證
讓我們從創(chuàng)建新項目開始。
4.1 創(chuàng)建一個社交網(wǎng)站項目
我們將會創(chuàng)建一個社交應(yīng)用,讓用戶可以分享他們在Internet上發(fā)現(xiàn)的圖片。我們需要為該項目構(gòu)建以下元素:
- 一個認(rèn)證系統(tǒng),用于用戶注冊,登錄,編輯個人資料,修改或重置密碼
- 一個關(guān)注系統(tǒng),允許用戶互相關(guān)注
- 顯示分享的圖片,并實現(xiàn)一個書簽工具,讓用戶可以分享任何網(wǎng)站的圖片
- 每個用戶的活動信息,讓用戶可以看到他關(guān)注的用戶上傳的內(nèi)容
本章討論第一點。
4.1.1 啟動社交網(wǎng)站項目
打開終端,使用以下命令為項目創(chuàng)建一個虛擬環(huán)境,并激活:
mkdir env
virtualenv env/bookmarks
source env/bookmarks/bin/activate
終端會如下顯示你激活的虛擬環(huán)境:
(bookmarks)laptop:~ zenx$
使用以下命令,在虛擬環(huán)境中安裝Django:
pip install Django
執(zhí)行以下命令創(chuàng)建一個新項目:
django-admin startproject bookmarks
創(chuàng)建初始項目結(jié)構(gòu)之后,使用以下命令進入項目目錄,并創(chuàng)建一個account的新應(yīng)用:
cd bookmarks/
django-admin startapp account
通過把該應(yīng)用添加到settings.py文件的INSTALLED_APPS中,來激活它。把它放在INSTALLED_APPS列表的最前面:
INSTALLED_APPS = (
'account',
# ...
)
執(zhí)行下面的命令,同步INSTALLED_APPS設(shè)置中默認(rèn)應(yīng)用的模型到數(shù)據(jù)庫中:
python manage.py migrate
接下來,我們用authentication框架在項目中構(gòu)建一個認(rèn)證系統(tǒng)。
4.2 使用Django認(rèn)證框架
Django內(nèi)置一個認(rèn)證框架,可以處理用戶認(rèn)證,會話,權(quán)限和用戶組。該認(rèn)證系統(tǒng)包括常見的用戶操作視圖,比如登錄,登出,修改密碼和重置密碼。
認(rèn)證框架位于django.contrib.auth中,并且被其它Django contrib包使用。記住,你已經(jīng)在第一章中使用過認(rèn)證框架,為博客應(yīng)用創(chuàng)建了一個超級用戶,以便訪問管理站點。
當(dāng)你使用startproject命令創(chuàng)建新Django項目時,認(rèn)證框架已經(jīng)包括在項目的默認(rèn)設(shè)置中。它由django.contrib.auth應(yīng)用和以下兩個中間件(middleware)類組成(這兩個中間類位于項目的MIDDLEWARE_CLASSES設(shè)置中):
-
AuthenticationMiddleware:使用會話管理用戶和請求 -
SessionMiddleware:跨請求處理當(dāng)前會話
一個中間件是一個帶有方法的類,在解析請求或響應(yīng)時,這些方法在全局中執(zhí)行。你會在本書的好幾個地方使用中間件類。你會在第13章學(xué)習(xí)如何創(chuàng)建自定義的中間件。
該認(rèn)證框架還包括以下模塊:
-
User:一個有基礎(chǔ)字典的用戶模型;主要字段有:username,password,email,first_name,last_name和is_active。 -
Group:一個用于對用戶分類的組模型。 -
Permission:執(zhí)行特定操作的標(biāo)識。
該框架還包括默認(rèn)的認(rèn)證視圖和表單,我們之后會學(xué)習(xí)。
4.2.1 創(chuàng)建登錄視圖
我們從使用Django認(rèn)證框架允許用戶登錄網(wǎng)站開始。我們的視圖要執(zhí)行以下操作來登錄用戶:
- 通過提交表單獲得用戶名和密碼。
- 對比數(shù)據(jù)庫中的數(shù)據(jù),來驗證用戶。
- 檢查用戶是否激活。
- 用戶登錄,并開始一個認(rèn)證的會話(authenticated session)。
首先,我們將創(chuàng)建一個登錄表單。在account應(yīng)用目錄中創(chuàng)建forms.py文件,添加以下代碼:
from django import forms
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput)
該表單用于在數(shù)據(jù)庫用驗證用戶。注意,我們使用PasswordInput組件來渲染包括type="password"屬性的HTML input元素。編輯account應(yīng)用的views.py文件,添加以下代碼:
from django.shortcuts import render
from django.http import HttpResponse
from django.contrib.auth import authenticate, login
from .forms import LoginForm
def user_login(request):
if request.method == 'POST':
form = LoginForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
user = authenticate(username=cd['username'],
password=cd['password'])
if user is not None:
if user.is_active:
login(request, user)
return HttpResponse('Authenticated successfully')
else:
return HttpResponse('Disabled account')
else:
return HttpResponse('Invalid login')
else:
form = LoginForm()
return render(request, 'account/login.html', {'form': form})
這是我們在視圖中所做的基本登錄操作:當(dāng)使用GET請求調(diào)用user_login視圖時,我們使用form = LoginForm()實例化一個新的登錄表單,用于在模板中顯示。當(dāng)用戶通過POST提交表單時,我們執(zhí)行以下操作:
- 使用
form = LoginForm(request.POST)實例化帶有提交的數(shù)據(jù)的表單。 - 檢查表單是否有效。如果無效,則在模板中顯示表單錯誤(例如,用戶沒有填寫某個字段)。
- 如果提交的數(shù)據(jù)有效,我們使用
authenticate()方法,在數(shù)據(jù)庫中驗證用戶。該方法接收username和password參數(shù),如果用戶驗證成功,則返回User對象,否則返回None。如果用戶沒有通過驗證,我們返回一個原始的HttpResponse,顯示一條消息。 - 如果用戶驗證成功,我們通過
is_active屬性檢查用戶是否激活。這是DjangoUser模型的屬性。如果用戶沒有激活,我們返回一個HttpResponse顯示信息。 - 如果是激活的用戶,我們在網(wǎng)站登錄用戶。我們調(diào)用
login()方法,把用戶設(shè)置在session中,并返回一條成功消息。
注意
authenticate和login之間的區(qū)別:authenticate()方法檢查用戶的認(rèn)證信息,如果正確,則返回User對象;login()在當(dāng)前session中設(shè)置用戶。
現(xiàn)在,你需要為該視圖創(chuàng)建URL模式。在account應(yīng)用目錄中創(chuàng)建urls.py文件,并添加以下代碼:
from django.conf.urls import url
from . import views
urlpatterns = [
# post views
url(r'^login/$', views.user_login, name='login'),
]
編輯bookmarks項目目錄中的urls.py文件,在其中包括account應(yīng)用的URL模式:
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^account/', include('account.urls')),
]
現(xiàn)在可以通過URL訪問登錄視圖了。是時候為該視圖創(chuàng)建一個模板了。因為該項目還沒有模板,所以你可以創(chuàng)建一個基礎(chǔ)模板,在登錄模板中擴展它。在account應(yīng)用目錄中創(chuàng)建以下文件和目錄:
templates/
account/
login.html
base.html
編輯base.html文件,添加以下代碼:
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<span class="logo">Bookmarks</span>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>
這是網(wǎng)址的基礎(chǔ)模板。跟之前的項目一樣,我們在主模板中包括CSS樣式。該基礎(chǔ)模板定義了title和content區(qū)域,可以被從它擴展的模板填充內(nèi)容。
讓我們?yōu)榈卿洷韱蝿?chuàng)建模板。打開account/login.html模板,添加以下代碼:
{% extends "base.html" %}
{% block title %}Log-in{% endblock %}
{% block content %}
<h1>Log-in</h1>
<p>Please, user the following form to log-in</p>
<form action="." method="post">
{{ form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Log-in"></p>
</form>
{% endblock %}
該模板包括了在視圖中實例化的表單。因為我們的表單會通過POST提交,所以我們使用{% csrf_token %}模板標(biāo)簽進行CSRF保護。你在第2章學(xué)習(xí)了CSRF保護。
現(xiàn)在數(shù)據(jù)庫中還沒有用戶。首先,你需要創(chuàng)建一個超級用戶,訪問管理站點來管理其他用戶。打開命令行,執(zhí)行python manage.py createsuperuser。填寫必需的用戶名,郵箱和密碼。然后使用python manage.py runserver啟動開發(fā)服務(wù)器,并在瀏覽器中打開http://127.0.0.1:8000/admin/。使用你剛創(chuàng)建的用戶登錄管理站點。你會看到Django管理站點中包括了Django認(rèn)證框架的User和Group模型,如下圖所示:

通過管理站點創(chuàng)建一個新用戶,并在瀏覽器中打開http://127.0.0.1:8000/account/login/。你會看到包括登錄表單的模板:

現(xiàn)在,提交表單時不填其中一個字段。這時,你會看到表單是無效的,并顯示錯誤信息,如下圖所示:

如果你輸入一個不存在的用戶,或者錯誤的密碼,你會看到一條Invalid login消息。
如果你輸入有效的認(rèn)證信息,會看到一條Authenticated successfully消息,如下圖所示:

4.2.2 使用Django認(rèn)證視圖
Django在認(rèn)證框架中包括了幾個表單和視圖,你可以直接使用。你已經(jīng)創(chuàng)建的登錄視圖對于理解Django中的用戶認(rèn)證過程是一個很好的練習(xí)。然而,你在絕大部分情況下可以使用默認(rèn)的Django認(rèn)證視圖。
Django提供了以下視圖處理認(rèn)證:
-
login:操作一個登錄表單,并登錄用戶 -
logout:登出一個用戶 -
logout_then_login:登出一個用戶,并重定向用戶到登錄頁面
Django提供以下視圖處理修改密碼:
-
password_change:操作一個修改用戶密碼的表單 -
password_change_done:修改密碼后,顯示成功頁面
Django還提供以下視圖用于重置密碼:
-
password_reset:允許用戶重置密碼。它生成一個帶令牌的一次性鏈接,并發(fā)送到用戶的電子郵箱中。 -
password_reset_done:告訴用戶,重置密碼的郵件已經(jīng)發(fā)送到他的郵箱中。 -
password_reset_confirm:讓用戶設(shè)置新密碼。 -
password_reset_complete:用戶重置密碼后,顯示成功頁面。
創(chuàng)建一個帶用戶賬戶的網(wǎng)站時,這里列出的視圖會節(jié)省你很多時間。你可以覆蓋這些視圖使用的默認(rèn)值,比如需要渲染的模板的位置,或者視圖使用的表單。
你可以在這里獲得更多關(guān)于內(nèi)置的認(rèn)證視圖的信息。
4.2.3 登錄和登出視圖
編輯account應(yīng)用的urls.py文件,如下所示:
from django.conf.urls import url
from django.contrib.auth.views import login, logout, logout_then_login
from . import views
urlpatterns = [
# previous login view
# url(r'^login/$', views.user_login, name='login'),
# login / logout urls
url(r'^login/$', login, name='login'),
url(r'^logout/$', logout, name='logout'),
url(r'^logout-then-login/$', logout_then_login, name='logout_then_login'),
]
譯者注:Django新版本中,URL模式使用方式跟舊版本不一樣。
我們注釋了之前為user_login視圖創(chuàng)建的URL模式,使用了Django認(rèn)證框架的login視圖。
在account應(yīng)用的templates目錄中創(chuàng)建一個registration目錄。這是Django認(rèn)證視圖的默認(rèn)路徑,它期望你的認(rèn)證模板在這個路徑下。在新創(chuàng)建的目錄中創(chuàng)建login.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Log-in{% endblock %}
{% block content %}
<h1>Log-in</h1>
{% if form.errors %}
<p>
Your username and password didn't match.
Please try again.
</p>
{% else %}
<p>Please, user the following form to log-in</p>
{% endif %}
<div class="login-form">
<form action="{% url 'login' %}" method="post">
{{ form.as_p }}
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<p><input type="submit" value="Log-in"></p>
</form>
</div>
{% endblock %}
這個login模板跟我們之前創(chuàng)建那個很像。Django默認(rèn)使用django.contrib.auth.forms中的AuthenticationForm。該表單嘗試驗證用戶,如果登錄不成功,則拋出一個驗證錯誤。這種情況下,如果認(rèn)證信息出錯,我們可以在模板中使用{% if form.errors %}查找錯誤。注意,我們添加了一個隱藏的HTML <input>元素,用于提交名為next的變量的值。當(dāng)你在請求中傳遞一個next參數(shù)時(比如,http://127.0.0.1:8000/account/login/?next=/account/),這個變量首次被登錄視圖設(shè)置。
next參數(shù)必須是一個URL。如果指定了這個參數(shù),Django登錄視圖會在用戶登錄后,重定義到給定的URL。
現(xiàn)在,在registration模板目錄中創(chuàng)建一個logged_out.html模板,添加以下代碼:
{% extends "base.html" %}
{% block title %}Logged out{% endblock %}
{% block content %}
<h1>Logged out</h1>
<p>You have been successfully logged out. You can <a href="{% url "login" %}">log-in again></a>.</p>
{% endblock %}
用戶登出之后,Django會顯示這個模板。
為登錄和登出視圖添加URL模式和模板后,網(wǎng)站已經(jīng)可以使用Django認(rèn)證視圖登錄了。
注意,我們在
urlconf中包含的logout_then_login視圖不需要任何模板,因為它重定義到了登錄視圖。
現(xiàn)在我們開始創(chuàng)建一個新的視圖,當(dāng)用戶登錄賬號時,用于顯示用戶的儀表盤。打開account應(yīng)用的views.py文件,添加以下代碼:
from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
return render(request,
'account/dashboard.html',
{'section': 'dashboard'})
我們用認(rèn)證框架的login_required裝飾器裝飾視圖。該裝飾器檢查當(dāng)前用戶是否認(rèn)證。如果是認(rèn)證用戶,它會執(zhí)行被裝飾的視圖。如果不是認(rèn)證用戶,它會重定向用戶到登錄URL,并在登錄URL中帶上一個名為next的GET參數(shù),該參數(shù)是用戶試圖訪問的URL。通過這樣的做法,當(dāng)用戶成功登錄后,登錄視圖會重定向用戶到用戶登錄之前試圖訪問的頁面。記住,我們在登錄模板的表單中添加了一個隱藏的<input>元素就是為了這個目的。
我們還定義了一個section變量。我們用這個變量跟蹤用戶正在查看網(wǎng)站的哪一部分(section)。多個視圖可能對應(yīng)相同的部分。這是定義每個視圖對應(yīng)的section的簡便方式。
現(xiàn)在,你需要為儀表盤視圖創(chuàng)建一個模板。在templates/account/目錄下創(chuàng)建dashboard.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<p>Welcome to your dashboard.</p>
{% endblock %}
接著,在account應(yīng)用的urls.py文件中,為該視圖添加URL模式:
urlpatterns = [
# ...
url(r'^$', views.dashboard, name='dashboard'),
]
編輯項目的settings.py文件,添加以下代碼:
from django.core.urlresolvers import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('dashboard')
LOGIN_URL = reverse_lazy('login')
LOGOUT_URL = reverse_lazy('logout')
這些設(shè)置是:
-
LOGIN_REDIRECT_URL:告訴Django,如果contrib.auth.views.login視圖沒有獲得next參數(shù)時,登錄后重定向到哪個URL -
LOGIN_URL:重定向用戶登錄的URL(比如使用login_required裝飾器) -
LOGOUT_URL:重定向用戶登出的URL
我們使用reverse_lazy(),通過URL的名字動態(tài)創(chuàng)建URL。reverse_lazy()函數(shù)跟reverse()函數(shù)一樣逆向URL。當(dāng)你需要在項目URL配置加載之前逆向URL時,可以使用reverse_lazy()。
讓我們總結(jié)一下,到現(xiàn)在為止,我們做了哪些工作:
- 你在項目中添加了內(nèi)置的Django認(rèn)證登錄和登出視圖
- 你為這兩個視圖創(chuàng)建了自定義模板,并定義了一個簡單的視圖,讓用戶登錄后重定向到這個視圖
- 最后,你配置了設(shè)置,讓Django默認(rèn)使用這些URL
現(xiàn)在,我們需要把登錄和登出鏈接到基礎(chǔ)模板中,把所有功能串起來。
要做到這點,我們需要確定,無論當(dāng)前用戶是否登錄,都能顯示適當(dāng)?shù)逆溄?。通過認(rèn)證中間件,當(dāng)前用戶被設(shè)置在HttpRequest對象中。你可以通過request.user訪問。即使用戶沒有認(rèn)證,你也可以找到一個用戶對象。一個未認(rèn)證的用戶在request中是一個AnonymousUser的實例。調(diào)用request.user.is_authenticated()是檢測當(dāng)前用戶是否認(rèn)證最好的方式。
編輯base.html文件,修改ID為header的<div>,如下所示:
<div id="header">
<span class="logo">Bookmarks</span>
{% if request.user.is_authenticated %}
<ul class="menu">
<li {% if section == "dashboard" %}class="selected"{% endif %}>
<a href="{% url "dashboard" %}">My dashboard</a>
</li>
<li {% if section == "images" %}class="selected"{% endif %}>
<a href="#">Images</a>
</li>
<li {% if section == "people" %}class="selected"{% endif %}>
<a href="#">People</a>
</li>
</ul>
{% endif %}
<span class="user">
{% if request.user.is_authenticated %}
Hello {{ request.user.first_name }},
<a href="{% url "logout" %}">Logout</a>
{% else %}
<a href="{% url "login" %}">Log-in</a>
{% endif %}
</span>
</div>
正如你所看到的,我們只為認(rèn)證的用戶顯示網(wǎng)站的菜單。我們還檢查當(dāng)前的section,通過CSS為相應(yīng)的<li>項添加selected類屬性來高亮顯示菜單中的當(dāng)前section。我們還顯示用戶的姓,如果是認(rèn)證過的用戶,還顯示一個登出鏈接,否則顯示登錄鏈接。
現(xiàn)在,在瀏覽器中打開http://127.0.0.1:8000/account/login。你會看到登錄頁面。輸入有效的用戶名和密碼,點擊Log-in按鈕,你會看到這樣的頁面:

因為My dashboard有selected屬性,所以你會看到它是高亮顯示的。因為是認(rèn)證過的用戶,所以用戶的姓顯示在頭部的右邊。點擊Logout鏈接,你會看到下面的頁面:

在這個頁面中,用戶已經(jīng)登出,所以你不能再看到網(wǎng)站的菜單。現(xiàn)在頭部右邊顯示Log-in鏈接。
如果你看到的是Django管理站點的登出頁面,而不是你自己的登出頁面,檢查項目的INSTALLED_APPS設(shè)置,確保django.contrib.admin在account應(yīng)用之后。這兩個模板位于同樣的相對路徑中,Django目錄加載器會使用第一個。
4.2.4 修改密碼視圖
用戶登錄我們的網(wǎng)站后,我們需要用戶可以修改他們的密碼。我們通過集成Django認(rèn)證視圖來修改密碼。打開account應(yīng)用的urls.py文件,添加以下URL模式:
from django.contrib.auth.views import password_change
from django.contrib.auth.views import password_change_done
# change password urls
urlpatterns = [
url(r'^password-change/$', password_change, name='password_change'),
url(r'^password_change/done/$', password_change_done, name='password_change_done'),
]
password_change視圖會處理修改密碼表單,password_change_done會在用戶成功修改密碼后顯示一條成功消息。讓我們?yōu)槊總€視圖創(chuàng)建一個模板。
在account應(yīng)用的templates/registration/目錄中創(chuàng)建password_change_form.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Change you password{% endblock %}
{% block content %}
<h1>Change you password</h1>
<p>Use the form below to change your password.</p>
<form action="." method="post">
{{ form.as_p }}
<p><input type="submit" value="Change"></p>
{% csrf_token %}
</form>
{% endblock %}
該模板包括修改密碼的表單。在同一個目錄下創(chuàng)建password_change_done.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Password changed{% endblock %}
{% block content %}
<h1>Password changed</h1>
<p>Your password has been successfully changed.</p>
{% endblock %}
該模板只包括一條用戶成功修改密碼后顯示的成功消息。
在瀏覽器中打開http://127.0.0.1:8000/account/password-change/。如果用戶沒有登錄,瀏覽器會重定向到登錄頁面。當(dāng)你認(rèn)證成功后,你會看到下面的修改密碼頁面:

在表單中填寫當(dāng)前密碼和新密碼,點擊Change按鈕。你會看到以下成功頁面:

登出后,使用新密碼再次登錄,確定所有功能都能正常工作。
4.2.5 重置密碼視圖
在account應(yīng)用的urls.py文件中,為重置密碼添加以下URL模式:
from django.contrib.auth.views import password_reset
from django.contrib.auth.views import password_reset_done
from django.contrib.auth.views import password_rest_confirm
from django.contrib.auth.views import password_reset_complete
# restore password urls
url(r'^password-reset/$', password_reset, name='password_reset'),
url(r'^password-reset/done/$', password_reset_done, name='password_reset_done'),
url(r'^password-reset/confirm/(?P<uidb64>[-\w]+)/(?P<token>[-\w]+)/$', password_reset_confirm, name='password_reset_confirm'),
url(r'^password-reset/complete/$', password_reset_complete, name='password_reset_complete'),
在account應(yīng)用的templates/registration/目錄中創(chuàng)建password_reset_form.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Reset your password{% endblock %}
{% block content %}
<h1>Forgotten your password?</h1>
<p>Enter your e-mail address to obtain a new password.</p>
<form action="." method="post">
{{ form.as_p }}
<p><input type="submit" value="Send e-mail"></p>
{% csrf_token %}
</form>
{% endblock %}
在同一個目錄下創(chuàng)建password_reset_email.html文件,添加以下代碼:
Someon asked for password reset for email {{ email }}. Fllow the link below:
{{ protocol }}://{{ domain }}/{% url "password_reset_form" uidb64=uid token=token %}
Your usernmae, in case you've forgotten: {{ user.get_username }}
這個模板用于渲染發(fā)送給用戶重置密碼的郵件。
在同一個目錄下創(chuàng)建password_reset_done.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Reset your password{% endblock %}
{% block content %}
<h1>Reset your password</h1>
<p>We've emailed you instructions for setting your password.</p>
<p>If you don't receive an email, please make sure you've entered the address you registered with.</p>
{% endblock %}
創(chuàng)建另一個模板文件password_reset_confirm.html,添加以下代碼:
{% extends "base.html" %}
{% block title %}Reset your password{% endblock %}
{% block content %}
<h1>Reset your password</h1>
{% if validlink %}
<p>Please enter your new password twice:</p>
<form action="." method="post">
{{ formo.as_p }}
{% csrf_token %}
<p><input type="submit" value="Change my password" /></p>
</form>
{% else %}
<p>The password reset link was invalid, possible because it has already been used.
Please request a new password reset.</p>
{% endif %}
{% endblock %}
我們檢查提供的鏈接是否有效。Django重置頁面視圖設(shè)置該變量,并把它放在這個模板的上下文中。如果鏈接有效,我們顯示重置密碼表單。
創(chuàng)建另一個password_reset_complete.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Password reset{% endblock %}
{% block content %}
<h1>Password set</h1>
<p>Your password has been set. You can <a href="{% url "login" %}">log in now</a></p>
{% endblock %}
最后,編輯account應(yīng)用的registration/login.html模板,在<form>元素后面添加以下代碼:
<p><a href="{% url "password_reset" %}">Forgotten your password?</a></p>
現(xiàn)在,在瀏覽器中打開htth://127.0.0.1:8000/account/login/,點擊Forgotten your password?鏈接,你會看到以下鏈接:

此時,你需要在項目的settings.py文件中添加SMTP配置,讓Django可以發(fā)送郵件。我們已經(jīng)在第二章學(xué)習(xí)了如何添加郵件設(shè)置。但是在開發(fā)期間,你可以讓Django在標(biāo)準(zhǔn)輸出中寫郵件,代替通過SMTP服務(wù)發(fā)送郵件。Django提供了一個郵件后臺,可以把郵件輸出到控制臺。編輯項目的settings.py文件,添加下面這一行代碼:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND設(shè)置指定用于發(fā)送郵件的類。
回到瀏覽器,輸入已有用戶的郵箱地址,點擊Send a e-mail按鈕。你會看到以下頁面:

看一眼正在運行開發(fā)服務(wù)器的控制臺,你會看到生成的郵件:
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: lakerszhy@gmail.com
Date: Tue, 02 May 2017 03:50:20 -0000
Message-ID: <20170502035020.7440.93778@bogon>
Someon asked for password reset for email lakerszhy@gmail.com. Fllow the link below:
http://127.0.0.1:8000/account/password-reset/confirm/Mg/4lp-4b14906c833231658e9f/
Your usernmae, in case you've forgotten: antonio
郵件使用我們之間創(chuàng)建的password_reset_email.html模板渲染。重置密碼的URL包括一個Django動態(tài)生成的令牌。在瀏覽器中打開連接,會看到以下頁面:

設(shè)置新密碼的頁面對應(yīng)password_reset_confirm.html模板。填寫新密碼并點擊Change my password按鈕。Django會創(chuàng)建一個新的加密密碼,并保存到數(shù)據(jù)庫中。你會看到一個成功頁面:

現(xiàn)在你可以使用新密碼再次登錄。每個用于設(shè)置新密碼的令牌只能使用一次。如果你再次打開收到的鏈接,會看到一條令牌無效的消息。
你已經(jīng)在項目中集成了Django認(rèn)證框架的視圖。這些視圖適用于大部分場景。如果需要不同的行為,你可以創(chuàng)建自己的視圖。
4.3 用戶注冊和用戶資料
現(xiàn)在,已存在的用戶可以登錄,登出和修改密碼,如果用戶忘記密碼,可以重置密碼。現(xiàn)在,我們需要創(chuàng)建視圖,用于游客創(chuàng)建賬戶。
4.3.1 用戶注冊
讓我們創(chuàng)建一個簡單的視圖,允許用戶在我們的網(wǎng)站注冊。首先,我們必須創(chuàng)建一個表單,讓用戶輸入用戶名,姓名和密碼。編輯account應(yīng)用中的forms.py文件,添加以下代碼:
from django.contrib.auth.models import User
class UserRegistrationForm(forms.ModelForm):
password = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Repeat Password', widget=forms.PasswordInput)
class Meta:
model = User
fields = ('username', 'first_name', 'email')
def clean_password2(self):
cd = self.cleaned_data
if cd['password'] != cd['password2']:
raise forms.ValidationError("Passwords don't match.")
return cd['password2']
我們?yōu)?code>User模型創(chuàng)建了一個模型表單。在表單中,我們只包括了模型的username,first_name,email字段。這些字段會根據(jù)相應(yīng)的模型字段驗證。例如,如果用戶選擇了一個已存在的用戶名,會得到一個驗證錯誤。我們添加了兩個額外字段:password和password2,用來設(shè)置密碼和確認(rèn)密碼。我們定義了clean_password2()方法,檢查兩次輸入的密碼是否一致,如果不一致,則讓表單無效。當(dāng)我們調(diào)用表單的is_valid()方法驗證時,這個檢查會執(zhí)行。你可以為任何表單字段提供clean_<fieldname>()方法,清理特定字段的值或拋出表單驗證錯誤。表單還包括一個通用的clean()方法驗證整個表單,驗證相互依賴的字段時非常有用。
Django還在django.contrib.auth.forms中提供了UserCreationForm表單供你使用,這個表單跟我們剛創(chuàng)建的表單類似。
編輯account應(yīng)用中的views.py文件,添加以下代碼:
from .forms import LoginForm, UserRegistrationForm
def register(request):
if request.method == 'POST':
user_form = UserRegistrationForm(request.POST)
if user_form.is_valid():
# Create a new user object but avoid saving it yet
new_user = user_form.save(commit=False)
# Set the chosen password
new_user.set_password(user_form.cleaned_data['password'])
# Save the User object
new_user.save()
return render(request, 'account/register_done.html', {'new_user': new_user})
else:
user_form = UserRegistrationForm()
return render(request, 'account/register.html', {'user_form': user_form})
這個創(chuàng)建用戶賬戶的視圖非常簡單。為了安全,我們使用User模型的set_password()方法處理加密保存,來代替保存用戶輸入的原始密碼。
現(xiàn)在編輯account應(yīng)用的urls.py文件,添加以下URL模式:
url(r'^register/$', views.register, name='register')
最后,我們在account/模板目錄中創(chuàng)建register.html文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Create an account{% endblock %}
{% block content %}
<h1>Create an account</h1>
<p>Please, sign up using the following form:</p>
<form action="." method="post">
{{ user_form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Create my account"></p>
</form>
{% endblock %}
在同一個目錄中添加register_done.html模板文件,添加以下代碼:
{% extends "base.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Welcome {{ new_user.first_name }}!</h1>
<p>Your account has been successfully created. Now you can <a href="{% url "login" %}">log in</a>.</p>
{% endblock %}
現(xiàn)在,在瀏覽器中打開http://127.0.0.1:8000/account/register/,你會看到剛創(chuàng)建的注冊頁面:

為新用戶填寫信息,點擊Create my account按鈕。如果所有字段都有效,則會創(chuàng)建用戶,你會看到下面的成功消息:

點擊log in鏈接,輸入你的用戶名和密碼驗證能否訪問你的賬戶。
現(xiàn)在,你還可以在登錄模板中添加注冊鏈接。編輯registration/login.html模板,把這行代碼:
<p>Please, user the following form to log-in</p>
替換為:
<p>Please, user the following form to log-in.
If you don't have an account <a href="{% url "register" %}">register here</a></p>
我們可以通過登錄頁面訪問注冊頁面了。
4.3.2 擴展User模型
當(dāng)你必須處理用戶賬戶時,你會發(fā)現(xiàn)Django認(rèn)證框架的User模型適用于常見情況。但是User模型有非常基礎(chǔ)的字段。你可能希望擴展User模型包含額外的數(shù)據(jù)。最好的方式是創(chuàng)建一個包括所有額外字段的個人資料模型,并且與Django的User模型是一對一的關(guān)系。
編輯account應(yīng)用的models.py文件,添加以下代碼:
from django.db import models
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
date_of_birth = models.DateField(blank=True, null=True)
photo = models.ImageField(upload_to='users/%Y/%m/%d', blank=True)
def __str__(self):
return 'Pofile for User {}'.format(self.user.username)
為了讓代碼保持通用性,請使用
get_user_model()方法檢索用戶模型。同時,定義模型和用戶模型之間的關(guān)系時,使用AUTH_USER_MODEL設(shè)置引用用戶模型,而不是直接引用該用戶模型。
一對一的user字段允許我們用用戶關(guān)聯(lián)個人資料。photo字段是一個ImageField字段。你需要安裝PIL(Python Imaging Library)或Pillow(PIL的一個分支)Python包來管理圖片。在終端中執(zhí)行以下命令安裝Pillow:
pip install Pillow
為了在Django開發(fā)服務(wù)器中提供多媒體文件上傳功能,需要在項目的settings.py文件中添加以下設(shè)置:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL是用戶上傳的多媒體文件的基URL,MEDIA_ROOT是多媒體文件的本地路徑。我們根據(jù)項目路徑動態(tài)構(gòu)建該路徑,讓代碼更通用。
現(xiàn)在,編輯bookmarks項目的主urls.py文件,如下所示修改代碼:
from django.conf import settings
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
這樣,Django開發(fā)服務(wù)器將在開發(fā)過程中負(fù)責(zé)多媒體文件服務(wù)。
static()幫助函數(shù)只適用于開發(fā)環(huán)境,不適合生產(chǎn)環(huán)境。永遠(yuǎn)不要在生產(chǎn)環(huán)境使用Django為靜態(tài)文件提供服務(wù)。
打開終端執(zhí)行以下命令,為新模型創(chuàng)建數(shù)據(jù)庫遷移:
python manage.py makemigrations
你會得到這樣的輸出:
Migrations for 'account':
account/migrations/0001_initial.py
- Create model Profile
接著使用以下命令同步數(shù)據(jù)庫:
python manage.py migrate
你會看到包括下面這一樣的輸出:
Applying account.0001_initial... OK
編輯account應(yīng)用的admin.py文件,在管理站點注冊Profile模型,如下所示:
from .models import Profile
class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'date_of_birth', 'photo')
admin.site.register(Profile, ProfileAdmin)
使用python manage.py runserver命令運行開發(fā)服務(wù)器?,F(xiàn)在,你會在項目的管理站點看到Profile模型,如下圖所示:

現(xiàn)在,我們將讓用戶在網(wǎng)站上編輯個人資料。在account應(yīng)用的forms.py文件中添加以下模型表單:
from .models import Profile
class UserEditForm(forms.ModelForm):
class Meta:
model = User
fields = ('first_name', 'last_name', 'email')
class ProfileEditForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('date_of_birth', 'photo')
這些表單的作用是:
-
UserEditForm:允許用戶編輯存在內(nèi)置的User模型中的姓,名和郵箱。 -
ProfileEditForm:允許用戶編輯存在自定義的Profile模型中的額外數(shù)據(jù)。用戶可以編輯出生日期,并上傳一張圖片。
編輯account應(yīng)用的views.py文件,導(dǎo)入Profile模型:
from .models import Profile
在register視圖的new_user.save()下面添加以下代碼:
# Create the user profile
profile = Profile.objects.create(user=new_user)
當(dāng)用戶在我們網(wǎng)站注冊時,我們會創(chuàng)建一個空的個人資料關(guān)聯(lián)到用戶。你需要使用管理站點手動為之前創(chuàng)建的用戶創(chuàng)建Profile對象。
現(xiàn)在我們讓用戶可以編輯個人資料。添加以下代碼到同一個文件中:
from .forms import LoginForm, UserRegistrationForm, UserEditForm, ProfileEditForm
@login_required
def edit(request):
if request.method == 'POST':
user_form = UserEditForm(instance=request.user, data=request.POST)
profile_form = ProfileEditForm(instance=request.user.profile,
data=request.POST,
files=request.FILES)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
else:
user_form = UserEditForm(instance=request.user)
profile_form = ProfileEditForm(instance=request.user.profile)
return render(request, 'account/edit.html', {'user_form': user_form, 'profile_form': profile_form})
我們使用了login_required裝飾器,因為用戶必須認(rèn)證后才能編輯個人資料。在這里,我們使用了兩個模型表單:UserEditForm存儲內(nèi)置的User模型數(shù)據(jù),ProfileEditForm存儲額外的個人數(shù)據(jù)。我們檢查兩個表單的is_valid()方法返回True來驗證提交的數(shù)據(jù)。在這里,我們保持兩個表單,用來更新數(shù)據(jù)庫中相應(yīng)的對象。
在account應(yīng)用的urls.py文件中添加以下URL模式:
url(r'^edit/$', views.edit, name='edit')
最后,在templates/account/目錄中,為該視圖創(chuàng)建edit.html模板,添加以下代碼:
{% extends "base.html" %}
{% block title %}Edit your account{% endblock %}
{% block content %}
<h1>Edit your account</h1>
<p>You can edit your account using the following form:</p>
<form action="." method='post' enctype="multipart/form-data">
{{ user_form.as_p }}
{{ profile_form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Save changes"></p>
</form>
{% endblock %}
我們在表單中包括了
enctype="multipart/form-data",來啟用文件上傳。我們使用一個HTML表單提交user_form和profile_form兩個表單。
注冊一個新用戶,并在瀏覽器中打開http://127.0.0.1:8000/account/edit/,你會看到以下界面:

現(xiàn)在你可以編輯儀表盤頁面,來包括編輯個人資料和修改密碼的頁面鏈接。打開account/dashboard.html模板,把這一行代碼:
<p>Welcome to your dashboard.</p>
替換為:
<p>
Welcome to your dashboard.
You can <a href="{% url "edit" %}">edit your profiles</a>
or <a href="{% url "password_change" %}">change your password</a>.
</p>
用戶現(xiàn)在可以通過儀表盤訪問編輯個人資料的表單。
4.3.2.1 使用自定義User模型
Django還提供了方式,可以用自定義模型代替整個User模型。你的用戶類應(yīng)從Django的AbstractUser類繼承,它作為一個抽象模型,提供了默認(rèn)用戶的完整實現(xiàn)。你可以在這里閱讀更多關(guān)于這個模型的信息。
使用自定義用戶模型會有更多的靈活性,但它也可能給一些需要與User模型交互的可插拔應(yīng)用應(yīng)用的集成帶來一定的困難。
4.3.3 使用消息框架
處理用戶動作時,你可能想要通知用戶動作的結(jié)果。Django內(nèi)置一個消息框架,允許你顯示一次性提示。該消息框架位于django.contrib.message中,當(dāng)你用python manage.py startproject創(chuàng)建新項目時,它默認(rèn)包括在settings.py的INSTALLED_APPS列表中。你注意到,設(shè)置文件的MIDDLEWARE_CLASSES設(shè)置列表中,包括一個名為django.contrib.message.middleware.MessageMiddleware的中間件。該消息框架提供了一種簡單的方式來給用戶添加消息。消息存儲在數(shù)據(jù)庫中,并會在用戶下次請求時顯示。你可以通過導(dǎo)入消息模塊,使用簡單的快捷方式添加新消息,來在視圖中使用消息框架,如下所示:
from django.contrib import message
message.error(request, 'Something went wrong')
你可以使用add_message()方法,或者以下任何一個快捷方法創(chuàng)建新消息:
-
success():動作執(zhí)行成功后顯示成功消息 -
info():信息消息 -
waring():還沒有失敗,但很可能馬上失敗 -
error():一個不成功的操作,或某些事情失敗 -
debug():調(diào)試信息,會在生產(chǎn)環(huán)境移除或忽略
讓我們顯示消息給用戶。因為消息框架對項目來說是全局的,所以我們可以在基礎(chǔ)模板中顯示消息給用戶。打開base.html模板,在id為header和content的<div>元素之間添加以下代碼:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li class="{{ message.tags }}">
{{ message|safe }}
<a href="#" class="close">?</a>
</li>
{% endfor %}
</ul>
{% endif %}
消息框架包括一個上下文處理器(context processor),它會添加messages變量到請求上下文中。因此,你可以在模板使用該變量顯示當(dāng)前消息。
現(xiàn)在,讓我們修改edit視圖來使用消息框架。編輯account應(yīng)用的views.py文件,如下修改edit視圖:
from django.contrib import messages
@login_required
def edit(request):
if request.method == 'POST':
# ...
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, 'Profile updated successfully')
else:
messages.error(request, 'Error updating your profile')
else:
user_form = UserEditForm(instance=request.user)
# ...
當(dāng)用戶成功更新個人資料后,我們添加一條成功消息。如果任何一個表單無效,我們添加一條錯誤消息。
在瀏覽器中打開http://127.0.0.1:8000/account/edit/,并編輯你的個人資料。當(dāng)個人資料更新成功后,你會看到以下消息:

當(dāng)表單無效時,你會看到以下消息:

4.4 創(chuàng)建自定義認(rèn)證后臺
Django允許你針對不同來源進行身份驗證。AUTHENTICATION_BACKENDS設(shè)置包括了項目的認(rèn)證后臺列表。默認(rèn)情況下,該設(shè)置為:
('django.contrib.auth.backends.ModelBackend',)
默認(rèn)的ModelBackend使用django.contrib.auth的User模型,驗證數(shù)據(jù)庫中的用戶。這適用于大部分項目。但是你可以創(chuàng)建自定義的后臺,來驗證其它來源的用戶,比如一個LDAP目錄或者其它系統(tǒng)。
你可以在這里閱讀更多關(guān)于自定義認(rèn)證的信息。
一旦你使用django.contrib.auth中的authenticate()函數(shù),Django會一個接一個嘗試AUTHENTICATION_BACKENDS中定義的每一個后臺來驗證用戶,直到其中一個驗證成功。只有所有后臺都驗證失敗,才不會在站點中驗證通過。
Django提供了一種簡單的方式來定義自己的認(rèn)證后臺。一個認(rèn)證后臺是提供了以下兩個方法的類:
-
authenticate():接收用戶信息作為參數(shù),如果用戶認(rèn)證成功,則返回True,否則返回False。 -
get_user():接收用戶ID作為參數(shù),并返回一個User對象。
創(chuàng)建一個自定義認(rèn)證后臺跟編寫一個實現(xiàn)這兩個方法的Python類一樣簡單。我們會創(chuàng)建一個認(rèn)證后臺,讓用戶使用郵箱地址代替用戶名驗證。
在account應(yīng)用目錄中創(chuàng)建一個authentication.py文件,添加以下代碼:
from django.contrib.auth.models import User
class EmailAuthBackend:
"""
Authenticates using e-mail account.
"""
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
retur None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
這是一個很簡單的認(rèn)證后臺。authenticate()方法接收username和password作為可選參數(shù)。我們可以使用不同的參數(shù),但我們使用username和password確保后臺可以立即在認(rèn)證框架中工作。上面的代碼完成以下工作:
-
authenticate():我們嘗試使用給定的郵箱地址檢索用戶,并用User模型內(nèi)置的check_password()方法檢查密碼。該方法會處理密碼哈希化,并比較給定的密碼和數(shù)據(jù)庫中存儲的密碼。 -
get_user():我們通過user_id參數(shù)獲得一個用戶。在用戶會話期間,Django使用認(rèn)證用戶的后臺來檢索User對象。
編輯項目的settings.py,添加以下設(shè)置:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'account.authentication.EmailAuthBackend',
)
我們保留了默認(rèn)的ModelBackend,使用用戶名和密碼認(rèn)證,并包括了自己的基于郵箱地址的認(rèn)證后臺?,F(xiàn)在,在瀏覽器中打開http://127.0.0.1/8000/account/login/。記住,Django會試圖使用每一個后臺驗證用戶,所以你現(xiàn)在可以使用用戶名或郵箱賬號登錄。
AUTHENTICATION_ BACKENDS設(shè)置中列出的后端順序很重要。如果同樣的信息對于多個后臺都有效,Django會在第一個成功驗證用戶的后臺停止。
4.5 為網(wǎng)站添加社交認(rèn)證
你可能還想為網(wǎng)站添加社交認(rèn)證,比如使用Facebook,Twitter或Google服務(wù)認(rèn)證。Python-socail-auth是一個Python模塊,可以簡化添加社交認(rèn)證過程。通過這個模塊,你可以讓用戶使用其他服務(wù)的賬戶登錄你的網(wǎng)站。
譯者注:從2016年12月3日開始,這個模塊遷移到了Python Social Auth。原書的內(nèi)容已經(jīng)過時,所以就不翻譯了。
4.6 總結(jié)
在本章中,你學(xué)習(xí)了如何在網(wǎng)站中構(gòu)建認(rèn)證系統(tǒng)和創(chuàng)建自定義用戶資料。你還在網(wǎng)站中添加了社交認(rèn)證。
下一章中,你會學(xué)習(xí)如何創(chuàng)建一個圖片書簽系統(tǒng),生成圖片的縮略圖,以及創(chuàng)建AJAX視圖。