almost 4 years ago

  • 用戶
  • 登入
    • 確認使用者身分
    • 登入使用者並保持其登入
    • 根據用戶身分確認並規範使用者的操作功能
  • 登出
  • 使用內建的login/logout視圖
    • 內建login做的事
    • 內建login提供給呼叫模版的變量
    • 提供重導URL給內建login
      • 於settings.py中設定LOGIN_REDIRECT_URL
      • 透過POST方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值
      • 透過GET方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值
    • 對於內建的login視圖使用別的模版名稱

在上一節中學習了cookie和session這兩個重要的機制之後,我們可以來面對將web個人化的問題了。概略來說,我們希望我們的web應用是非匿名的,每個使用者都有一組帳號和密碼,用來核對他的身分,並且能夠將這個身分保持直到登出為止。這種身分的保持,可以使得整個web對該使用者呈現客製化的效果,包括資料、偏好以及功能的差異化。

用戶

一個用戶是一個web裡具有正式身分的使用者,我們為了能夠存取用戶的資訊,我們會想要建立model。但在Django中不用那麼麻煩,django.contrib套件中的authapp,便內建了user的模型。不論讀者想要自己刻或是使用現成的模型,都沒問題。不過我們先來看看要如何使用auth app中的User model。

首先我們一樣先打開settings.py:

mysite/mysite/settings.py
...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',  # 確認此行有加

    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restaurants',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',

    'django.contrib.auth.middleware.AuthenticationMiddleware',  # 確認此行有加

    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
...

authapp和AuthenticationMiddleware給安裝上去,如果這是第一次安裝的話,別忘了要同步資料庫喔。

$ python manage.py syncdb

有了auth,我們就等於有了一個內建的用戶權限系統,讀者們可以進入admin後端看看,我們會發現有使用者群組這兩個屬於auth應用的model,包含了使用者的名稱、姓氏、名字、電子郵箱和工作人員狀態等欄位。除非讀者們的web有特殊需求,不然這個模型已足夠使用。

登入

因為使用了內建的用戶系統,我們暫且先將註冊的部分放下,因為我們可以手動透過admin後端來新增使用者,我們先來看看要如何設計一個登入的機制。要知道,我們所期待的登入包含了下面三個步驟:

1. 藉由比對資料庫中的用戶資訊,確認使用者的身分(通常使用帳號及密碼)
2. 保持該使用者的登入狀態直到登出或預設時限
3. 根據用戶身分確認並規範使用者的操作功能

確認使用者身分

要確認使用者的身分,必須要求使用者給予一些資訊,而絕大多數的情況,我們需要帳號與密碼。首先,讓我們先來設計一個頁面用來輸入帳號密碼,當然,這需要表單以及對應的視圖函式:

mysite/templates/login.html
...
    <body>
        <form action="" method="post">
            <label for="username">用戶名稱:</label>
            <input type="text" name="username" value="" id="username"> <br />
            <label for="password">用戶密碼:</label>
            <input type="password" name="password" value="" id="password"> <br />
            <input type="submit" value="登入" />
        </form>
    </body>
...

因為登入是有關於整個web的操作,所以他應該隸屬於整個project而非單一一個app,所以我們選擇在mysite/templates下撰寫login.html這個登入頁面,並且在mysite/mysite/views.py中撰寫視圖函式。

登入的頁面非常簡單,包含了一個使用POST方法的form,會要求使用者輸入帳號密碼,然後會有一個submit button可以登入,為了簡化說明,我們這裡就簡單的略過表單的模型化與表單正確性的檢查,但讀者在實作自己的登入表單時,務必要確實做好。

mysite/mysite/views.py
...
from django.contrib import auth  # 別忘了import auth

...
def login(request):

    if request.user.is_authenticated(): 
        return HttpResponseRedirect('/index/')

    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:
        auth.login(request, user)
        return HttpResponseRedirect('/index/')
    else:
        return render_to_response('login.html') 

與之對應的視圖函式login包含幾個重要的部份,首先,我們可以發現HttpRequest物件中包含了一個user屬性,他代表了當前的使用者。

如果用戶已經登入,則HttpRequest.user是一個User物件,也就是具名用戶。
如果使用者尚未登入,HttpRequest.user是一個AnonymousUser物件,也就是匿名用戶。

下表列出User物件中的屬性:

屬性 說明
username 使用者的帳號,由字母、數字和底線組成
first_name 名字
last_name 姓氏
email 電子郵箱
password 加密(經編碼)過後的密碼
is_staff 真假值。若為True,該用戶可登入admin後端
is_active 真假值。若為True,該用戶可登入
is_superuser 真假值。若為True,該用戶擁有全權限
last_login 用戶上一次登入的日期與時間
date_joined 用戶被創建的日期與時間

下表則列出User物件中幾個重要的方法:

方法 說明
get_username( ) 取得用戶帳號
is_anonymous( ) 是否是匿名用戶,永遠回傳False; 若為AnonymousUser物件,則永遠回傳True
is_authenticated( ) 用戶是否認證過,永遠回傳True; 若為AnonymousUser物件,則永遠回傳False
get_full_name( ) 回傳完整的姓名
get_short_name( ) 只回傳名字
set_password(password) 設定密碼,會自動編碼加密,不包含User物件的儲存
check_password(password) 確認密碼,正確會回傳True,會自動編碼加密才比較

由上表可知,對AnonymousUser來說,is_authenticated方法會返回一個False值,而User會拿到True,所以is_authenticated方法是用來判定當下的使用者是否認證過(比對過身份)的重要函式。

順帶一提,is_anonymous和is_authenticated這兩個方法對於User和AnonymousUser來說算是一種多型概念的表現。

所以如果使用者已經認證過,我們將他重導回首頁(別擔心,我們等等會開始著手設計index.html)。
如果還是匿名用戶,我們試圖從request.POST中拿取表單中的帳密資訊(如果POST中沒有username或是password的話,我們填給他一個空值,讓他在接下來的檢查中產生失敗),並且使用auth中的authenticate方法來確認用戶。

登入使用者並保持其登入

auth.authenticate方法接受兩個引數,分別是usernamepassword,如果帳密資訊正確,authenticate方法會回給我們一個具名用戶的User物件,否則我們會拿到None

為了謹慎,我們還須檢查user.is_active,確認該用戶的帳戶沒有被凍結。只要兩道檢查任何一個有問題,我們便回應一個login頁面讓使用者重新輸入帳密,否則使用auth.login方法可以幫助我們真正登入使用者並保持他的登入狀態。

auth.login需要一個HttpRequest物件和一個User物件當做引數,他會利用Django的session(前一節介紹那麼久的session終於登場了!)將這個具名用戶保存在該session中,這也代表了該用戶的登入狀態得以跨頁面的保存直到session結束或登出。

這邊稍微來整理一下:

1. auth.athenticate負責進行用戶的認證
2. auth.login負責進行用戶登入狀態的保持

所以通常一個完整的login視圖函式都會用到上述兩個方法,可驗證用戶後保持其狀態。其實讀者可以發現,Django中所謂的登入,代表在當下的HttpRequest中的user屬性是一個具名的User物件。

根據用戶身分確認並規範使用者的操作功能

為了讓上述的功能被完整展示,我們便來製作一個首頁:

mysite/templates/index.html
<html>
    <head>
        <title>Restaurant King</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h2>歡迎來到餐廳王</h2>
        {% if request.user.is_authenticated %}
            <p>{{request.user}} 您已經登入囉</p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
    </body>
</html>

在這裡唯一值得一提的是,我們利用is_authenticated來判斷是否登入(根據前面的結論,所謂登入不過就是user屬性是User物件,所謂未登入,指的是user屬性是AnonymousUser物件),所以一個登入的用戶其is_authenticated方法的回傳值一定是True的。

is_authenticated指代表了該用戶是經過認證的(也就是具名的),他不會檢查該用戶的帳號是否被凍結。

透過判斷登入與否,提供給匿名與使用者不同的資訊與功能,透過html來規範操作功能是一個重要的手段。另一個可行的辦法是透過視圖函式,我們將在後面提到,在此之前,我們記得要幫index.html撰寫一個對應的view function:

mysite/mysite/views.py
...
def index(request):
    return render_to_response('index.html',locals())

別忘了設定一下我們的urls.py不然一切都是白搭。

mystie/mysite/urls.py
...
from views import login, index
...
urlpatterns = patterns('',
    ...
    url(r'^accounts/login/$',login),
    url(r'^index/$',index),
)

