1. 程式人生 > 其它 >Django2實戰示例 第四章 建立社交網站

Django2實戰示例 第四章 建立社交網站

第四章 建立社交網站

在之前的章節學習瞭如何建立站點地圖、訂閱資訊和建立一個全文搜尋引擎。這一章我們來開發一個社交網站。會建立使用者登入、登出、修改和重置密碼功能,為使用者建立額外的使用者資訊,以及使用第三方身份認證登入。

本章包含以下內容:

  • 使用Django內建驗證模組
  • 建立使用者註冊檢視
  • 使用自定義的使用者資訊表擴充套件使用者模型
  • 新增第三方身份認證系統

我們來建立本書的第二個專案。

1社交網站

我們將建立一個社交網站,讓使用者可以把網上看到的圖片分享到網站來。這個社交網站包含如下功能:

  • 一個供使用者註冊、登入、登出、修改和重置密碼的使用者身份驗證系統,還能夠讓使用者自行填寫使用者資訊
  • 關注系統,讓使用者可以關注其他使用者
  • 一個JS小書籤工具,讓使用者可以將外部的圖片分享(上傳)到本站
  • 一個追蹤系統,讓使用者可以看到他所關注的使用者的上傳內容

本章涉及到其中的第一個內容:使用者身份驗證系統。

1.1啟動社交網站專案

啟動系統命令列,輸入下列命令建立並激活一個虛擬環境:

mkdir env
virtualenv env/bookmarks
source env/bookmarks/bin/activate

終端會顯示當前的虛擬環境,如下:

(bookmarks)laptop:~ zenx$

在終端中安裝Django並啟動bookmarks專案:

pip install Django==2.0.5
django-admin startproject bookmarks

然後到專案根目錄內建立account應用:

cd bookmarks/
django-admin startapp account

然後在settings.py中的INSTALLED_APPS設定中啟用該應用:

INSTALLED_APPS = [
    'account.apps.AccountConfig',
    # ...
]

這裡將我們的應用放在應用列表的最前邊,原因是:我們稍後會為自己的應用編寫驗證系統的模板,Django內建的驗證系統自帶了一套模板,如此設定可以讓我們的模板覆蓋其他應用中的模板設定。Django按照INSTALLED_APPS中的順序尋找模板。

之後執行資料遷移過程。

譯者注:新建立的Django專案預設依然使用Python的SQLlite資料庫,建議讀者為每個專案配置一個新建立的資料庫。推薦使用上一章的PostgreSQL,因為本書之後還會使用PostgreSQL。

2使用Django內建驗證框架

django提供了一個驗證模組框架,具備使用者驗證,會話控制(session),許可權和使用者組功能並且自帶一組檢視,用於控制常見的使用者行為如登入、登出、修改和重置密碼。

驗證模組框架位於django.contrib.auth,也被其他Django的contrib庫所使用。在第一章裡建立超級使用者的時候,就使用到了驗證模組。

使用startproject命令建立一個新專案時,驗證模組預設已經被設定並啟用,包括INSTALLED_APPS設定中的django.contrib.auth應用,和MIDDLEWARE設定中的如下兩個中介軟體:

  • AuthenticationMiddleware:將使用者與HTTP請求聯絡起來
  • SessionMiddleware:處理當前HTTP請求的session

中介軟體是一個類,在接收HTTP請求和傳送HTTP響應的階段被呼叫,在本書的部分內容中會使用中介軟體,第十三章上線中會學習開發自定義中介軟體。

驗證模組還包括如下資料模型:

  • User:一個使用者數資料表,包含如下主要欄位:usernamepasswordemailfirst_namelast_nameis_active
  • Group:一個使用者組表格
  • Permission:存放使用者和組的許可權清單

驗證框架還包括預設的驗證檢視以及對應表單,稍後會使用到。

2.1建立登入檢視

從這節開始使用Django的驗證模組,一個登入檢視需要如下功能:

  • 通過使用者提交的表單獲取使用者名稱和密碼
  • 將使用者名稱和密碼與資料庫中的資料進行匹配
  • 檢查使用者是否處於活動狀態
  • 通過在HTTP請求上附加session,讓使用者進入登入狀態

首先需要建立一個登入表單,在account應用內建立forms.py檔案,新增以下內容:

from django import forms

class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)

這是使用者輸入使用者名稱和密碼的表單。由於一般密碼框不會明文顯示,這裡採用了widget=forms.PasswordInput,令其在頁面上顯示為一個type="password"INPUT元素。

然後編輯account應用的views.py檔案,新增如下程式碼:

from django.shortcuts import render, 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(request, 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})

