第十四章: 會話、用戶和註冊

第十四章: 會話、用戶和註冊

是時候認可了: 咱們有意的避開了Web開發中極其重要的方面。 到目前爲止,咱們都在假定,網站流量是大量的匿名用戶帶來的。html

這固然不對。 瀏覽器的背後都是活生生的人(至少某些時候是)。 這忽略了重要的一點: 互聯網服務於人而不是機器。 要開發一個真正使人心動的網站,咱們必須面對瀏覽器後面活生生的人。git

很不幸,這並不容易。 HTTP被設計爲」無狀態」,每次請求都處於相同的空間中。 在一次請求和下一次請求之間沒有任何狀態保持,咱們沒法根據請求的任何方面(IP地址,用戶代理等)來識別來自同一人的連續請求。算法

在本章中你將學會如何搞定狀態的問題。 好了,咱們會從較低的層次(cookies)開始,而後過渡到用高層的工具來搞定會話,用戶和註冊的問題。數據庫

Cookies

瀏覽器的開發者在很早的時候就已經意識到, HTTP’s 的無狀態會對Web開發者帶來很大的問題,因而(cookies)應運而生。 cookies 是瀏覽器爲 Web 服務器存儲的一小段信息。 每次瀏覽器從某個服務器請求頁面時,它向服務器回送以前收到的cookiesdjango

來看看它是怎麼工做的。 當你打開瀏覽器並訪問 google.com ,你的瀏覽器會給Google發送一個HTTP請求,起始部分就象這樣:瀏覽器

GET / HTTP/1.1
Host: google.com
...

當 Google響應時,HTTP的響應是這樣的:安全

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時,你的瀏覽器會發送像這樣的請求:服務器

GET / HTTP/1.1
Host: google.com
Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671
...

因而 Cookies 的值會告訴Google,你就是早些時候訪問過Google網站的人。 這個值多是數據庫中存儲用戶信息的key,能夠用它在頁面上顯示你的用戶名。 Google會(以及目前)使用它在網頁上顯示你帳號的用戶名。cookie

存取Cookies

在Django中處理持久化,大部分時候你會更願意用高層些的session 和/或 後面要討論的user 框架。 但在此以前,咱們須要停下來在底層看看如何讀寫cookies。 這會幫助你理解本章節後面要討論的工具是如何工做的,並且若是你須要本身操做cookies,這也會有所幫助。網絡

讀取已經設置好的cookies極其簡單。 每個`` HttpRequest`` 對象都有一個`` COOKIES`` 對象,該對象的行爲相似一個字典,你可使用它讀取任何瀏覽器發送給視圖(view)的cookies。

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的例子:

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.

.. 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)中你能夠這樣用:

# 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):

request.session['_fav_color'] = 'blue' # Don't do this!

不要用一個新對象來替換掉 request.session ,也不要存取其屬性。 能夠像Python中的字典那樣使用。 例如:

request.session = some_other_object # Don't do this!

request.session.foo = 'bar' # Don't do this!

咱們來看個簡單的例子。 這是個簡單到不能再簡單的例子:在用戶發了一次評論後將has_commented設置爲True。 這是個簡單(但不很安全)的、防止用戶屢次評論的方法。

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):

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()`` 登陸的用戶:

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已工做了以後這樣操做。

這是個典型例子:

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。

>>> 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數據。 這是必需的,由於字典存儲爲一種特定的編碼格式。

>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}

什麼時候保存Session

缺省的狀況下,Django只會在session發生變化的時候纔會存入數據庫,好比說,字典賦值或刪除。

# 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() 方法來判斷一個用戶是否已經登陸了:

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)和方法。

User 表 14-3.  對象屬性
屬性 描述
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.

.. 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`` 對象能夠得到它們相關的對象:

# 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

>>> 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() 函數:

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對象而且沒有返回值。

from django.contrib import auth

def logout_view(request):
    auth.logout(request)
    # Redirect to a success page.
    return HttpResponseRedirect("/account/loggedout/")

注意,即便用戶沒有登陸, logout() 也不會拋出任何異常。

