幾乎所有的網站都提供了用戶注冊與管理功能,這一節,我們將講解如何利用Django自身提供的用戶認證系統實現用戶注冊與管理功能。
會話認證
在上一節中,我們學習了User數據模型,並用它來保存用戶信息,實際上用戶數據模型只是Django提供的認證管理系統的一小部分,Django認證系統位於django.contrib.auth包中,默認情況下是已經安裝了的。
可以通過檢查settings.py文件中的INSTALLED_APPS來判斷是否已經安裝了,它的配置信息應該如下:
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django_bookmarks.bookmarks', )
讓我們先了解下Django認證系統都提供了哪些功能:
- User:用戶數據模型
- Permissions:權限控制
- Groups:組管理
- Messages:用戶信息
創建登錄頁面
如果我們檢查上一節的User數據模型,會發現其實里面已經包含了一個用戶,以就是創建項目時創建的root用戶。
相比那些直接在底層操作session信息的的web開發語言(那樣更加麻煩,稍不留神就可能導致安全問題),如PHP,Django非常小心的為我們實現了session管理功能,激活它只需要將其暴露給一些執行的視圖函數,我們不需要擔心用戶會話信息的管理與口令認證的過程,Django已經為我們實現了一切。
為了創建登錄頁面,首先需要編輯urls.py,添加下面這條URL規則:
urlpatterns = patterns('', (r'^$', main_page), (r'^user/(\w+)/$', user_page), (r'^login/$', 'django.contrib.auth.views.login'), )
這個URL跟之前不太一樣,之前的都是使用視圖函數,這里我們直接使用模塊路徑作為參數,這是django提供的一個快捷方式,通常用於從當前項目之外模塊中導入視圖,Django會自動導入相應視圖。
django.contrib.auth.views提供了一些與會話管理相關的視圖函數,這里我們只使用login視圖,這個視圖處理用戶的登錄,但是在使用它之前,我們還需要編寫一個模板。
使用login視圖需要在提供一個模板,這個模板的路徑為registration/login.html,login視圖會加載這個模板然后傳遞一個login表單對象給它,在后面我們將詳細學習表單對象,但是現在我們只需要知道這個表單對象form有如下屬性form.username, form.password以及form.has_errors。
接下來創建模板,在templates目錄下新建一個registration文件夾,然后創建一個文件login.html:
<html> <head> <title>Django Bookmarks - User Login</title> </head> <body> <h1>User Login</h1> {% if form.has_errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} <form method="post" action="."> <p><label for="id_username">Username:</label> {{ form.username }}</p> <p><label for="id_password">Password:</label> {{ form.password }}</p> <input type="hidden" name="next" value="/" /> {% csrf_token %} <input type="submit" value="login" /> </form> </body> </html>
這段代碼中首先檢查是否有登陸錯誤,然后創建了一個包含用戶與口令以及提交按鈕的表單,表單中還有一個隱藏按鈕,name為next,值為成功登錄之后跳轉的URL。在表單中還有一個{% csrf_tag %}標簽,這個是用來防止跨域非法訪問的,在使用post方法提交這個表單的時候必須使用這個標簽。
接下來就可以進行登錄了,輸入http://127.0.0.1:8000/login/即可。可以使用剛開始創建項目時創建的用戶進行登錄。成功登錄之后就會重定向至主頁。既然我們可以登錄了,那么在主頁中就應該顯示我們是否處於已登錄狀態。對templates/main_page.html進行修改:
<html> <head> <title>Django Bookmarks</title> </head> <body> <h1>Welcome to Django Bookmarks</h1> {% if user.username %} <p>Welcome {{ user.username }}! Here you can store and share bookmarks!</p> {% else %} <p>Welcome anonymous user! You need to <a href="/login/">login</a> before you can store and share bookmarks.</p> {% endif %} </body> </html>
現在這個模板可以通過檢查變量user.username是否已經賦值,如果有,則顯示歡迎信息,否則,顯示登錄的鏈接。這個顯示渲染成功的前提是user對象傳遞給了它,所以需要修改bookmarks/views.py:
def main_page(request): template = get_template('main_page.html') variables = Context({ 'user': request.user }) output = template.render(variables) return HttpResponse(output)
user對象從request中獲取,然后傳遞給模板進行渲染。刷新主頁,,就可以顯示友好的歡迎信息了。
你可能發現,加載模板,傳遞變量,渲染頁面是個經常性的工作,那有沒有簡便的方法了,這些Django都想到了,所以Django提供了render_to_response,修改代碼/bookmarks/views.py:
from django.shortcuts import render_to_response def main_page(request): return render_to_response( 'main_page.html', { 'user': request.user } )
這樣一來方便多了,只需要引入一個模塊,使用一條語句就完成了上面的三個工作。
request中的user對象實際上就是我們之前介紹的User對象模型實例,我們已經熟悉了它的常用屬性,比如用戶名,密碼等,接下來再了解一下它的一些常用方法:
- is_authenticated() 返回一個布爾值,判斷用戶是否登錄
- get_full_name() 獲取用戶全名
- email_user(subject, message, from_email=None) 發送一封郵件給用戶
- set_password(raw_password) 設置用戶密碼
- check_password(raw_password) 檢查用戶密碼是否正確,並返回一個布爾值。
既然用戶的口令很容易獲取,為什么上面還有個set_password()方法呢?我們先看下面的例子:
>>> from django.contrib.auth.models import User >>> user = User.objects.get(id=1) >>> user.password 'sha1$e1f02$bc3c0ef7d3e5e405cbaac0a44cb007c3d34c372c'
從上面可以得知,返回的password是個很長的字符串,而不是原始的密碼,這其中發生了什么呢?這是處於安全考慮,Django不會直接保存原始密碼,而是保存密碼的hash值,這個hash值很難逆向獲取,但是依然可以用來驗證密碼。所以設置密碼必須調用set_password()。
實現注銷功能
實現登錄功能之后,下一步就是實現注銷功能了,當用戶單擊/logout時,就注銷用戶然后重定向至主頁。首先在bookmarks/views.py添加下面的視圖函數:
from django.http import HttpResponseRedirect from django.contrib.auth import logout def logout_page(request): logout(request) return HttpResponseRedirect('/')
我們使用django.contrib.auth中提供的logout函數,刪除相應用戶的會話信息,然后重定向至主頁,這里使用django.http提供的HttpRequestRedirect對象。
添加完視圖函數之后,就需要提供URL入口,所以編輯urls.py:
urlpatterns = patterns('', (r'^$', main_page), (r'^user/(\w+)/$', user_page), (r'^login/$', 'django.contrib.auth.views.login'), (r'^logout/$', logout_page), )
為了讓注銷的鏈接對用戶可見,我們需要對所有的模板進行編輯,這是很不現實的,因為改動的范圍比較大,為了克服這個缺點,我們接下來將學習模板繼承的使用。
改進模板結構
到現在我們已經創建了三個模板,它們都有着相似的結構,只是標題與內容不同而已,那么我們可不可以提取出這些模板的共性保存在一個文件中,然后再在這個基礎上進行繼承修改呢?答案是可以的。
Django提供的模板系統已經支持這樣的模板繼承,原理很簡單,首先創建一個基礎模板base.html,基礎模板中包含子模板中需要修改的代碼塊block,然后創建一個繼承於基礎模板的子模板,
在templates目錄下創建base.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Django Bookmarks | {% block title %}{% endblock %}</title> </head> <body> <h1>{% block head %}{% endblock %}</h1> {% block content %}{% endblock %} </body> </html>
這個模板中使用了一個新的標簽block,它用來定義子模板中可以修改的部分,base模板中包含了三個block,分別是title,head以及content。
接下來修改/templates/main_page.html,將它的內容替換成:
{% extends "base.html" %} {% block title %}Welcome to Django Bookmarks{% endblock %} {% block head %}Welcome to Django Bookmarks{% endblock %} {% block content %} {% if user.username %} <p>Welcome {{ user.username }}! Here you can store and share bookmarks!</p> {% else %} <p>Welcome anonymous user! You need to <a href="/login/">login</a> before you can store and share bookmarks.</p> {% endif %} {% endblock %}
新模板首先使用extend繼承base.html模板,也就是說main_page.html是base.html的子模板,main_page.html不再包含通用的HTML的結構,它只需要重新定義基礎模板中需要修改的部分。
接下里修改/templates/user_page.html:
{% extends "base.html" %} {% block title %}{{ username }}{% endblock %} {% block head %}Bookmarks for {{ username }}{% endblock %} {% block content %} {% if bookmarks %} <ul> {% for bookmark in bookmarks %} <li><a href="{{ bookmark.link.url }}"> {{ bookmark.title }}</a></li> {% endfor %} </ul> {% else %} <p>No bookmarks found.</p> {% endif %} {% endblock %}
最后修改/templates/registration/login.html:{% extends "base.html" %}:
{% extends "base.html" %} {% block title %}User Login{% endblock %} {% block head %}User Login{% endblock %} {% block content %} {% if form.has_errors %} <p>Your username and password didn't match. Please try again.</p> {% endif %} <form method="post" action="."> <p><label for="id_username">Username:</label> {{ form.username }}</p> <p><label for="id_password">Password:</label> {{ form.password }}</p> {% csrf_token %} <input type="submit" value="login" /> <input type="hidden" name="next" value="/" /> </form> {% endblock %}
現在我們的模板有了一個通用的基礎模板,接下來我們開始優化站點的可用性以及外觀。我們可以給項目增加CSS文件,CSS以及圖像都是靜態文件。在產品部署時通過web服務器提供支持,但是在Django開發環境中,則需要進行下面的操作才行。
打開urls.py,進行如下修改:
import os.path from django.conf.urls.defaults import * from bookmarks.views import * site_media = os.path.join( os.path.dirname(__file__), 'static' ) urlpatterns = patterns('', (r'^$', main_page), (r'^user/(\w+)/$', user_page), (r'^login/$', 'django.contrib.auth.views.login'), (r'^logout/$', logout_page), (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', { 'document_root': site_media }), )
在settings.py中的STATICFILES_DIRS添加os.path.join(os.path.dirname(__file__), 'static')。
在項目中新建一個文件夾site_media,在其中新建style.css,然后編輯templates/base.html,添加css的鏈接 並增加導航菜單:
<html> <head> <title>Django Bookmarks | {% block title %}{% endblock %}</title> <link rel="stylesheet" href="/site_media/style.css" type="text/css" /> </head> <body> <div id="nav"> <a href="/">home</a> | {% if user.is_authenticated %} welcome {{ user.username }} (<a href="/logout">logout</a>) {% else %} <a href="/login/">login</a> {% endif %} </div> <h1>{% block head %}{% endblock %}</h1> {% block content %}{% endblock %} </body> </html>
編輯style.css,使導航菜單靠近頁面右側:
#nav {
float: right;
}
現在主頁中就可以正常顯示導航菜單了,但是如果打開用戶頁面,卻發現不管用戶有沒有登錄,導航菜單中都會顯示登錄鏈接,這是因為導航菜單中的if標簽使用user對象來檢查用戶的狀態,但是這個對象並沒有通過Context變量傳遞給user_page.html模板,為了克服這個問題,可以使用下面兩種辦法:
- 修改user_page視圖函數,將user對象傳遞給模板。
- 使用RequestContext對象,這個對象與Context對象稍微不同,它會自動將user對象以及其他幾個變量傳遞給模板。為了實現這個目的,RequestContext使用request變量作為第一個參數,一個模板變量組成的字典作為第二個參數。
我們將使用第二個方法,因為它更加簡潔,因為我們需要將user對象傳遞給每個模板,而且,這樣可以提煉出通用代碼,減少代碼編寫量。
使用RequestContext修改main_page與user_page,編輯bookmarks/views.py,定位到main_page視圖,進行如下修改:
from django.template import RequestContext def main_page(request): return render_to_response( 'main_page.html', RequestContext(request) )
從上面可以看出,我們不需要再傳遞request.user變量給Context對象了,RequestContext自動進行處理。修改user_page:
def user_page(request, username): try: user = User.objects.get(username=username) except: raise Http404('Requested user not found.') bookmarks = user.bookmark_set.all() variables = RequestContext(request, { 'username': username, 'bookmarks': bookmarks }) return render_to_response('user_page.html', variables)
這樣代碼看起來更加緊湊了,盡管重構模板花費了不少時間,但是新的結構在以后會給我們節省下大量時間。
用戶注冊
第一個用戶是在創建Django項目時生成的,但是網站也需要為訪客提供注冊功能,在現在的社交網站中,用戶注冊是很基本的功能,這一節我們將學習如何實現用戶注冊功能。
Django表單
創建、驗證並處理表單數據是一個很普遍的操作,Web應用接收用戶輸入並通過表單的形式手機數據,所以很自然的Django提供了自帶的模塊forms實現這些通用功能,使用下面的命令導入模塊:
from django import forms
Django表單庫可以完成下面三個任務:
- 生成表單
- 服務器端表單驗證
- 表單錯誤顯示
表單form工作的模式與數據模型models類似,首先定義一個表單類,這個類必須繼承forms.Form基類,類屬性代表表單元素,form模塊提供了很多表單類型,跟models類似。除此之外,form還提供了很多方法,比如生成HTML代碼,獲取表單數據,驗證表單等等。
設計用戶注冊表單
讓我們來創建第一個Django表單,在bookmarks中創建forms.py,鍵入如下代碼:
from django import forms class RegistrationForm(forms.Form): username = forms.CharField(label='Username', max_length=30) email = forms.EmailField(label='Email') password1 = forms.CharField( label='Password', widget=forms.PasswordInput() ) password2 = forms.CharField( label='Password (Again)', widget=forms.PasswordInput() )
定義Registration,它繼承於forms.Form,所有表單類都繼承於這個基類,在forms模塊中有許多字段類型,還有其它參數,如下所示:
- label:當生成HTML代碼的時候表單元素的lable名
- required:表單元素是否必填,默認為True
- widget:控制字段的渲染結果,也就是表單元素的類型
- help_text:幫助文本
下面是常見的字段類型:
字段類型 描述
CharField 返回一個字符串
IntegerField 返回一個整數
DateField 返回一個Python datetime.date 對象
DateTimeField 返回一個Python datetime.datetime對象
EmailField 返回一個合法的email字符串
URLField 返回一個合法的URL字符串
下面是常用表單元素類型:
表單元素類型 描述
PasswordInput 密碼輸入框
HiddenInput 隱藏輸入框
Textarea 多行文本輸入框
FileInput 文件上傳框
我們可以通過交互命令窗口了解更多關於forms的API:
$ python manage.py shell >>> from bookmarks.forms import * >>> form = RegistrationForm()
接下來將表單渲染成HTML:
>>> print form.as_table(
結果將輸出表格形式的表單,除此之外,還可以通過form.as_ul()以及form.as_p()分別將表單渲染成列表形式以及段落形式。
此外,我們還可以直接渲染單個的表單元素:
>>> print form['username'] <input id="id_username" type="text" name="username" maxlength="30" />
現在我們已經知道如何渲染表單,接下來我們將學習如何驗證表單:
>>> form = RegistrationForm({ ... 'username': 'test', ... 'email': 'test@example.com', ... 'password1': 'test', ... 'password2': 'test' ... }) >>> form.is_valid() True
form.is_valid()返回True,因為所有的表單元素都通過了驗證,接下來使用下面的非法表單:
>>> form = RegistrationForm({ ... 'username': 'test', ... 'email': 'invalid email', ... 'password1': 'test', ... 'password2': 'test' ... }) >>> form.is_valid() False >>> form.errors {'email': [u'Enter a valid e-mail address.']}
Django為我們處理了表單驗證。我們可以使用form.is_bound來檢查表單是否有數據,如果你嘗試驗證一個沒有數據的表單,結果將拋出異常,用戶輸入可以通過form.data獲取,如果表單通過驗證,合法的輸入就可以通過form.cleaned_data獲取。
熟悉表單驗證之后,就可以改進之前的表單代碼,使其具有表單驗證功能:
- 防止用戶輸入不合法的用戶名或者已經存在的用戶名
- 確保兩次輸入的密碼一致。
首先修改密碼驗證,打開bookmarks/forms.py,給Registration類添加如下方法:
def clean_password2(self): if 'password1' in self.cleaned_data: password1 = self.cleaned_data['password1'] password2 = self.cleaned_data['password2'] if password1 == password2: return password2 raise forms.ValidationError('Passwords do not match.')
下面分析下這段代碼:
- 方法名為clean_password2,自定義的驗證方法都遵循這個格式clean_fieldname。
- 首先檢查password1是否通過驗證,如果通過驗證,那么通過self.cleaned_data字典就可以獲取到。
- 接下來檢查兩次輸入的密碼是否一致,如果一致則返回正確的password2。
- 如果驗證失敗,則拋出forms.ValidationError異常。
實現密碼驗證之后,接着實現用戶名驗證,在bookmarks/forms.py中添加如下語句:
import re from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist
re是正則表達式模塊,我們使用它來確保用戶名不包含非法字符,此外,我們還導入User數據模型,以檢查輸入的用戶名是否已經存在,最后,引入ObjectNotExist異常,以便在找不到相應對象時拋出:
對RegistrationForm進行修改,添加如下方法:
def clean_username(self): username = self.cleaned_data['username'] if not re.search(r'^\w+$', username): raise forms.ValidationError('Username can only contain alphanumeric characters and the underscore.') try: User.objects.get(username=username) except ObjectDoesNotExist: return username raise forms.ValidationError('Username is already taken.')
首先檢查用戶名是否包含非法字符串,然后減產這個用戶名是否已經被使用。我們可以通過交互命令行來檢查上面的驗證方法。如果你願意的話,也可以給email添加驗證方法。
現在注冊表單已經准備好了,但是還需要視圖函數與模板,我們先創建視圖函數,編輯bookmarks/views.py,鍵入如下代碼:
from bookmarks.forms import * def register_page(request): if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): user = User.objects.create_user( username=form.cleaned_data['username'], password=form.cleaned_data['password1'], email=form.cleaned_data['email'] ) return HttpResponseRedirect('/') else: form = RegistrationForm() variables = RequestContext(request, { 'form': form }) return render_to_response( 'registration/register.html', variables )
如果使用POST方法發送請求,用戶就提交注冊信息,視圖函數處理用戶輸入信息,然后重定向至主頁面,如果是其他請求,則生成注冊表單,然后渲染模板registration/register.html。
我們從request.POST中獲取用戶輸入,request.POST是一個字典,它包含了通過POST方法提交的數據,我們直接將這個字典傳遞給RegistrationForm構造函數,然后就可以進行表單驗證了。
你可能注意到這里是使用User.object.create_user()方法來創建用戶,而不是使用實例化User類的形式,我們之所以使用這個方法是因為create_user能幫我們處理密碼的哈希化,還有其他一些操作。
如果驗證失敗,表單對象還是會傳遞給模板但是表單對象可以提供有用的錯誤信息給用戶。
接下來創建模板,新建一個文件templates/registration/register.html,然后田間如下代碼:
{% extends "base.html" %} {% block title %}User Registration{% endblock %} {% block head %}User Registration{% endblock %} {% block content %} <form method="post" action="."> {{ form.as_p }}
{% csrf_token %} <input type="submit" value="register" /> </form> {% endblock %}
現在發現視圖與模板的緊湊與直接沒,這些都是Django強大的表單庫的功勞。
最后添加url,編輯urls.py,添加如下代碼:
(r'^register/$', register_page),
這樣就大功告成了!打開頁面一看,怎么感覺有點凌亂,為了讓它看起來正常點,我們給它增加一點樣式,編輯style.css:
input {
display: block;
}
現在看起來就正常多了,多測試幾遍,看看輸入正確信息與不正確信息頁面將如何展示。
注冊用戶頁面實現之后,我們還需要給導航菜單添加注冊鏈接,編輯templates/base.html:
<div id="nav"> <a href="/">home</a> | {% if user.is_authenticated %} welcome {{ user.username }} (<a href="/logout">logout</a>) {% else %} <a href="/login/">login</a> | <a href="/register/">register</a> {% endif %} </div>
還有一件事沒做,那就是在用戶成功提交注冊信息之后,展示一個注冊成功的信息,實現這個功能非常簡單,只需要加載並展示一個模板即可,Django已經為我們提供了這樣一個視圖,那就是django.views.generic.simple中的direct_to_templates,我們可以直接使用它。
創建模板templates/registration/register_success.html:
{% extends "base.html" %} {% block title %}Registration Successful{% endblock %} {% block head %} Registration Completed Successfully {% endblock %} {% block content %} Thank you for registering. Your information has been saved in the database. Now you can either <a href="/login/">login</a> or go back to the <a href="/">main page</a>. {% endblock %}
然后編輯urls.py在開頭導入下面的方法:
from django.views.generic.simple import direct_to_template
然后添加下面的url:
(r'^register/success/$', direct_to_template, { 'template': 'registration/register_success.html' }),
這里模板名以字典的形式作為第三個參數傳遞給urlpattern,最后修改bookmarks/views.py中的register_view,在注冊成功之后重定向至上面的模板,將下面的代碼:
return HttpResponseRedirect('/')
改成:
return HttpResponseRedirect('/register/success/')
這樣成功注冊之后就會顯示注冊成功的頁面了。
賬戶管理
現在我們已經實現了會話管理類與用戶注冊功能,但是我們還可以讓用戶能夠更新賬戶信息,比如密碼或者郵件地址,實現這樣的功能,可以這樣做:
- 方法一,使用Django提供的通用賬號管理視圖
- 方法二,設計自己的表單與視圖函數。
每種方法都有各自的優缺點,設計自己的表單給予自己更多的控制權,但是需要更多的編碼,而使用Django提供的視圖,則更為簡單,更快捷,但是這樣一來就只能使用Django提供的表單了。
我來總結一下django.contrib.auth提供的通用視圖,每個視圖函數都需要相應的提供一個模板,並傳遞一些變量給這些模板,用戶的輸入由這些視圖函數處理,不需要我們關心。所有的視圖都位於django.contrib.auth.views當中:
- logout 注銷用戶,注銷成功之后返回一個模板
- logout_then_login 注銷用戶,然后重定向至登錄頁面
- password_change 允許用戶修改密碼
- password_change_done 修改完密碼之后,展示一個模板
- password_reset 允許用戶重置密碼,都是郵件接收新密碼。
- password_reset_done,同上,但是返回一個頁面
- redirect_to_login 重定向至登錄頁面
這些視圖的用戶與開始講的login視圖類似,更多信息可以查閱Django官方文檔。通過這篇的學習,我們可以對Django認證系統以及會話機制有了一個了解。在我們以后自己的項目中就可以學以致用了。