這是我們的登入檢視,其基本邏輯是:當檢視接受一個GET請求,通過form = LoginForm()例項化一個空白表單;如果接收到POST請求,則進行如下工作:

  1. 通過form = LoginForm(request.POST),使用提交的資料例項化一個表單物件。
  2. 通過呼叫form.is_valid()驗證表單資料。如果未通過,則將當前表單物件展示在頁面中。
  3. 如果表單資料通過驗證,則呼叫內建authenticate()方法。該方法接受request物件,usernamepassword三個引數,之後到資料庫中進行匹配,如果匹配成功,會返回一個User資料物件;如果未找到匹配資料,返回None。在匹配失敗的情況下,檢視返回一個登陸無效資訊。
  4. 如果使用者資料成功通過匹配,則根據is_active屬性檢查使用者是否為活動使用者,這個屬性是Django內建User模型的一個欄位。如果使用者不是活動使用者,則返回一個訊息顯示不活動使用者。
  5. 如果使用者是活動使用者,則呼叫login()方法,在會話中設定使用者資訊,並且返回登入成功的訊息。

注意區分內建的authenticate()login()方法。authenticate()僅到資料庫中進行匹配並且返回User資料物件,其工作類似於進行資料庫查詢。而login()用於在當前會話中設定登入狀態。二者必須搭配使用才能完成使用者名稱和密碼的資料驗證和使用者登入的功能。

現在需要為檢視設定路由,在account應用下建立urls.py,新增如下程式碼:

from django.urls import path
from . import views

urlpatterns = [
    path('login/', views.user_login, name='login'),
]

然後編輯專案的根ulrs.py檔案,匯入include並且增加一行轉發到account應用的二級路由配置:

from django.conf.urls import path, include
from django.contrib import admin

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
]

之後需要配置模板。由於專案還沒有任何模板,可以先建立一個母版,在account應用下建立如下目錄和檔案結構:

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>

這是這個專案使用的母版。和上一個專案一樣使用了CSS檔案,你需要把static資料夾從原始碼複製到account應用目錄下。這個母版有一個title塊和一個content塊用於繼承。

譯者注:原書第一章使用了{% load static %},這裡的模板使用了{% load staticfiles %},作者並沒有對這兩者的差異進行說明,讀者可以參考What is the difference between {% load staticfiles %} and {% load static %}

之後編寫account/login.html

{% extends 'base.html' %}

{% block title %}Log-in{% endblock %}

{% block content %}
<h1>Log-in</h1>
<p>Please, use 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 %}

我們的站點還沒有任何使用者,建立一個超級使用者,然後使用超級使用者到http://127.0.0.1:8000/admin/登入,會看到預設的管理後臺:

使用管理後臺新增一個使用者,然後開啟http://127.0.0.1:8000/account/login/,可以看到如下登入介面:

填寫剛建立的使用者資訊並故意留空表單然後提交,可以看到錯誤資訊如下:

注意和第一章一樣,很可能一些現代瀏覽器會阻止表單提交,修改模板關閉表單的瀏覽器驗證即可。

再進行一些實驗,如果輸入不存在的使用者名稱或密碼,會得到無效登入的提示,如果輸入了正確的資訊,就會看到如下的登入成功資訊:

2.2使用內建驗證檢視

Django內建很多檢視和表單可供直接使用,上一節的登入檢視就是一個很好的例子。在大多數情況下都可以使用Django內建的驗證模組而無需自行編寫。

Django在django.contrib.auth.views中提供瞭如下基於類的檢視供使用:

  • LoginView:處理登入表單填寫和登入功能(和我們寫的功能類似)
  • LogoutView:退出登入
  • PaswordChangeView:處理一個修改密碼的表單,然後修改密碼
  • PasswordChangeDoneView:成功修改密碼後執行的檢視
  • PasswordResetView:使用者選擇重置密碼功能執行的檢視,生成一個一次性重置密碼連結和對應的驗證token,然後傳送郵件給使用者
  • PasswordResetDoneView:通知使用者已經發送給了他們一封郵件重置密碼
  • PasswordResetConfirmView:使用者設定新密碼的頁面和功能控制
  • PasswordResetCompleteView:成功重置密碼後執行的檢視

上邊的檢視列表按照一般處理使用者相關功能的順序列出相關檢視,在編寫帶有使用者功能的站點時可以參考使用。這些內建檢視的預設值可以被修改,比如渲染的模板位置和使用的表單等。

可以通過官方文件https://docs.djangoproject.com/en/2.0/topics/auth/default/#all-authentication-views瞭解更多內建驗證檢視的資訊。

2.3登入與登出檢視

由於直接使用內建檢視和內建資料模型,所以不需要編寫模型與檢視,來為內建登入和登出檢視配置URL,編輯account應用的urls.py檔案,註釋掉之前的登入方法,改成內建方法:

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # path('login/', views.user_login, name='login'),
    path('login/',auth_views.LoginView.as_view(),name='login'),
    path('logout/',auth_views.LogoutView.as_view(),name='logout'),
]

現在我們把登入和登出的URL導向了內建檢視,然後需要為內建檢視建立模板

templates目錄下新建registration目錄,這個目錄是內建檢視預設到當前應用的模板目錄裡尋找具體模板的位置。

django.contrib.admin模組中自帶一些驗證模板,用於管理後臺使用。我們在INSTALLED_APPS中將account應用放到admin應用的上邊,令django預設使用我們編寫的模板。