這裡讀者們可能會很好奇,為何需要使用/accounts/login/這個pattern,選擇/login/不是更簡單嗎?
這是因為/accounts/login/是Django默認的登入pattern(/accounts/logout/也是默認值),這個pattern對於某些Django的函式而言是參數的預設值,使用默認的pattern可以使得我們再使用到這些函式時減少一些負擔,不過若讀者沒有這些顧慮的話,使用任何想要的pattern都是可以的。
順帶一提,讀者可以透過settins.py來設定此一默認值,LOGIN_URL可以修改成任意想要的默認pattern。

登出

登出的手段很簡單,使用auth中的logout函數即可,我們先撰寫登出的view function:

mysite/mysite/views.py
...
def logout(request):
    auth.logout(request)
    return HttpResponseRedirect('/index/')

logout函式比起login就簡單多了,auth.logout方法會將用戶登出,然後跟login函式一樣,我們要記得在views.py中,從django.contrib中匯入auth

logout用在匿名用戶上也不會產生錯誤,這點要稍微注意一下。

接著我們需要調整一下index.html:

mysite/templates/index.html
...
        {% if request.user.is_authenticated %}
            <p>
                {{request.user}} 您已經登入囉~
                <a href="/accounts/logout/">登出</a>
            </p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
...

最後,加入新的pattern/action對應到urls.py中:

mysite/mysite/urls.py
...
from views import login, logout, index  # 記得匯入該function

...
urlpatterns = patterns('',
...
    url(r'^accounts/login/$',login),
    url(r'^accounts/logout/$',logout),  # 加入此對應

    url(r'^index/$',index),
)

現在讀者們可以利用早先在admin中建立的帳號來測試登入與登出的功能囉(這裡使用超級使用者或一般使用者都沒問題,但記得帳戶若被凍結則無法登入)。

使用內建的login/logout視圖

其實Django早在其內部便幫我們撰寫好了登入與登出的視圖函式,我們大可不必手動刻一個(但自己刻過一遍了解背後原理也是不錯啦,各位讀者不要打我),使用的方法如下,我們只要使用django.contrib.auth.views中的loginlogout函式即可:

mysite/mysite/urls.py
...
from views import index
from django.contrib.auth.views import login, logout  # 利用內建的view funciton

...
urlpatterns = patterns('',
...
    url(r'^accounts/login/$',login),
    url(r'^accounts/logout/$',logout),
    url(r'^index/$',index),
)

這兩個視圖函式預設使用的模版是registration/login.htmlregistration/logged_out.html,我們先試著將我們的mysite/templates/login.html複製一份到mysite/templates/registration/中(請讀者新增一個registration目錄到templates底下吧!)在試著登入一次。

天啊,又碰到CSRF的問題了,這是因為內建的login函式有強制使用到CSRF的功能,為了解決這個問題,我們在login.html的表單中加入{% csrf_token %}這個標籤,如下:

mysite/templates/registration/login.html
<form action="" method="post">
        {% csrf_token %}  # 加入此標籤
        <label for="username">用戶名稱:</label>
        <input type="text" name="username" value="" id="username"> <br />
        <label for="password">用戶密碼:</label>
        <input type="password" name="password" value="" id="password"> <br />
        <input type="submit" value="登入" />
    </form>

關於更多有關CSRF的議題跟詳細的討論,我們留待後面的章節,這裡要注意的是,雖然我們並沒有開啟csrf的功能(看看自己的settings.py吧),但是內建的login還是會啟動保護。

不過因為視圖函式和模版有密不可分的關係(模版的變量依賴視圖提供,而模版的表單內容也會提供給視圖),所以當我們使用了內建的視圖搭配自定的模版時,要小心中間可能造成的資訊代溝(所以建議讀者們可以去看auth.views.login的原始碼,這裡很好心地附在下面)。

auth.views.login
def login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          current_app=None, extra_context=None):
    """
    Displays the login form and handles the login action.
    """
    redirect_to = request.POST.get(redirect_field_name,
                                   request.GET.get(redirect_field_name, ''))

    if request.method == "POST":
        form = authentication_form(request, data=request.POST)
        if form.is_valid():

            # Ensure the user-originating redirection url is safe.

            if not is_safe_url(url=redirect_to, host=request.get_host()):
                redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)

            # Okay, security check complete. Log the user in.

            auth_login(request, form.get_user())

            return HttpResponseRedirect(redirect_to)
    else:
        form = authentication_form(request)

    current_site = get_current_site(request)

    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }
    if extra_context is not None:
        context.update(extra_context)
    return TemplateResponse(request, template_name, context, current_app=current_app)

我們僅針對auth.views.login做以下幾點重要的說明:

內建login做的事