在實際中,你通常不須要本身寫登陸/登出的函數;認證系統提供了一系例視圖用來處理登陸和登出。 使用認證視圖的第一步是把它們寫在你的URLconf中。 你須要這樣寫:

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 看起來是這樣的

{% 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() ,而後重定向到登錄頁面:

from django.http import HttpResponseRedirect

def my_view(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/?next=%s' % request.path)
    # ...

或者顯示一個出錯信息:

def my_view(request):
    if not request.user.is_authenticated():
        return render_to_response('myapp/login_error.html')
    # ...

做爲一個快捷方式, 你可使用便捷的 login_required 修飾符:

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權限:

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 的簡潔方式。它接受參數而後爲你指定的狀況生成裝飾器。

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() 裝飾器。 使用這個裝飾器,前面的例子能夠改寫爲:

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中,將你本身的版本替換通用視圖:

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 輔助函數建立用戶:

>>> 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() 函數並無在數據庫中建立記錄,在保存數據以前,你仍然能夠繼續修改它的屬性值。

>>> user.is_staff = True
>>> user.save()

修改密碼

你可使用 set_password() 來修改密碼:

>>> user = User.objects.get(username='john')
>>> user.set_password('goo goo goo joob')
>>> user.save()

除非你清楚的知道本身在作什麼,不然不要直接修改 password 屬性。 其中保存的是密碼的 加入salt的hash值 ,因此不能直接編輯。

通常來講, User 對象的 password 屬性是一個字符串,格式以下:

hashtype$salt$hash

這是哈希類型,salt和哈希自己,用美圓符號($)分隔。

hashtype 是 sha1 (默認)或者 md5 ,它是用來處理單向密碼哈希的算法。 Salt是一個用來加密原始密碼以建立哈希的隨機字符串,例如:

sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4

User.set_password() 和 User.check_password() 函數在後臺處理和檢查這些值。

salt化得哈希值

一次 哈希 是一次單向的加密過程,你能容易地計算出一個給定值的哈希碼,可是幾乎不可能從一個哈希碼解出它的原值。

若是咱們以普通文本存儲密碼,任何能進入數據庫的人都能輕易的獲取每一個人的密碼。 使用哈希方式來存儲密碼相應的減小了數據庫泄露密碼的可能。

然而,攻擊者仍然可使用 暴力破解 使用上百萬個密碼與存儲的值對比來獲取數據庫密碼。 這須要花一些時間,可是智能電腦驚人的速度超出了你的想象。

更糟糕的是咱們能夠公開地獲得 rainbow tables (一種暴力密碼破解表)或預備有上百萬哈希密碼值的數據庫。 使用rainbow tables能夠在幾秒以內就能搞定最複雜的一個密碼。

在存儲的hash值的基礎上,加入 salt 值(一個隨機值),增長了密碼的強度,使得破解更加困難。 由於每一個密碼的salt值都不相同,這也限制了rainbow table的使用,使得攻擊者只能使用最原始的暴力破解方法。

加入salt值得hash並非絕對安全的存儲密碼的方法,然而倒是安全和方便之間很好的折衷。

處理註冊

咱們可使用這些底層工具來建立容許用戶註冊的視圖。 最近每一個開發人員都但願實現各自不一樣的註冊方法,因此Django把寫註冊視圖的工做留給了你。 幸運的是,這很容易。

做爲這個事情的最簡化處理, 咱們能夠提供一個小視圖, 提示一些必須的用戶信息並建立這些用戶。 Django爲此提供了可用的內置表單, 下面這個例子就使用了這個表單:

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 的模板。這個模板多是這樣的:

{% 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 }} 中:

{% 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 %} 語句中檢查權限:

{% 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)之後,爲用戶保存了一條消息。

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中。

{% if messages %}
<ul>
    {% for message in messages %}
    <li>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

須要注意的是 RequestContext 會在後臺調用 get_and_delete_messages ,所以即便你沒有顯示它們,它們也會被刪除掉。

最後注意,這個消息框架只能服務於在用戶數據庫中存在的用戶。 若是要向匿名用戶發送消息,請直接使用會話框架。

相關文章
相關標籤/搜索