templates/registration目錄下建立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, use 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 %}

這個模板和剛才自行編寫登入模板很類似。內建登入檢視預設使用django.contrib.auth.forms裡的AuthenticationForm表單,通過檢查{% if form.errors %}可以判斷驗證資訊是否錯誤。注意我們添加了一個name屬性為next的隱藏<input>元素,這是內建檢視通過Get請求獲得並記錄next引數的位置,用於返回登入前的頁面,例如http://127.0.0.1:8000/account/login/?next=/account/

next引數必須是一個URL地址,如果具有這個引數,登入檢視會在登入成功後將使用者重定向到這個引數的URL。

registration目錄下建立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 %}

這是使用者登出之後顯示的提示頁面。

現在我們的站點已經可以使用使用者登入和登出的功能了。現在還需要為使用者製作一個登入成功後自己的首頁,開啟account應用的views.py檔案,新增如下程式碼:

from django.contrib.auth.decorators import login_required
@login_required
def dashboard(request):
    return render(request, 'account/dashboard.html', {'section': 'dashboard'})

使用@login_required裝飾器,表示被裝飾的檢視只有在使用者登入的情況下才會被執行,如果使用者未登入,則會將使用者重定向至Get請求附加的next引數指定的URL。這樣設定之後,如果使用者在未登入的情況下,無法看到首頁。

還定義了一個引數section,可以用來追蹤使用者當前所在的功能板塊。

現在可以建立首頁對應的模板,在templates/account/目錄下建立dashboard.html

{% extends 'base.html' %}

{% block title %}
Dashboard
{% endblock %}

{% block content %}
    <h1>Dashboard</h1>
    <p>Welcome to your dashboard.</p>
{% endblock %}

然後在account應用的urls.py裡增加新檢視對應的URL:

urlpatterns = [
    # ...
    path('', views.dashboard, name='dashboard'),
]

還需要在settings.py裡增加如下設定:

LOGIN_REDIRECT_URL = 'dashboard'
LOGIN_URL = 'login'
LOGOUT_URL = 'logout'

這三個設定分別表示:

  • 如果沒有指定next引數,登入成功後重定向的URL
  • 使用者需要登入的情況下被重定向到的URL地址(例如@login_required重定向到的地址)
  • 使用者需要登出的時候被重定向到的URL地址

這裡都使用了path()方法中的name屬性,以動態的返回連結。在這裡也可以硬編碼URL。

總結一下我們現在做過的工作:

  • 為專案新增內建登入和登出檢視
  • 為兩個檢視編寫模板並編寫了首頁檢視和對應模板
  • 為三個檢視配置了URL

最後需要在母版上新增登入和登出相關的展示。為了實現這個功能,必須根據當前使用者是否登入,決定模板需要展示的內容。在內建函式LoginView成功執行之後,驗證模組的中介軟體在HttpRequest物件上設定了使用者物件User,可以通過request.user訪問使用者資訊。在使用者未登入的情況下,request.user也存在,是一個AnonymousUser類的例項。判斷當前使用者是否登入最好的方式就是判斷User物件的is_authenticated只讀屬性。

編輯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 }},{{ request.user.username }},<a href="{% url 'logout' %}">Logout</a>
            {% else %}
            <a href="{% url 'login' %}">Log-in</a>
        {% endif %}
 </span>
</div>

上邊的檢視只顯示站點的選單給已登入使用者。還添加了了根據section的內容為<li>新增CSS類selected的功能,用於顯示高亮當前的板塊。最後對登入使用者顯示名稱和登出連結,對未登入使用者則顯示登入連結。

現在啟動專案,到http://127.0.0.1:8000/account/login/,會看到登入頁面,輸入有效的使用者名稱和密碼並點選登入按鈕,之後會看到如下頁面:

可以看到當前的 My dashboard 應用了selected類的CSS樣式。當前使用者的資訊顯示在頂部的右側,點選登出連結,會看到如下頁面:

可以看到使用者已經登出,頂部的選單欄已經不再顯示,右側的連結變為登入連結。

如果這裡看到Django內建的管理站點樣式的頁面,檢查settings.py檔案中的INSTALLED_APPS設定,確保account應用在django.contrib.admin應用的上方。由於內建的檢視和我們自定義的檢視使用了相同的相對路徑,Django的模板載入器會使用先找到的模板。

2.4修改密碼檢視

在使用者登入之後需要允許使用者修改密碼,我們在專案中整合Django的內建修改密碼相關的檢視。編輯account應用的urls.py檔案,新增如下兩行URL:

path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),

asswordChangeView檢視會控制渲染修改密碼的頁面和表單,PasswordChangeDoneView檢視在成功修改密碼之後顯示成功訊息。

之後要為兩個檢視建立模板,在templates/registration/目錄下建立password_change_form.html,新增如下程式碼:

{% extends 'base.html' %}

{% block title %}
Change your password
{% endblock %}

