# 第十四章: 會話、用戶和注冊
# 第十四章: 會話、用戶和注冊
是時候承認了: 我們有意的避開了Web開發中極其重要的方面。 到目前為止,我們都在假定,網站流量是大量的匿名用戶帶來的。
這當然不對。 瀏覽器的背后都是活生生的人(至少某些時候是)。 這忽略了重要的一點: 互聯網服務于人而不是機器。 要開發一個真正令人心動的網站,我們必須面對瀏覽器后面活生生的人。
很不幸,這并不容易。 HTTP被設計為”無狀態”,每次請求都處于相同的空間中。 在一次請求和下一次請求之間沒有任何狀態保持,我們無法根據請求的任何方面(IP地址,用戶代理等)來識別來自同一人的連續請求。
在本章中你將學會如何搞定狀態的問題。 好了,我們會從較低的層次(*cookies*)開始,然后過渡到用高層的工具來搞定會話,用戶和注冊的問題。
## Cookies
瀏覽器的開發者在很早的時候就已經意識到, HTTP’s 的無狀態會對Web開發者帶來很大的問題,于是(*cookies*)應運而生。 cookies 是瀏覽器為 Web 服務器存儲的一小段信息。 每次瀏覽器從某個服務器請求頁面時,它向服務器回送之前收到的cookies
來看看它是怎么工作的。 當你打開瀏覽器并訪問 `google.com` ,你的瀏覽器會給Google發送一個HTTP請求,起始部分就象這樣:
```
<pre class="calibre9">```
GET / HTTP/1.1
Host: google.com
...
```
```
當 Google響應時,HTTP的響應是這樣的:
```
<pre class="calibre9">```
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671;
expires=Sun, 17-Jan-2038 19:14:07 GMT;
path=/; domain=.google.com
Server: GWS/2.1
...
```
```
注意 `Set-Cookie` 的頭部。 你的瀏覽器會存儲cookie值( `PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671` ) ,而且每次訪問google 站點都會回送這個cookie值。 因此當你下次訪問Google時,你的瀏覽器會發送像這樣的請求:
```
<pre class="calibre9">```
GET / HTTP/1.1
Host: google.com
Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671
...
```
```
于是 `Cookies` 的值會告訴Google,你就是早些時候訪問過Google網站的人。 這個值可能是數據庫中存儲用戶信息的key,可以用它在頁面上顯示你的用戶名。 Google會(以及目前)使用它在網頁上顯示你賬號的用戶名。
### 存取Cookies
在Django中處理持久化,大部分時候你會更愿意用高層些的session 和/或 后面要討論的user 框架。 但在此之前,我們需要停下來在底層看看如何讀寫cookies。 這會幫助你理解本章節后面要討論的工具是如何工作的,而且如果你需要自己操作cookies,這也會有所幫助。
讀取已經設置好的cookies極其簡單。 每一個`HttpRequest` 對象都有一個`COOKIES` 對象,該對象的行為類似一個字典,你可以使用它讀取任何瀏覽器發送給視圖(view)的cookies。
```
<pre class="calibre9">```
def show_color(request):
if "favorite_color" in request.COOKIES:
return HttpResponse("Your favorite color is %s" % request.COOKIES["favorite_color"])
else:
return HttpResponse("You don't have a favorite color.")
```
```
寫cookies稍微復雜點。 你需要使用 `HttpResponse`對象的 `set_cookie()`方法。 這兒有個基于 `GET` 參數來設置 `favorite_color`
cookie的例子:
```
<pre class="calibre9">```
def set_color(request):
if "favorite_color" in request.GET:
# Create an HttpResponse object...
response = HttpResponse("Your favorite color is now %s" % request.GET["favorite_color"])
# ... and set a cookie on the response
response.set_cookie("favorite_color",
request.GET["favorite_color"])
return response
else:
return HttpResponse("You didn't give a favorite color.")
```
```
你可以給 `response.set_cookie()` 傳遞一些可選的參數來控制cookie的行為,詳見表14-1。
System Message: ERROR/3 (`<string>`, line 145)
Error parsing content block for the “table” directive: exactly one table expected.
```
<pre class="calibre9">```
.. table:: 表 14-1: Cookie 選項
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|參數 |缺省值 |描述 |
+=================================+===========================+====================================================================================================================================================================================+
|``max_age`` |``None`` |cookie需要延續的時間(以秒為單位) 如果參數是\ `` None`` ,這個cookie會延續到瀏覽器關閉為止。 |
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|``expires`` |``None`` |cookie失效的實際日期/時間。 它的格式必須是:\ `` "Wdy, DD-Mth-YY HH:MM:SS GMT"`` 。如果給出了這個參數,它會覆蓋\ `` max_age`` 參數。 |
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|``path`` |``"/"`` |cookie生效的路徑前綴。 瀏覽器只會把cookie回傳給帶有該路徑的頁 面,這樣你可以避免將cookie傳給站點中的其他的應用。 |
| | | |
| | |當你不是控制你的站點的頂層時,這樣做是特別有用的。 |
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|``domain`` |``None`` |這個cookie有效的站點。 你可以使用這個參數設置一個跨站點(cross-domain)的cookie。 比如,\ `` domain=".example.com"`` 可以設置一個在\ `` www.example.com`` 、\ `` www2.example.com`` 以及\ `` an.other.sub.domain.example.com`` 站點下都可讀到的cookie。|
| | | |
| | |如果這個參數被設成\ `` None`` ,cookie將只能在設置它的站點下可以讀到。 |
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|``False`` |``False`` |如果設置為 ``True`` ,瀏覽器將通過HTTPS來回傳cookie。 |
+---------------------------------+---------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
```
```
### 好壞參半的Cookies
也許你已經注意到了,cookies的工作方式可能導致的問題。 讓我們看一下其中一些比較重要的問題:
> cookie的存儲是自愿的,一個客戶端不一定要去接受或存儲cookie。 事實上,所有的瀏覽器都讓用戶自己控制 是否接受cookies。 如果你想知道cookies對于Web應用有多重要,你可以試著打開這個瀏覽器的 選項:
>
> 盡管cookies廣為使用,但仍被認為是不可靠的的。 這意味著,開發者使用cookies之前必須 檢查用戶是否可以接收cookie。
>
> Cookie(特別是那些沒通過HTTPS傳輸的)是非常不安全的。 因為HTTP數據是以明文發送的,所以 特別容易受到嗅探攻擊。 也就是說,嗅探攻擊者可以在網絡中攔截并讀取cookies,因此你要 絕對避免在cookies中存儲敏感信息。 這就意味著您不應該使用cookie來在存儲任何敏感信息。
>
> 還有一種被稱為”中間人”的攻擊更陰險,攻擊者攔截一個cookie并將其用于另一個用戶。 第19章將深入討論這種攻擊的本質以及如何避免。
>
> 即使從預想中的接收者返回的cookie也是不安全的。 在大多數瀏覽器中您可以非常容易地修改cookies中的信息。有經驗的用戶甚至可以通過像mechanize(<http://wwwsearch.sourceforge.net/mechanize/>) 這樣的工具手工構造一個HTTP請求。
>
> 因此不能在cookies中存儲可能會被篡改的敏感數據。 在cookies中存儲 `IsLoggedIn=1` ,以標識用戶已經登錄。 犯這類錯誤的站點數量多的令人難以置信; 繞過這些網站的安全系統也是易如反掌。
## Django的 Session 框架
由于存在的限制與安全漏洞,cookies和持續性會話已經成為Web開發中令人頭疼的典范。 好消息是,Django的目標正是高效的“頭疼殺手”,它自帶的session框架會幫你搞定這些問題。
你可以用session 框架來存取每個訪問者任意數據, 這些數據在服務器端存儲,并對cookie的收發進行了抽象。 Cookies只存儲數據的哈希會話ID,而不是數據本身,從而避免了大部分的常見cookie問題。
下面我們來看看如何打開session功能,并在視圖中使用它。
### 打開 Sessions功能
Sessions 功能是通過一個中間件(參見第17章)和一個模型(model)來實現的。 要打開sessions功能,需要以下幾步操作:
1. 編輯 `MIDDLEWARE_CLASSES` 配置,確保 `MIDDLEWARE_CLASSES` 中包含 `'django.contrib.sessions.middleware.SessionMiddleware'`。
2. 確認 `INSTALLED_APPS` 中有 `'django.contrib.sessions'` (如果你是剛打開這個應用,別忘了運行 `manage.py syncdb` )
如果項目是用 `startproject` 來創建的,配置文件中都已經安裝了這些東西,除非你自己刪除,正常情況下,你無需任何設置就可以使用session功能。
如果不需要session功能,你可以刪除 `MIDDLEWARE_CLASSES` 設置中的 `SessionMiddleware` 和 `INSTALLED_APPS` 設置中的 `'django.contrib.sessions'` 。雖然這只會節省很少的開銷,但積少成多啊。
### 在視圖中使用Session
`SessionMiddleware` 激活后,每個傳給視圖(view)函數的第一個參數`HttpRequest` 對象都有一個 `session` 屬性,這是一個字典型的對象。 你可以象用普通字典一樣來用它。 例如,在視圖(view)中你可以這樣用:
```
<pre class="calibre9">```
# Set a session value:
request.session["fav_color"] = "blue"
# Get a session value -- this could be called in a different view,
# or many requests later (or both):
fav_color = request.session["fav_color"]
# Clear an item from the session:
del request.session["fav_color"]
# Check if the session has a given key:
if "fav_color" in request.session:
...
```
```
其他的映射方法,如 `keys()` 和 `items()` 對 `request.session` 同樣有效:
下面是一些有效使用Django sessions的簡單規則:
> 用正常的字符串作為key來訪問字典 `request.session` , 而不是整數、對象或其它什么的。
>
> Session字典中以下劃線開頭的key值是Django內部保留key值。 框架只會用很少的幾個下劃線 開頭的session變量,除非你知道他們的具體含義,而且愿意跟上Django的變化,否則,最好 不要用這些下劃線開頭的變量,它們會讓Django攪亂你的應用。
>
> 比如,不要象這樣使用`_fav_color` 會話密鑰(session key):
```
<pre class="calibre9">```
request.session['_fav_color'] = 'blue' # Don't do this!
```
```
> 不要用一個新對象來替換掉 `request.session` ,也不要存取其屬性。 可以像Python中的字典那樣使用。 例如:
```
<pre class="calibre9">```
request.session = some_other_object # Don't do this!
request.session.foo = 'bar' # Don't do this!
```
```
我們來看個簡單的例子。 這是個簡單到不能再簡單的例子:在用戶發了一次評論后將`has_commented`設置為`True`。 這是個簡單(但不很安全)的、防止用戶多次評論的方法。
```
<pre class="calibre9">```
def post_comment(request):
if request.method != 'POST':
raise Http404('Only POSTs are allowed')
if 'comment' not in request.POST:
raise Http404('Comment not submitted')
if request.session.get('has_commented', False):
return HttpResponse("You've already commented.")
c = comments.Comment(comment=request.POST['comment'])
c.save()
request.session['has_commented'] = True
return HttpResponse('Thanks for your comment!')
```
```
下面是一個很簡單的站點登錄視圖(view):
```
<pre class="calibre9">```
def login(request):
if request.method != 'POST':
raise Http404('Only POSTs are allowed')
try:
m = Member.objects.get(username=request.POST['username'])
if m.password == request.POST['password']:
request.session['member_id'] = m.id
return HttpResponseRedirect('/you-are-logged-in/')
except Member.DoesNotExist:
return HttpResponse("Your username and password didn't match.")
```
```
下面的例子將登出一個在上面已通過`login()` 登錄的用戶:
```
<pre class="calibre9">```
def logout(request):
try:
del request.session['member_id']
except KeyError:
pass
return HttpResponse("You're logged out.")
```
```
注意
在實踐中,這是很爛的用戶登錄方式,稍后討論的認證(authentication )框架會幫你以更健壯和有利的方式來處理這些問題。 這些非常簡單的例子只是想讓你知道這一切是如何工作的。 這些實例盡量簡單,這樣你可以更容易看到發生了什么
### 設置測試Cookies
就像前面提到的,你不能指望所有的瀏覽器都可以接受cookie。 因此,為了使用方便,Django提供了一個簡單的方法來測試用戶的瀏覽器是否接受cookie。 你只需在視圖(view)中調用 `request.session.set_test_cookie()`
,并在后續的視圖(view)、而不是當前的視圖(view)中檢查 `request.session.test_cookie_worked()` 。
雖然把 `set_test_cookie()` 和 `test_cookie_worked()` 分開的做法看起來有些笨拙,但由于cookie的工作方式,這無可避免。 當設置一個cookie時候,只能等瀏覽器下次訪問的時候,你才能知道瀏覽器是否接受cookie。
檢查cookie是否可以正常工作后,你得自己用 `delete_test_cookie()` 來清除它,這是個好習慣。 在你證實了測試cookie已工作了之后這樣操作。
這是個典型例子:
```
<pre class="calibre9">```
def login(request):
# If we submitted the form...
if request.method == 'POST':
# Check that the test cookie worked (we set it below):
if request.session.test_cookie_worked():
# The test cookie worked, so delete it.
request.session.delete_test_cookie()
# In practice, we'd need some logic to check username/password
# here, but since this is an example...
return HttpResponse("You're logged in.")
# The test cookie failed, so display an error message. If this
# were a real site, we'd want to display a friendlier message.
else:
return HttpResponse("Please enable cookies and try again.")
# If we didn't post, send the test cookie along with the login form.
request.session.set_test_cookie()
return render_to_response('foo/login_form.html')
```
```
注意
再次強調,內置的認證函數會幫你做檢查的。
### 在視圖(View)外使用Session
從內部來看,每個session都只是一個普通的Django model(在 `django.contrib.sessions.models` 中定義)。每個session都由一個隨機的32字節哈希串來標識,并存儲于cookie中。 因為它是一個標準的模型,所以你可以使用Django數據庫API來存取session。
```
<pre class="calibre9">```
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)
```
```
你需要使用`get_decoded()` 來讀取實際的session數據。 這是必需的,因為字典存儲為一種特定的編碼格式。
```
<pre class="calibre9">```
>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}
```
```
### 何時保存Session
缺省的情況下,Django只會在session發生變化的時候才會存入數據庫,比如說,字典賦值或刪除。
```
<pre class="calibre9">```
# Session is modified.
request.session['foo'] = 'bar'
# Session is modified.
del request.session['foo']
# Session is modified.
request.session['foo'] = {}
# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session['foo']['bar'] = 'baz'
```
```
你可以設置 `SESSION_SAVE_EVERY_REQUEST` 為 `True` 來改變這一缺省行為。如果置為True的話,Django會在每次收到請求的時候保存session,即使沒發生變化。
注意,會話cookie只會在創建和修改的時候才會送出。 但如果 `SESSION_SAVE_EVERY_REQUEST` 設置為 `True` ,會話cookie在每次請求的時候都會送出。 同時,每次會話cookie送出的時候,其 `expires` 參數都會更新。
### 瀏覽器關閉即失效會話 vs 持久會話
你可能注意到了,Google給我們發送的cookie中有 `expires=Sun, 17-Jan-2038 19:14:07 GMT;` cookie可以有過期時間,這樣瀏覽器就知道什么時候可以刪除cookie了。 如果cookie沒有設置過期時間,當用戶關閉瀏覽器的時候,cookie就自動過期了。 你可以改變 `SESSION_EXPIRE_AT_BROWSER_CLOSE` 的設置來控制session框架的這一行為。
缺省情況下, `SESSION_EXPIRE_AT_BROWSER_CLOSE` 設置為 `False` ,這樣,會話cookie可以在用戶瀏覽器中保持有效達 `SESSION_COOKIE_AGE` 秒(缺省設置是兩周,即1,209,600 秒)。 如果你不想用戶每次打開瀏覽器都必須重新登陸的話,用這個參數來幫你。
如果 `SESSION_EXPIRE_AT_BROWSER_CLOSE` 設置為 `True` ,當瀏覽器關閉時,Django會使cookie失效。
### 其他的Session設置
除了上面提到的設置,還有一些其他的設置可以影響Django session框架如何使用cookie,詳見表 14-2.
表 14-2\\. 影響cookie行為的設置 設置描述缺省`SESSION\_COOKIE\_DOMAIN`使用會話cookie(session cookies)的站點。 將它設成一個字符串,就好象`` “.example.com”`` 以用于跨站點(cross-domain)的cookie,或`` None`` 以用于單個站點。`None``SESSION\_COOKIE\_NAME`會話中使用的cookie的名字。 它可以是任意的字符串。`"sessionid"``SESSION\_COOKIE\_SECURE`是否在session中使用安全cookie。 如果設置 `True` , cookie就會標記為安全, 這意味著cookie只會通過HTTPS來傳輸。`False`技術細節
如果你還是好奇的話,下面是一些關于session框架內部工作方式的技術細節:
> session 字典接受任何支持序列化的Python對象。 參考Python內建模塊`pickle`的文檔以獲取更多信息。
>
> Session 數據存在數據庫表 `django_session` 中
>
> Session 數據在需要的時候才會讀取。 如果你從不使用 `request.session` , Django不會動相關數據庫表的一根毛。
>
> Django 只在需要的時候才送出cookie。 如果你壓根兒就沒有設置任何會話數據,它不會 送出會話cookie(除非 `SESSION_SAVE_EVERY_REQUEST` 設置為 `True` )。
>
> Django session 框架完全而且只能基于cookie。 它不會后退到把會話ID編碼在URL中(像某些工具(PHP,JSP)那樣)。
>
> 這是一個有意而為之的設計。 把session放在URL中不只是難看,更重要的是這讓你的站點 很容易受到攻擊——通過 `Referer` header進行session ID”竊聽”而實施的攻擊。
如果你還是好奇,閱讀源代碼是最直接辦法,詳見 `django.contrib.sessions` 。
## 用戶與Authentication
通過session,我們可以在多次瀏覽器請求中保持數據, 接下來的部分就是用session來處理用戶登錄了。 當然,不能僅憑用戶的一面之詞,我們就相信,所以我們需要認證。
當然了,Django 也提供了工具來處理這樣的常見任務(就像其他常見任務一樣)。 Django 用戶認證系統處理用戶帳號,組,權限以及基于cookie的用戶會話。 這個系統一般被稱為 *auth/auth* (認證與授權)系統。 這個系統的名稱同時也表明了用戶常見的兩步處理。 我們需要
1. 驗證 (*認證*) 用戶是否是他所宣稱的用戶(一般通過查詢數據庫驗證其用戶名和密碼)
2. 驗證用戶是否擁有執行某種操作的 *授權* (通常會通過檢查一個權限表來確認)
根據這些需求,Django 認證/授權 系統會包含以下的部分:
- *用戶* : 在網站注冊的人
- *權限* : 用于標識用戶是否可以執行某種操作的二進制(yes/no)標志
- *組* :一種可以將標記和權限應用于多個用戶的常用方法
- *Messages* : 向用戶顯示隊列式的系統消息的常用方法
如果你已經用了admin工具(詳見第6章),就會看見這些工具的大部分。如果你在admin工具中編輯過用戶或組,那么實際上你已經編輯過授權系統的數據庫表了。
### 打開認證支持
像session工具一樣,認證支持也是一個Django應用,放在 `django.contrib` 中,所以也需要安裝。 與session系統相似,它也是缺省安裝的,但如果它已經被刪除了,通過以下步驟也能重新安裝上:
1. 根據本章早前的部分確認已經安裝了session 框架。 需要確認用戶使用cookie,這樣sesson 框架才能正常使用。
2. 將 `'django.contrib.auth'` 放在你的 `INSTALLED_APPS` 設置中,然后運行 `manage.py syncdb`以創建對應的數據庫表。
3. 確認 `SessionMiddleware` 后面的 `MIDDLEWARE_CLASSES` 設置中包含 `'django.contrib.auth.middleware.AuthenticationMiddleware'` SessionMiddleware。
這樣安裝后,我們就可以在視圖(view)的函數中處理user了。 在視圖中存取users,主要用 `request.user` ;這個對象表示當前已登錄的用戶。 如果用戶還沒登錄,這就是一個`AnonymousUser`對象(細節見下)。
你可以很容易地通過 `is_authenticated()` 方法來判斷一個用戶是否已經登錄了:
```
<pre class="calibre9">```
if request.user.is_authenticated():
# Do something for authenticated users.
else:
# Do something for anonymous users.
```
```
### 使用User對象
`User` 實例一般從 `request.user` ,或是其他下面即將要討論到的方法取得,它有很多屬性和方法。 `AnonymousUser` 對象模擬了 *部分* 的接口,但不是全部,在把它當成真正的user對象 使用前,你得檢查一下 `user.is_authenticated()` 表14-3和14-4分別列出了`User` 對象中的屬性(fields)和方法。
表 14-3\\. `User` 對象屬性 屬性描述username必需的,不能多于30個字符。 僅用字母數字式字符(字母、數字和下劃線)。`first\_name`可選; 少于等于30字符。`last\_name`可選; 少于等于30字符。`email`可選。 郵件地址。`password`必需的。 密碼的哈希值(Django不儲存原始密碼)。 See the Passwords section for more about this value.`is\_staff`布爾值。 用戶是否擁有網站的管理權限。`is\_active`布爾值. 設置該賬戶是否可以登錄。 把該標志位置為`False`而不是直接刪除賬戶。`is\_superuser`布爾值 標識用戶是否擁有所有權限,無需顯式地權限分配定義。`last\_login`用戶上次登錄的時間日期。 它被默認設置為當前的日期/時間。`date\_joined`賬號被創建的日期時間 當賬號被創建時,它被默認設置為當前的日期/時間。System Message: ERROR/3 (`<string>`, line 735)
Error parsing content block for the “table” directive: exactly one table expected.
```
<pre class="calibre9">```
.. table:: 表 14-4\. ``User`` 對象方法
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|方法 |描述 |
+=============================================================================================+======================================================================================================================================================+
|``is_authenticated()`` |對于真實的User對象,總是返回\ `` True`` 。 |
| |這是一個分辨用戶是否已被鑒證的方法。 它并不意味著任何權限,也不檢查用戶是否仍是活動的。 它僅說明此用戶已被成功鑒證。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``is_anonymous()`` |對于\ `` AnonymousUser`` 對象返回\ `` True`` (對于真實的\ `` User`` 對象返回\ `` False`` )。 |
| |總的來說,比起這個方法,你應該傾向于使用\ `` is_authenticated()`` 方法。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``get_full_name()`` |返回\ `` first_name`` 加上\ `` last_name`` ,中間插入一個空格。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``set_password(passwd)`` |設定用戶密碼為指定字符串(自動處理成哈希串)。 實際上沒有保存\ ``User``\對象。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|check_password(passwd) |如果指定的字符串與用戶密碼匹配則返回\ ``True``\。 比較時會使用密碼哈希表。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``get_group_permissions()`` |返回一個用戶通過其所屬組獲得的權限字符串列表。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``get_all_permissions()`` |返回一個用戶通過其所屬組以及自身權限所獲得的權限字符串列表。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``has_perm(perm)`` |如果用戶有指定的權限,則返回\ `` True`` ,此時\ `` perm`` 的格式是\ `` "package.codename"`` 。如果用戶已不活動,此方法總是返回\ `` False`` 。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|has_perms(perm_list) |如果用戶擁有\ * 全部* 的指定權限,則返回\ `` True`` 。 如果用戶是不活動的,這個方法總是返回\ `` False`` 。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``has_module_perms(app_label)`` |如果用戶擁有給定的\ `` app_label`` 中的任何權限,則返回\ `` True`` 。如果用戶已不活動,這個方法總是返回\ `` False`` 。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|get_and_delete_messages() |返回一個用戶隊列中的\ `` Message`` 對象列表,并從隊列中將這些消息刪除。 |
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
|``email_user(subj, msg)`` |向用戶發送一封電子郵件。 這封電子郵件是從\ `` DEFAULT_FROM_EMAIL`` 設置的地址發送的。 你還可以傳送一個第三參數:\ `` from_email`` ,以覆蓋電郵中的發送地址。|
+---------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+
```
```
最后,`User` 對象有兩個many-to-many屬性。 `groups` 和`permissions` 。正如其他的many-to-many屬性使用的方法一樣,`User` 對象可以獲得它們相關的對象:
```
<pre class="calibre9">```
# Set a user's groups:
myuser.groups = group_list
# Add a user to some groups:
myuser.groups.add(group1, group2,...)
# Remove a user from some groups:
myuser.groups.remove(group1, group2,...)
# Remove a user from all groups:
myuser.groups.clear()
# Permissions work the same way
myuser.permissions = permission_list
myuser.permissions.add(permission1, permission2, ...)
myuser.permissions.remove(permission1, permission2, ...)
myuser.permissions.clear()
```
```
### 登錄和退出
Django 提供內置的視圖(view)函數用于處理登錄和退出 (以及其他奇技淫巧),但在開始前,我們來看看如何手工登錄和退出。 Django提供兩個函數來執行`django.contrib.auth`\\中的動作 : `authenticate()`
和`login()`。
認證給出的用戶名和密碼,使用 `authenticate()` 函數。它接受兩個參數,用戶名 username 和 密碼 password ,并在密碼對給出的用戶名合法的情況下返回一個 User 對象。 如果密碼不合法,`authenticate()`返回`None`。
```
<pre class="calibre9">```
>>> from django.contrib import auth
>>> user = auth.authenticate(username='john', password='secret')
>>> if user is not None:
... print "Correct!"
... else:
... print "Invalid password."
```
```
`authenticate()` 只是驗證一個用戶的證書而已。 而要登錄一個用戶,使用 `login()` 。該函數接受一個 `HttpRequest` 對象和一個 `User` 對象作為參數并使用Django的會話( `session` )框架把用戶的ID保存在該會話中。
下面的例子演示了如何在一個視圖中同時使用 `authenticate()` 和 `login()` 函數:
```
<pre class="calibre9">```
from django.contrib import auth
def login_view(request):
username = request.POST.get('username', '')
password = request.POST.get('password', '')
user = auth.authenticate(username=username, password=password)
if user is not None and user.is_active:
# Correct password, and the user is marked "active"
auth.login(request, user)
# Redirect to a success page.
return HttpResponseRedirect("/account/loggedin/")
else:
# Show an error page
return HttpResponseRedirect("/account/invalid/")
```
```
注銷一個用戶,在你的視圖中使用 `django.contrib.auth.logout()` 。 它接受一個`HttpRequest`對象并且沒有返回值。
```
<pre class="calibre9">```
from django.contrib import auth
def logout_view(request):
auth.logout(request)
# Redirect to a success page.
return HttpResponseRedirect("/account/loggedout/")
```
```
注意,即使用戶沒有登錄, `logout()` 也不會拋出任何異常。
在實際中,你一般不需要自己寫登錄/登出的函數;認證系統提供了一系例視圖用來處理登錄和登出。 使用認證視圖的第一步是把它們寫在你的URLconf中。 你需要這樣寫:
```
<pre class="calibre9">```
from django.contrib.auth.views import login, logout
urlpatterns = patterns('',
# existing patterns here...
(r'^accounts/login/$', login),
(r'^accounts/logout/$', logout),
)
```
```
`/accounts/login/` 和 `/accounts/logout/` 是Django提供的視圖的默認URL。
缺省情況下, `login` 視圖渲染 `registragiton/login.html` 模板(可以通過視圖的額外參數 `template_name` 修改這個模板名稱)。 這個表單必須包含 `username` 和 `password` 域。如下示例: 一個簡單的 template 看起來是這樣的
```
<pre class="calibre9">```
{% extends "base.html" %}
{% block content %}
{% if form.errors %}
<p class="error">Sorry, that's not a valid username or password</p>
{% endif %}
<form action="" method="post">
<label for="username">User name:</label>
<input type="text" name="username" value="" id="username">
<label for="password">Password:</label>
<input type="password" name="password" value="" id="password">
<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next|escape }}" />
</form>
{% endblock %}
```
```
如果用戶登錄成功,缺省會重定向到 `/accounts/profile` 。 你可以提供一個保存登錄后重定向URL的`next`隱藏域來重載它的行為。 也可以把值以`GET`參數的形式發送給視圖函數,它會以變量`next`的形式保存在上下文中,這樣你就可以把它用在隱藏域上了。
logout視圖有一些不同。 默認情況下它渲染 `registration/logged_out.html` 模板(這個視圖一般包含你已經成功退出的信息)。 視圖中還可以包含一個參數 `next_page` 用于退出后重定向。
### 限制已登錄用戶的訪問
有很多原因需要控制用戶訪問站點的某部分。
一個簡單原始的限制方法是檢查 `request.user.is_authenticated()` ,然后重定向到登陸頁面:
```
<pre class="calibre9">```
from django.http import HttpResponseRedirect
def my_view(request):
if not request.user.is_authenticated():
return HttpResponseRedirect('/accounts/login/?next=%s' % request.path)
# ...
```
```
或者顯示一個出錯信息:
```
<pre class="calibre9">```
def my_view(request):
if not request.user.is_authenticated():
return render_to_response('myapp/login_error.html')
# ...
```
```
作為一個快捷方式, 你可以使用便捷的 `login_required` 修飾符:
```
<pre class="calibre9">```
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
# ...
```
```
`login_required` 做下面的事情:
- 如果用戶沒有登錄, 重定向到 `/accounts/login/` , 把當前絕對URL作為 `next` 在查詢字符串中傳遞過去, 例如: /accounts/login/?next=/polls/3/ 。
- 如果用戶已經登錄, 正常地執行視圖函數。 視圖代碼就可以假定用戶已經登錄了。
### 對通過測試的用戶限制訪問
限制訪問可以基于某種權限,某些檢查或者為login視圖提供不同的位置,這些實現方式大致相同。
一般的方法是直接在視圖的 `request.user` 上運行檢查。 例如,下面視圖確認用戶登錄并是否有 `polls.can_vote`權限:
```
<pre class="calibre9">```
def vote(request):
if request.user.is_authenticated() and request.user.has_perm('polls.can_vote')):
# vote here
else:
return HttpResponse("You can't vote in this poll.")
```
```
并且Django有一個稱為 `user_passes_test` 的簡潔方式。它接受參數然后為你指定的情況生成裝飾器。
```
<pre class="calibre9">```
def user_can_vote(user):
return user.is_authenticated() and user.has_perm("polls.can_vote")
@user_passes_test(user_can_vote, login_url="/login/")
def vote(request):
# Code here can assume a logged-in user with the correct permission.
...
```
```
`user_passes_test` 使用一個必需的參數: 一個可調用的方法,當存在 `User` 對象并當此用戶允許查看該頁面時返回 `True` 。 注意 user\_passes\_test 不會自動檢查 User
是否認證,你應該自己做這件事。
例子中我們也展示了第二個可選的參數 `login_url` ,它讓你指定你的登錄頁面的URL(默認為 `/accounts/login/` )。 如果用戶沒有通過測試,那么`user_passes_test`將把用戶重定向到`login_url`
既然檢查用戶是否有一個特殊權限是相對常見的任務,Django為這種情形提供了一個捷徑: `permission_required()` 裝飾器。 使用這個裝飾器,前面的例子可以改寫為:
```
<pre class="calibre9">```
from django.contrib.auth.decorators import permission_required
@permission_required('polls.can_vote', login_url="/login/")
def vote(request):
# ...
```
```
注意, `permission_required()` 也有一個可選的 `login_url` 參數, 這個參數默認為 `'/accounts/login/'` 。
限制通用視圖的訪問
在Django用戶郵件列表中問到最多的問題是關于對通用視圖的限制性訪問。 為實現這個功能,你需要自己包裝視圖,并且在URLconf中,將你自己的版本替換通用視圖:
```
<pre class="calibre9">```
from django.contrib.auth.decorators import login_required
from django.views.generic.date_based import object_detail
@login_required
def limited_object_detail(*args, **kwargs):
return object_detail(*args, **kwargs)
```
```
當然, 你可以用任何其他限定修飾符來替換 `login_required` 。
### 管理 Users, Permissions 和 Groups
管理認證系統最簡單的方法是通過管理界面。 第六章討論了怎樣使用Django的管理界面來編輯用戶和控制他們的權限和可訪問性,并且大多數時間你使用這個界面就可以了。
然而,當你需要絕對的控制權的時候,有一些低層 API 需要深入專研,我們將在下面的章節中討論它們。
#### 創建用戶
使用 `create_user` 輔助函數創建用戶:
```
<pre class="calibre9">```
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user(username='john',
... email='jlennon@beatles.com',
... password='glass onion')
```
```
在這里, `user` 是 `User` 類的一個實例,準備用于向數據庫中存儲數據。(`create_user()`實際上沒有調用`save()`)。 `create_user()` 函數并沒有在數據庫中創建記錄,在保存數據之前,你仍然可以繼續修改它的屬性值。
```
<pre class="calibre9">```
>>> user.is_staff = True
>>> user.save()
```
```
#### 修改密碼
你可以使用 `set_password()` 來修改密碼:
```
<pre class="calibre9">```
>>> user = User.objects.get(username='john')
>>> user.set_password('goo goo goo joob')
>>> user.save()
```
```
除非你清楚的知道自己在做什么,否則不要直接修改 `password` 屬性。 其中保存的是密碼的 *加入salt的hash值* ,所以不能直接編輯。
一般來說, `User` 對象的 `password` 屬性是一個字符串,格式如下:
```
<pre class="calibre9">```
hashtype$salt$hash
```
```
這是哈希類型,salt和哈希本身,用美元符號($)分隔。
`hashtype` 是 `sha1` (默認)或者 `md5` ,它是用來處理單向密碼哈希的算法。 Salt是一個用來加密原始密碼以創建哈希的隨機字符串,例如:
```
<pre class="calibre9">```
sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4
```
```
`User.set_password()` 和 `User.check_password()` 函數在后臺處理和檢查這些值。
salt化得哈希值
一次 *哈希* 是一次單向的加密過程,你能容易地計算出一個給定值的哈希碼,但是幾乎不可能從一個哈希碼解出它的原值。
如果我們以普通文本存儲密碼,任何能進入數據庫的人都能輕易的獲取每個人的密碼。 使用哈希方式來存儲密碼相應的減少了數據庫泄露密碼的可能。
然而,攻擊者仍然可以使用 *暴力破解* 使用上百萬個密碼與存儲的值對比來獲取數據庫密碼。 這需要花一些時間,但是智能電腦驚人的速度超出了你的想象。
更糟糕的是我們可以公開地得到 *rainbow tables* (一種暴力密碼破解表)或預備有上百萬哈希密碼值的數據庫。 使用rainbow tables可以在幾秒之內就能搞定最復雜的一個密碼。
在存儲的hash值的基礎上,加入 *salt* 值(一個隨機值),增加了密碼的強度,使得破解更加困難。 因為每個密碼的salt值都不相同,這也限制了rainbow table的使用,使得攻擊者只能使用最原始的暴力破解方法。
加入salt值得hash并不是絕對安全的存儲密碼的方法,然而卻是安全和方便之間很好的折衷。
#### 處理注冊
我們可以使用這些底層工具來創建允許用戶注冊的視圖。 最近每個開發人員都希望實現各自不同的注冊方法,所以Django把寫注冊視圖的工作留給了你。 幸運的是,這很容易。
作為這個事情的最簡化處理, 我們可以提供一個小視圖, 提示一些必須的用戶信息并創建這些用戶。 Django為此提供了可用的內置表單, 下面這個例子就使用了這個表單:
```
<pre class="calibre9">```
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
def register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
new_user = form.save()
return HttpResponseRedirect("/books/")
else:
form = UserCreationForm()
return render_to_response("registration/register.html", {
'form': form,
})
```
```
這個表單需要一個叫 `registration/register.html` 的模板。這個模板可能是這樣的:
```
<pre class="calibre9">```
{% extends "base.html" %}
{% block title %}Create an account{% endblock %}
{% block content %}
<h1>Create an account</h1>
<form action="" method="post">
{{ form.as_p }}
<input type="submit" value="Create the account">
</form>
{% endblock %}
```
```
### 在模板中使用認證數據
當前登入的用戶以及他(她)的權限可以通過 `RequestContext` 在模板的context中使用(詳見第9章)。
注意
從技術上來說,只有當你使用了 `RequestContext`這些變量才可用。 \_并且\_TEMPLATE\_CONTEXT\_PROCESSORS 設置包含了 “django.core.context\_processors.auth” (默認情況就是如此)時,這些變量才能在模板context中使用。 `TEMPLATE_CONTEXT_PROCESSORS` 設置包含了 `"django.core.context_processors.auth"` (默認情況就是如此)時,這些變量才能在模板context中使用。
當使用 `RequestContext` 時, 當前用戶 (是一個 `User` 實例或一個 `AnonymousUser` 實例) 存儲在模板變量 `{{ user }}` 中:
```
<pre class="calibre9">```
{% if user.is_authenticated %}
<p>Welcome, {{ user.username }}. Thanks for logging in.</p>
{% else %}
<p>Welcome, new user. Please log in.</p>
{% endif %}
```
```
這些用戶的權限信息存儲在 `{{ perms }}` 模板變量中。
你有兩種方式來使用 `perms` 對象。 你可以使用類似于 `{{ perms.polls }}` 的形式來檢查,對于某個特定的應用,一個用戶是否具有 *任意* 權限;你也可以使用 `{{ perms.polls.can_vote }}` 這樣的形式,來檢查一個用戶是否擁有特定的權限。
這樣你就可以在模板中的 `{% if %}` 語句中檢查權限:
```
<pre class="calibre9">```
{% if perms.polls %}
<p>You have permission to do something in the polls app.</p>
{% if perms.polls.can_vote %}
<p>You can vote!</p>
{% endif %}
{% else %}
<p>You don't have permission to do anything in the polls app.</p>
{% endif %}
```
```
## 權限、組和消息
在認證框架中還有其他的一些功能。 我們會在接下來的幾個部分中進一步地了解它們。
### 權限
權限可以很方便地標識用戶和用戶組可以執行的操作。 它們被Django的admin管理站點所使用,你也可以在你自己的代碼中使用它們。
Django的admin站點如下使用權限:
- 只有設置了 *add* 權限的用戶才能使用添加表單,添加對象的視圖。
- 只有設置了 *change* 權限的用戶才能使用變更列表,變更表格,變更對象的視圖。
- 只有設置了 *delete* 權限的用戶才能刪除一個對象。
權限是根據每一個類型的對象而設置的,并不具體到對象的特定實例。 例如,我們可以允許Mary改變新故事,但是目前還不允許設置Mary只能改變自己創建的新故事,或者根據給定的狀態,出版日期或者ID號來選擇權限。
會自動為每一個Django模型創建三個基本權限:增加、改變和刪除。 當你運行`manage.py syncdb`命令時,這些權限被添加到`auth_permission`數據庫表中。
權限以 `"<app>.<action>_<object_name>"` 的形式出現。
就跟用戶一樣,權限也就是Django模型中的 `django.contrib.auth.models` 。因此如果你愿意,你也可以通過Django的數據庫API直接操作權限。
### 組
組提供了一種通用的方式來讓你按照一定的權限規則和其他標簽將用戶分類。 一個用戶可以隸屬于任何數量的組。
在一個組中的用戶自動獲得了賦予該組的權限。 例如, `Site editors` 組擁有 `can_edit_home_page` 權限,任何在該組中的用戶都擁有這個權限。
組也可以通過給定一些用戶特殊的標記,來擴展功能。 例如,你創建了一個 `'Special users'` 組,并且允許組中的用戶訪問站點的一些VIP部分,或者發送VIP的郵件消息。
和用戶管理一樣,admin接口是管理組的最簡單的方法。 然而,組也就是Django模型 `django.contrib.auth.models` ,因此你可以使用Django的數據庫API,在底層訪問這些組。
### 消息
消息系統會為給定的用戶接收消息。 每個消息都和一個 `User` 相關聯。
在每個成功的操作以后,Django的admin管理接口就會使用消息機制。 例如,當你創建了一個對象,你會在admin頁面的頂上看到 `The object was created successfully` 的消息。
你也可以使用相同的API在你自己的應用中排隊接收和顯示消息。 API非常地簡單:
- 要創建一條新的消息,使用 `user.message_set.create(message='message_text')` 。
- 要獲得/刪除消息,使用 `user.get_and_delete_messages()` ,這會返回一個 `Message` 對象的列表,并且從隊列中刪除返回的項。
在例子視圖中,系統在創建了播放單(playlist)以后,為用戶保存了一條消息。
```
<pre class="calibre9">```
def create_playlist(request, songs):
# Create the playlist with the given songs.
# ...
request.user.message_set.create(
message="Your playlist was added successfully."
)
return render_to_response("playlists/create.html",
context_instance=RequestContext(request))
```
```
當使用 `RequestContext` ,當前登錄的用戶以及他(她)的消息,就會以模板變量 `{{ messages }}` 出現在模板的context中。
```
<pre class="calibre9">```
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
```
```
需要注意的是 `RequestContext` 會在后臺調用 `get_and_delete_messages` ,因此即使你沒有顯示它們,它們也會被刪除掉。
最后注意,這個消息框架只能服務于在用戶數據庫中存在的用戶。 如果要向匿名用戶發送消息,請直接使用會話框架。
## 下一章
是的,會話和認證系統有太多的東西要學。 大多數情況下,你并不需要本章所提到的所有功能。
在下一章,我們會看一下Django的緩存機制,這是一個提高你的網頁應用性能的便利的辦法。