如果使用者透過GET方法呼叫該函式,則login會呼叫模版回應一個帶有登入表單的頁面(當然還是得自己寫,並且要相容),而且該表單將會POST到相同的URL位置也就是說會呼叫login函式自己處理。

如果使用者透過POST方法呼叫該函式,則login會試著登入使用者,如果成功,會重導至next鍵值對應的URL,如果沒有next值,則會重導至settings中的LOGIN_REDIRECT_URL(當然沒設定的話他也有個預設值/accounts/profile/)。如果不成功,則會重新顯示登入表單。

內建login提供給呼叫模版的變量

login函式在最後會呼叫registration/login.html模版,而提供給模版的主要有下列變量(另外還有sitesite_name):

變量 說明
form AuthenticationForm的物件,用來做authenticate的確認的,有username和password兩個欄位
next 用在登入成功後重導的URL,可能包含查詢字串

其實next只是個預設的名稱,透過設定settings.py中的REDIRECT_FIELD_NAME可以設定成任意的字串。我們可以利用這些變量寫出一個更完整的login.html:

mysite/templates/registration/login.html
...
    {% if form.errors %}
        <p>Your username and password didn't match. Please try again.</p>
    {% endif %}

    <form method="post" action="{% url 'django.contrib.auth.views.login' %}">
        {% csrf_token %}
        <table>
            <tr>
                <td>{{ form.username.label_tag }}</td>
                <td>{{ form.username }}</td>
            </tr>
            <tr>
                <td>{{ form.password.label_tag }}</td>
                <td>{{ form.password }}</td>
            </tr>
        </table>

        <input type="submit" value="login" />
    </form>
...

提供重導URL給內建login

我們常常會碰到一個狀況,登入之後要將使用者帶至某個特定的頁面(也許是首頁,也許是登入前的頁面),為了讓內建的login能夠照我們的意思來重導(如果是我們自己寫的login當然沒問題,便如前面的範例,我們重導回/index/),我們必須提供重導URL給login,作法有下列幾種,我們以想要重導回/index/做示範:

於settings.py中設定LOGIN_REDIRECT_URL

這是最簡單直接的方法

mysite/mysite/settings.py
...
LOGIN_REDIRECT_URL = "/index/"

透過POST方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值

通常會使用hidden type的input元件

mysite/templates/registration/login.html
...
        <input type="submit" value="login" />
        <input type="hidden" name="next" value="{{ next }}" />  # 利用此行
    </form>
...

這裡有個點要注意, 我們利用名為next的隱藏元件來傳送重導URL,而這個URL卻是來自login函式提供的變量{{ next }}

透過GET方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值

透過URL pattern/accounts/login/?next=/index/中的查詢字串來提供next欄位。

不過說起來第二種方法還是依據第一種方式的設定,只能算是解決next值傳遞的方式而不算是設定一個next欄位的方法。
而我們對於預設的重導行為會偏向使用第一種方式進行設定,對於重導回登入前一個頁面的行為會偏向使用第三個方法。
另外,沒有特殊需求的話,建議各位讀者遵循使用next這個欄位名稱,可以在使用上避免一些需要調整的負擔,也能有較好的一致性。

對於內建的login視圖使用別的模版名稱

這必須要透過進階的視圖函式技巧,我們將留到後面的章節說明,不過如果先給出一個作法,我們可以對urls.py做出以下的更動:

mysite/mysite/urls.py
...
from views import index
from django.contrib.auth.views import login, logout

urlpatterns = patterns('',
        ...
    url(r'^accounts/login/$',login,{'template_name':'login.html'}),
    url(r'^accounts/logout/$',logout),
    url(r'^index/$',index),
)

如此我們便可以將使用的模版更換成任意的模版了。

提醒一下,這裡模版名稱的路徑必須在模版目錄底下,以前面章節的設定來說,此名稱必須位於mysite/templates下。

對於登出來說,我們也可以撰寫一個相應的模版,但有趣的是,如果我們沒有設定,Django預設會用admin的登出頁面作為登出頁面。

本節學習了如何實作login以及logout的功能,不過這顯然不夠,如果沒有對於登入的身分做出資訊與功能的差異化的話,登入與登出也就變得沒意義了,所以我們將在下一個筆記中對權限進行討論,同時也讓展現了如何實作用戶系統中的註冊功能。

← Django筆記(9) - Cookies 與 Sessions Django筆記(11) - 權限與註冊 →
 
comments powered by Disqus