{% block content %}
<h1>Change your password</h1>
    <p>Use the form below to change your password.</p>
    <form action="." method="post" novalidate>
    {{ form.as_p }}
    <p><input type="submit" value="Change"></p>
    {% csrf_token %}
    </form>
{% endblock %}

password_change_form.html模板包含修改密碼的表單,再在同一目錄下建立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 %}

password_change_done.html模板包含成功建立密碼後的提示訊息。

啟動服務,到http://127.0.0.1:8000/account/password_change/,成功登入之後可看到如下頁面:

填寫表單並修改密碼,之後可以看到成功訊息:

之後登出再登入,驗證是否確實成功修改密碼。

2.5重置密碼檢視

編輯account應用的urls.py檔案,新增如下對應到內建檢視的URL:

path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

然後在account應用的templates/registration/目錄下建立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" novalidate>
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Send e-mail"></p>
    </form>
{% endblock %}

在同一目錄下建立傳送郵件的頁面password_reset_email.html,新增如下程式碼:

Someone asked for password reset for email {{ email }}. Follow the link
below:
{{ protocol }}://{{ domain }}{% url "password_reset_confirm" uidb64=uid token=token %}
Your username, in case you've forgotten: {{ user.get_username }}

這個模板用來渲染向用戶傳送的郵件內容。

之後在同一目錄再建立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 %}

然後建立重置密碼的頁面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">
            {{ form.as_p }}
            {% csrf_token %}
            <p><input type="submit" value="Change my password"/></p>
        </form>
    {% else %}
        <p>The password reset link was invalid, possibly because it has
            already been used. Please request a new password reset.</p>
    {% endif %}
{% endblock %}

這個頁面裡有一個變數validlink,表示使用者點選的連結是否有效,由PasswordResetConfirmView檢視傳入模板。如果有效就顯示重置密碼的表單,如果無效就顯示一段文字說明連結無效。

在同一目錄內建立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 %}

最後編輯registration/login.html,在<form>元素之後加上如下程式碼,為頁面增加重置密碼的連結:

<p><a href="{% url 'password_reset' %}">Forgotten your password?</a></p>

之後在瀏覽器中開啟http://127.0.0.1:8000/account/login/,點選Forgotten your password?連結,會看到如下頁面:

這裡必須在settings.py中配置SMTP伺服器,在第二章中已經學習過配置STMP伺服器的設定。如果確實沒有SMTP伺服器,可以增加一行:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

以讓Django將郵件內容輸出到命令列視窗中。

返回瀏覽器,填入一個已經存在的使用者的電子郵件地址,之後點SEND E-MAIL按鈕,會看到如下頁面:

此時看一下啟動Django站點的命令列視窗,會列印如下郵件內容(或者到信箱中檢視實際收到的電子郵件):

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8000
From: webmaster@localhost
To: [email protected]
Date: Fri, 15 Dec 2017 14:35:08 -0000
Message-ID: <[email protected]>
Someone asked for password reset for email [email protected]. Follow the link
below:
http://127.0.0.1:8000/account/reset/MQ/45f-9c3f30caafd523055fcc/
Your username, in case you've forgotten: zenx

這個郵件的內容就是password_reset_email.html經過渲染之後的實際內容。其中的URL指向檢視動態生成的連結,將這個URL複製到瀏覽器中開啟,會看到如下頁面:

這個頁面使用password_reset_confirm.html模板生成,填入一個新密碼然後點選CHANGE MY PASSWORD按鈕,Django會用你輸入的內容生成加密後的密碼儲存在資料庫中,然後會看到如下頁面:

現在就可以使用新密碼登入了。這裡生成的連結只能使用一次,如果反覆開啟該連結,會收到無效連結的錯誤。

我們現在已經集成了Django內建驗證模組的主要功能,在大部分情況下,可以直接使用內建驗證模組。也可以自行編寫所有的驗證程式。

在第一個專案中,我們提到為應用配置單獨的二級路由,有助於應用的複用。現在的account應用的urls.py檔案中所有配置到內建檢視的URL,可以用如下一行來代替:

urlpatterns = [
    # ...
    path('', include('django.contrib.auth.urls')),
]

可以在github上看到django.contrib.auth.urls的原始碼:https://github.com/django/django/blob/stable/2.0.x/django/contrib/auth/urls.py

3使用者註冊與使用者資訊

已經存在的使用者現在可以登入、登出、修改和重置密碼了。現在需要建立一個功能讓使用者註冊。

3.1使用者註冊

為使用者註冊功能建立一個簡單的檢視:先建立一個供使用者輸入使用者名稱、姓名和密碼的表單。編輯account應用的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(r"Password don't match.")
        return cd['password2']

這裡通過使用者模型建立了一個模型表單,只包含usernamefirst_nameemail欄位。這些欄位會根據User模型中的設定進行驗證,比如如果輸入了一個已經存在的使用者名稱,則驗證不會通過,因為username欄位被設定了unique=True。添加了兩個新的欄位passwordpassword2,用於使用者輸入並且確認密碼。定義了一個clean_password2()方法用於檢查兩個密碼是否一致,這個方法是一個驗證器方法,會在呼叫is_valid()方法的時候執行。可以對任意的欄位採用clean_<fieldname>()方法名建立一個驗證器。Forms類還擁有一個clean()方法用於驗證整個表單,可以方便的驗證彼此相關的欄位。

譯者注:這裡必須瞭解表單的驗證順序。clean_password2()方法中使用了cd['password2'];為什麼驗證器還沒有執行完畢的時候,cleaned_data中已經存在password2資料了呢?這裡有一篇介紹django驗證表單順序的文章,可以看到,在執行自定義驗證器之前,已經執行了每個欄位的clean()方法,這個方法僅針對欄位本身的屬性進行驗證,只要這個通過了,cleaned_data中就有了資料,之後才執行自定義驗證器,最後執行form.clean()完成驗證。如果過程中任意時候丟擲ValidationErrorcleaned_data裡就會只剩有效的值,errors屬性內就有了錯誤資訊。

關於使用者註冊,Django提供了一個位於django.contrib.auth.formsUserCreationForm表單供使用,和我們自行編寫的表單非常類似。

編輯account應用的views.py檔案,新增如下程式碼:

from .forms import LoginForm, UserRegistrationForm

def register(request):
    if request.method == "POST":
        user_form = UserRegistrationForm(request.POST)
        if user_form.is_valid():
            # 建立新資料物件但是不寫入資料庫
            new_user = user_form.save(commit=False)
            # 設定密碼
            new_user.set_password(user_form.cleaned_data['password'])
            # 儲存User物件
            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})

這個檢視邏輯很簡單,我們使用了set_password()方法設定加密後的密碼。

再配置account應用的urls.py檔案,新增如下的URL匹配:

path('register/', views.register, name='register'),

templates/account/目錄下建立模板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" novalidate>
    {{ user_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Register"></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 %}

現在可以開啟http://127.0.0.1:8000/account/register/,看到註冊介面如下:

填寫表單並點選CREATE MY ACCOUNT按鈕,如果表單正確提交,會看如下成功頁面:

3.2擴充套件使用者模型

Django內建驗證模組的User模型只有非常基礎的欄位資訊,可能需要額外的使用者資訊。最好的方式是建立一個使用者資訊模型,然後通過一對一關聯欄位,將使用者資訊模型和使用者模型聯絡起來。

編輯account應用的models.py檔案,新增以下程式碼:

from django.db import models
from django.conf import settings

class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    date_of_birth = models.DateField(blank=True, null=True)
    photo = models.ImageField(upload_to='user/%Y/%m/%d/', blank=True)

    def __str__(self):
        return "Profile for user {}".format(self.user.username)

為了保持程式碼通用性,使用get_user_model()方法來獲取使用者模型;當定義其他表與內建User模型的關係時使用settings.AUTH_USER_MODEL指代User模型。

這個Profile模型的user欄位是一個一對一關聯到使用者模型的關係欄位。將on_delete設定為CASCADE,當用戶被刪除時,其對應的資訊也被刪除。這裡還有一個圖片檔案欄位,必須安裝Python的Pillow庫才能使用圖片檔案欄位,在系統命令列中輸入:

pip install Pillow==5.1.0

由於我們要允許使用者上傳圖片,必須配置Django讓其提供媒體檔案服務,在settings.py中加入下列內容:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL表示存放和提供使用者上傳檔案的URL路徑,MEDIA_ROOT表示實際媒體檔案的存放目錄。這裡都採用相對地址動態生成URL。

來編輯一下bookmarks專案的根urls.py,修改其中的程式碼如下:

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

這樣設定後,Django開發伺服器在DEBUG=True的情況下會提供媒體檔案服務。

static()方法僅用於開發環境,在生產環境中,不要用Django提供靜態檔案服務(而是用Web服務程式比如NGINX等提供靜態檔案服務)。

建立了新的模型之後需要執行資料遷移過程。之後將新的模型加入到管理後臺,編輯account應用的admin.py檔案,將Profile模型註冊到管理後臺中:

from django.contrib import admin
from .models import Profile

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'date_of_birth', 'photo']

啟動站點,開啟http://127.0.0.1:8000/admin/,可以在管理後臺中看到新增的模型:

現在需要讓使用者填寫額外的使用者資訊,為此需要建立表單,編輯account應用的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:這個表單依據User類生成,讓使用者輸入姓,名和電子郵件。
  • ProfileEditForm:這個表單依據Profile類生成,可以讓使用者輸入生日和上傳一個頭像。

之後建立檢視,編輯account應用的views.py檔案,匯入Profile模型:

from .models import Profile

然後在register檢視的new_user.save()下增加一行:

Profile.objects.create(user=new_user)

當用戶註冊的時候,會自動建立一個空白的使用者資訊關聯到使用者。在之前建立的使用者,則必須在管理後臺中手工為其新增對應的Profile物件

還必須讓使用者可以編輯他們的資訊,在同一個檔案內新增下列程式碼:

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裝飾器,因為使用者必須登入才能編輯自己的資訊。我們使用UserEditForm表單儲存內建的User類的資料,用ProfileEditForm存放Profile類的資料。然後呼叫is_valid()驗證兩個表單資料,如果全部都通過,將使用save()方法寫入資料庫。

譯者注:原書沒有解釋instance引數。instance用於指定表單類例項化為某個具體的資料物件。在這個例子裡,將UserEditForm``instance指定為request.user表示該物件是資料庫中當前登入使用者那一行的資料物件,而不是一個空白的資料物件,ProfileEditForminstance屬性指定為當前使用者對應的Profile類中的那行資料。這裡如果不指定instance引數,則變成向資料庫中增加兩條新記錄,而不是修改原有記錄。

之後編輯account應用的urls.py檔案,為新檢視配置URL:

path('edit/', views.edit, name='edit'),

最後,在templates/account/目錄下建立edit.html,新增如下程式碼:

{#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" novalidate>
    {{ 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_formprofile_form表單。

啟動站點,註冊一個新使用者,然後開啟http://127.0.0.1:8000/account/edit/,可以看到頁面如下:

現在可以在使用者登入後的首頁加上修改使用者資訊的連結了,開啟account/dashboard.html,找到下邊這行:

<p>Welcome to your dashboard.</p>

將其替換為:

<p>Welcome to your dashboard. You can <a href="{% url 'edit' %}">edit your profile</a> or <a href="{% url "password_change" %}">change your password</a>.</p>

使用者現在可以通過登入後的首頁修改使用者資訊,開啟http://127.0.0.1:8000/account/然後可以看到新增了修改使用者資訊的連結,頁面如下:

3.2.1使用自定義的使用者模型

Django提供了使用自定義的模型替代內建User模型的方法,需要編寫自定義的類繼承AbstractUser類。這個AbstractUser類提供了預設的使用者模型的完整實現,作為一個抽象類供其他類繼承。關於模型的繼承將在本書最後一個專案中學習。可以在https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#substituting-a-custom-user-model找到關於自定義使用者模型的詳細資訊。

使用自定義使用者模型比起預設內建使用者模型可以更好的滿足開發需求,但需要注意的是會影響一些使用Django內建使用者模型的第三方應用。

3.3使用訊息框架

當用戶在我們的站點執行各種操作時,在一些關鍵操作可能需要通知使用者其操作是否成功。Django有一個內建訊息框架可以給使用者傳送一次性的通知。

訊息模組位於django.contrib.messages,並且已經被包含在初始化的INSTALLED_APPS設定中,還有一個預設啟用的中介軟體叫做django.contrib.messages.middleware.MessageMiddleware,共同構成了訊息系統。

訊息框架提供了非常簡單的方法向用戶傳送通知:預設在cookie中儲存訊息內容(根據session的儲存設定),然後會在下一次HTTP請求的時候在對應的響應上附加該資訊。匯入訊息模組並且在檢視中使用很簡單的語句就可以傳送訊息,例如:

from django.contrib import messages
messages.error(request, 'Something went wrong')

這樣就在請求上附加了一個錯誤資訊。可以使用add_message()或如下的方法建立訊息:

  • success():一個動作成功之後傳送的訊息
  • info():通知性質的訊息
  • warning():警告性質的內容,所謂警告就是還沒有失敗但很可能失敗的情況
  • error():錯誤資訊,通知操作失敗
  • debug():除錯資訊,給開發者展示,在生產環境中需要被移除

在我們的站點中增加訊息內容。由於訊息是貫穿整個網站的,所以打算將訊息顯示的部分設定在母版中,編輯base.html,在ID為header<div>標籤和ID為content<div>標籤之間增加下列程式碼:

{% if messages %}
    <ul class="messages">
        {% for message in messages %}
            <li class="{{ message.tags }}">{{ message|safe }}<a href="#" class="close">X</a></li>
        {% endfor %}
    </ul>
{% endif %}

在模板中使用了messages變數,在後文可以看到檢視並未向模板傳入該變數。這是因為在settings.py中的TEMPLATES設定中,context_processors的設定中包含django.contrib.messages.context_processors.messages這個上下文管理器,從而為模板傳入了messages變數,而無需經過檢視。預設情況下可以看到還有debugrequestauth三個上下文處理器。其中後兩個就是我們在模板中可以直接使用request.user而無需傳入該變數,也無需為request物件新增user屬性的原因。

之後來修改account應用的views.py檔案,匯入messages,然後編輯edit檢視:

from django.contrib import messages

@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()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    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})

為檢視增加了兩條語句,分別在成功登入之後顯示成功資訊,在表單驗證失敗的時候顯示錯誤資訊。

瀏覽器中開啟http://127.0.0.1:8000/account/edit/,編輯使用者資訊,之後可以看到成功資訊如下:

故意填寫通不過驗證的資料,則可以看到錯誤資訊如下:

關於訊息框架的更多資訊,可以檢視官方文件:https://docs.djangoproject.com/en/2.0/ref/contrib/messages/

4建立自定義驗證後端

Django允許對不同的資料來源採用不同的驗證方式。在settings.py裡有一個AUTHENTICATION_BACKENDS設定列出了專案中可使用的驗證後端。其預設是:

['django.contrib.auth.backends.ModelBackend']

預設的ModelBackend通過django.contrib.auth後端進行驗證,這對於大部分專案已經足夠。然而我們也可以建立自定義的驗證後端,用於滿足個性化需求,比如LDAP目錄或者來自於其他系統的驗證。

關於自定義驗證後端可以參考官方文件:https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#other-authentication-sources

每次使用內建的authenticate()函式時,Django會按照AUTHENTICATION_BACKENDS設定中列出的順序,依次執行其中的驗證後端進行驗證工作,直到有一個驗證後端返回成功為止。如果列表中的後端全部返回失敗,則這個使用者就不會被認證通過。

Django提供了一個簡單的規則用於編寫自定義驗證後端:一個驗證後端必須是一個類,至少提供如下兩個方法:

  • authenticate():引數為request和使用者驗證資訊,如果使用者驗證資訊有效,必須返回一個user物件,否則返回Nonerequest引數必須是一個HttpRequest物件或者是None
  • get_user():引數為使用者的ID,返回一個user物件

我們來編寫一個採用電子郵件(而不是username欄位)和密碼登入的驗證後端,編寫驗證後端就和編寫一個Python的類沒有什麼區別:

from django.contrib.auth.models import User

class EmailAuthBakcend:
    """
    Authenticate using an e-mail address.
    """

    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            return None

以上程式碼是一個簡單的驗證後端。authenticate()方法接受request物件和usernamepassword作為可選引數,這裡可以用任何自定義的引數名稱,我們使用usernamepassword是為了可以與內建驗證框架配合工作。兩個方法工作流程如下:

  • authenticate():嘗試使用電子郵件和密碼獲取使用者物件,採用check_password()方法驗證加密後的密碼。
  • get_user():通過user_id引數獲取使用者ID,在會話存續Django會使用內建的驗證後端去驗證並取得User物件。

編輯settings.py檔案增加:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
]

在上邊的設定裡,我們將自定義驗證後端加到了預設驗證的後邊。開啟http://127.0.0.1:8000/account/login/,注意Django嘗試使用所有的驗證後端,所以我們現在可以使用使用者名稱或者電子郵件來登入,填寫的資訊會先交給ModelBackend進行驗證,如果沒有得到使用者物件,就會使用我們的EmailAuthBackend進行驗證。

AUTHENTICATION_BACKENDS中的順序很重要,如果一個使用者資訊對於多個驗證後端都有效,Django會停止在第一個成功驗證的後端處。

5第三方認證登入

很多社交網站除了註冊使用者之外,提供了連結可以快速的通過第三方平臺的使用者資訊進行登入,我們也可以為自己的站點新增例如Facebook,Twitter或Google的第三方認證登入功能。Python Social Auth是一個提供第三方認證登入的模組。使用這個模組可以讓使用者以第三方網站的資訊進行登入,而無需先註冊本網站的使用者。這個模組的原始碼在https://github.com/python-social-auth

這個模組支援很多不同的Python Web框架,其中也包括Django,通過以下命令安裝:

pip install social-auth-app-django==2.1.0

然後將應用名social_django新增到settings.py檔案的INSTALLED_APPS設定中:

INSTALLED_APPS = [
    #...
    'social_django',
]

該應用自帶了資料模型,所以需要執行資料遷移過程。執行之後可以在資料庫中看到新增social_auth開頭的一系列資料表。Python 的social auth模組具體支援的第三方驗證服務,可以檢視官方文件:https://python-social-auth.readthedocs.io/en/latest/backends/index.html#supported-backends

譯者注:Facebook,Twitter和Google的第三方驗證均通過OAuth2認證,而且操作方式基本相同。以下僅以Google為例子進行翻譯:

需要先把第三方認證的URL新增到專案中,編輯bookmarks專案的根urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
]

一些網站的第三方驗證介面不允許將驗證後的地址重定向到類似127.0.0.1或者localhost這種本地地址,為了正常使用第三方驗證服務,需要一個正式域名,可以通過修改Hosts檔案。如果是Linux或macOS X下,可以編輯/etc/hosts加入一行:

127.0.0.1 mysite.com

這樣會將mysite.com域名對應到本機地址。如果是Windows環境,可以在C:\Windows\System32\Drivers\etc\hosts找到hosts檔案。

為了測試該設定是否生效,啟動站點然後在瀏覽器中開啟http://mysite.com:8000/account/login/,會得到如下錯誤資訊:

這是因為Djanog在settings.py中的ALLOWED_HOSTS設定中,僅允許對此處列出的域名提供服務,這是為了防止HTTP請求頭攻擊。關於該設定可以參考官方文件:https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts

編輯settings.py檔案然後修改ALLOWED_HOSTS為如下:

ALLOWED_HOSTS = ['mysite.com', 'localhost', '127.0.0.1']

mysite.com之外,我們增加了localhost127.0.0.1,其中localhost是在DEBUG=TrueALLOWED_HOSTS留空情況下的預設值,現在就可以通過http://mysite.com:8000/account/login/正常訪問開發網站了。

5.1使用Google第三方認證

Google提供OAuth2認證,詳細文件可以參考:https://developers.google.com/identity/protocols/OAuth2

為使用Google的第三方認證服務,將以下驗證後端新增到settings.pyAUTHENTICATION_BACKENDS中:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'account.authentication.EmailAuthBackend',
    'social_core.backends.google.GoogleOAuth2',
]

譯者注:由於Google API的介面在原書成書後已經改變,以下在Google網站的操作步驟和截圖來自於譯者實際操作過程。

需要到Google開發者網站建立一個API key,按照以下步驟操作:

  1. 開啟https://console.developers.google.com/apis/credentials,點選螢幕左上方Google APIs字樣右邊的選擇專案,會彈出專案對話方塊,點選右上方的新建專案,如圖所示:
  1. 填寫新建專案的資訊,專案名稱為Bookmarks,位置可以不選,之後點選建立按鈕,如下圖所示:
  1. 之後與步驟1中的步驟類似,點開選擇專案,選中剛建立的Bookmarks專案,然後點選右下方的開啟
  2. 會自動跳轉到一個頁面提示尚未建立API憑據,點選頁面中的建立憑據按鈕,並選擇第二項OAuth客戶端ID,如下圖所示:
  1. 之後會進入一個介面,要求必須配置OAuth同意螢幕,如下圖所示:

點選右側的配置同意螢幕按鈕。

  1. 之後進入到OAuth同意螢幕,裡邊有一系列設定。在應用名稱中填入Bookmarks,預設支援電子郵件為你自己的電子郵件地址,可以修改為其他地址,在已獲授權的網域中填入mysite.com,之後點選儲存,如圖所示:
  1. 此時會跳轉到步驟5的問題頁面,選擇網頁應用,之後會被要求填寫輔助資訊,在名稱中填寫Bookmarks已獲授權的重定向 URI中填寫http://mysite.com:8000/social-auth/complete/google-oauth2/,如下圖所示:
  1. 點選建立按鈕,即可在頁面中看到當前API的ID和金鑰,如圖所示:
  1. 將API ID 和金鑰填寫到settings.py檔案中,增加如下兩行:
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'XXX' # API ID
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'XXX' # 金鑰
  1. 點選確認關閉對話方塊,之後在左側選單的憑據選單內可以回到此處檢視ID和金鑰。現在點選左側選單的,會跳轉到歡迎使用新版API庫的介面,在其中找到Google+ API,如圖所示:
  1. 點選Google+ API,在彈出的頁面中選擇啟用,如圖所示:

在Google中的配置就全部結束了,生成了一個OAuth2認證的ID和金鑰,之後我們就將採用這些資訊與Google進行通訊。

然後編輯account應用的registration/login.html模板,在content塊的內部最下方增加用於進行Google第三方認證登入的連結:

<div class="social">
    <ul>
        <li class="google"><a href="{% url 'social:begin' 'google-oauth2' %}">Log in with Google</a></li>
    </ul>
</div>

開啟http://mysite.com:8000/account/login/,可以看到如下頁面:

點選Login with Google按鈕,使用Google賬戶登入後,就會被重定向到我們網站的登入首頁。

我們現在就為專案增加了第三方認證登入功能,即使是沒有在本站註冊的使用者,也可以快捷的進行登入了。

譯者注:這裡有一個小問題,就是通過第三方登入進來的使用者,檢查auth_user表會發現其實使用者資訊已經被寫入到了該表裡,但是Profile表沒有寫入對應的外來鍵欄位,導致第三方認證使用者在修改使用者資訊時會報錯。很多網站的做法是:通過第三方驗證進來的使用者,必須捆綁到本站已經存在的賬號中。這裡我們簡化一下處理,當用戶修改欄位的Get請求進來時,檢測Profile表中該使用者的外來鍵是不是存在,如果不存在,就新建對應該使用者的Profile物件,然後再用這個資料物件返回表單例項供填寫。修改後的edit檢視如下:

@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()
            messages.success(request, 'Profile updated successfully')
        else:
            messages.error(request, "Error updating your profile")
    else:
        try:
            Profile.objects.get(user=request.user)
        except Profile.DoesNotExist:
            Profile.objects.create(user=request.user)
        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})

總結

這一章學習了使用內建框架快捷的建立使用者驗證系統,以及建立自定義的使用者資訊,還學習了為網站新增第三方認證。

下一章中將學習建立一個圖片分享系統,生成圖片縮圖,以及在Djanog中使用AJAX技術。