2015年7月19日 星期日

Python Django:Dinbendon(3)

用戶

django.contrib套件中的auth app,有內建了user的模型。

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

...
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',  # 確認此行有加

    ...
)
...

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

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

登入與登出

自訂login & logged out

#.../Dinbendon/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')

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

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

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

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

使用django.contrib.auth.views中的login和logout函式

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

#.../Dinbendon/views.py
# -*- coding: utf-8 -*-
from django.shortcuts import render_to_response
from django.contrib import auth

def welcome(request):
    if 'user_name' in request.GET:
        return HttpResponse('Welcome!~'+request.GET['user_name'])
    else:
        return render_to_response('welcome.html',locals())

def index(request):
    return render_to_response('index.html',locals())
#.../Dinebndon/urls.py
...
from Dinbendon.views import welcome, 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),
)

為何需要使用/accounts/login/這個pattern,選擇/login/不是更簡單嗎?

這是因為/accounts/login/是Django默認的登入pattern(/accounts/logout/也是默認值),這個pattern對於某些Django的函式而言是參數的預設值,使用默認的pattern可以使得我們再使用到這些函式時減少一些負擔,不過若沒有這些顧慮的話,使用任何想要的pattern都是可以的。
可以透過settins.py來設定此一默認值,LOGIN_URL可以修改成任意想要的默認pattern。

這兩個視圖函式預設使用的模版是registration/login.html和registration/logged_out.html. 先新增一個registration目錄到templates底下.

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

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

其實next只是個預設的名稱,透過設定settings.py中的REDIRECT_FIELD_NAME可以設定成任意的字串。

#.../Dinbendon/templates/registration/login.html

<!DOCTYPE html>
<html>
<head>
    <title>index</title>
</head>
<body>
        {% 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>
    </body>
</html>

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

提供重導URL給內建login

登入之後要將使用者帶至某個特定的頁面(也許是首頁,也許是登入前的頁面),為了讓內建的login能夠照我們的意思來重導,我們必須提供重導URL給login.
法一:
於settings.py中設定LOGIN_REDIRECT_URL

#.../Dinbendon/settings.py
...
LOGIN_REDIRECT_URL = "/index/"

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

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

使用hidden type的input元件,有個點要注意, 我們利用名為next的隱藏元件來傳送重導URL,而這個URL卻是來自login函式提供的變量{{ next }}
法三:
透過GET方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值.
透過URL pattern/accounts/login/?next=/index/中的查詢字串來提供next欄位。

權限與註冊

匿名用戶 vs. 具名用戶,關鍵就在於對HttpRequest物件的user屬性進行is_authenticated的判斷。透過is_authenticated方法,我們能很容易地判定當下的使用者是匿名或是具名,詳細的作法有三.

enter image description here
第一種方法雖然我們的確可以在模版中透過判定用戶的登入決定要顯示哪些資訊或連結,但我們無法阻止使用者透過GET方法直接存取某些頁面。舉例來說,用戶未登入前無法由首頁獲得餐廳列表的超連結,但是如果我們利用URLpattern/restaurants_list/還是可以進入餐廳列表的頁面。

第二種方法,是在視圖函式中先用is_authenticated進行判斷,如果發現是匿名用戶,則馬上將其重導到其他頁面,或是回應以一個錯誤訊息的頁面。

#.../restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/?next={0}'.format(request.path))
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...
return HttpResponseRedirect('/accounts/login/?next={0}'.format(request.path))
#也可以寫成以下的方式,但為了不把程式寫死,還是上面這樣的寫法比較妥當.
return HttpResponseRedirect('/index/')
# or
return render_to_response('error.html')  # 一樣要記得此模版要放置在正確的templates路徑下

這裡用到前面講到的利用GET方法傳遞next欄位的技術,讓使用者成功登入後可以重導至next欄位的URL,這裡我們希望重導到的頁面就是餐廳列表的url pattern:/restaurants_list/,我們發揮不寫死的精神,使用request.path來提供該url pattern。

Django提供我們一種快捷的作法,那就是法三:使用login_required修飾符。

#.../restaurants/views.py
...
from django.contrib.auth.decorators import login_required  # 記得import進來!
from django.http import HttpResponse, HttpResponseRedirect
...
@login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

@login_required會檢查使用者是否登入,若已登入,正常執行修飾的視圖函式,反之會重導至/accounts/login/,並附上一個查詢字串,以該頁面的URL作為查詢字串中next欄位的值。

@login_required也使用了默認的登入URL pattern “/accounts/login/”

真正好的設計,應該是結合法1和法3,直觀上讓匿名用戶不會誤入登入限定的頁面,也避免了有心人士想透過URL直接存取頁面。

如何使用代碼來進行註冊而非使用admin。

註冊用戶:
需要一個處理註冊的視圖函式和一個註冊用的頁面,當然還有最重要的,用來註冊的表單。這邊用Django中auth應用中內建的註冊表單模型UserCreationForm.

#.../Dinbendon/views.py
...
from django.contrib.auth.forms import UserCreationForm
...
def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            return HttpResponseRedirect('/accounts/login/')
    else:
        form = UserCreationForm()
    return render_to_response('register.html',locals())

UserCreationForm是繼承自forms.ModelForm的表單模型,ModelForm是一種特殊形式的表單,當我們發現表單欄位與某資料庫模型的欄位相同時(通常會發生在該表單的填寫就是為了產生某資料庫模型的物件),我們可以使用ModelForm避免一些不必要的手續,此處我們並不打算深入探究自定義ModelForm的寫法,我們只要了解如何從ModelForm產生一個資料庫模型的物件並且將之存入資料庫。

UserCreationForm主要有兩個步驟:

  1. 填入欄位資料並創造表單物件
  2. 使用save方法以表單中的內容生成資料並存入資料庫

我們將request.POST這個類字典當做引數(當然裡面包含了該表單所需的各個欄位內容:帳號跟密碼)來生成一個表單物件form,在驗證了內容的合法性後,使用save方法生成一個User物件並存入database,最後重導回/accounts/login/讓通過註冊的用戶可以立即登入網站。

接著是註冊頁面的撰寫,我們選擇了form的as_p方法來生成表單

#../Dinbendon/templates/register.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
 <body>
        <h1>註冊新帳號</h1>

        <form action="" method="post">
            {{ form.as_p }}
            <input type="submit" value="註冊">
        </form>
    </body>
</html>
#.../Dinbendon/urls.py
...
from Dinbendon.views import welcome, index, register
...
urlpatterns = patterns('',
...
    url(r'^accounts/register/$',register),
)

最後透過index.html來增加註冊這個超連結選項

#.../Dinbendon/templates/index.html
...
    <body>
        <h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
        {% if request.user.is_authenticated %}
...

權限

權限(Permission)也是一種Django內建的模型,主要包含了三個欄位如下.

name: 權限名稱
codename: 實際運用在代碼中的權限名稱
content_type: 來自一個模型的content_type

codename是實際運用在判定權限代碼中的名字,有點類似一個用戶的帳號,而name就好像是用戶名稱,通常只是拿來顯示,好閱讀的。每一個權限都會跟一個資料庫模型綁定,都會屬於一種資料庫模型,這也是需要content_type的原因。

新增權限有兩種方式,一種透過各資料庫模型中的Meta Class來設定,另外一種可以透過操作Permission模型來建立新物件.

使用Meta Class來新增權限

#.../restaurants/models.py
...
class Comment(models.Model):
    ...
    class Meta:
        ordering = ['date_time']
        permissions = (
            ("can_comment", "Can comment"),  # 只有一個權限時,千萬不要忘了逗號!
        )

permissions變數是一個元組,我們可以為該模型增加一至數種權限,而該組的每個元素又是一個有兩元素的元組,第一個元素是codename字串,第二個元素是name字串,至於不需要content_type的原因很簡單,我們在模型下直接定義了權限,content_type會由Django自動地默默幫我們取得。

權限的新增、移除與判定

對某用戶新增與移除權限最快的方式就是利用admin後台,如果是在python shell中,可以用User.user_permissions.add和User.user_permisssions.remove來為某個使用者新增或刪除一個權限,也可以用User.has_perm來查看使用者是否具備某種權限,但有一點很弔詭,經過測試之後發現,若沒有向管理器重新取得User物件的話,has_perm無法反應即刻性的結果。

使用權限

enter image description here

但為了方便,通常我們會利用admin後台來管理群組及其權限。
Ex.

#mysite/restaurants/
...
        <table>
            <tr>
                <th>選取</th>
                <th>店名</th>
                <th>電話</th>
                <th>地址</th>
                {% if perms.restaurants.can_comment %}
                    <th>評價</th>
                {% endif %}
            </tr>
            {% for r in restaurants %}
                <tr>
                    <!-- <td> <input type="radio" name="id" value="{{r.id}}"> </td> -->
                    <td> <a href="/menu/{{r.id}}/"> menu </a> </td>
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                    {% if perms.restaurants.can_comment %}
                        <td> <a href="/comment/{{r.id}}/"> comment </a> </td>
                    {% endif %}
                </tr>
            {% endfor %}
        </table>
...

這裡使用的是{{perms}}這個變量,我們知道一個變量之所以可以用來填寫模板,必須要含在context中並傳給模板。我們偷懶通常會用locals函數。那在這裡一樣,{{perms}}不可能會憑空出世,我們得要將當下使用者的各種權限含在context中並且傳給模板才能使用。但這顯然花功夫,Django其實已經提供了這種處理的機制,只要我們使用包含了HttpRequest物件的context就可以,而這樣的context,在Django中是一個內建的context子類別: RequestContext

我們直接來看看如何使用RequestContext:

#mysite/restaurants/views.py
...
from django.template import RequestContext
...
@login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    print request.user.user_permissions.all()
    return render_to_response('restaurants_list.html',
                               locals(),
                               context_instance=RequestContext(request))
...

只有兩點,第一個是記得從django.template中匯入RequestContext,另一個是多給render_to_response一個可選的引數context_instance,並將其設為RequestContext(request)。如此一來,restaurants_list.html模板便具備了{{perms}}變量了。

我們回到剛剛的html,利用perms.restaurants.can_comment可以得知當下的用戶是否具備了restaurants這個app中的can_comment權限,一個標準的{{perms}}變量使用法為:

{{ perms.<app名稱>.<權限名稱> }}

讀者們如果已經暈頭轉向了,沒關係,只要把握住基本的要點就可以了,畢竟我們還沒有正式談到RequestContext,我們將在之後的筆記中詳細地討論他,讀者最後會發現使用RequestContext的模板不但具有perms變量,還有user等其他重要的變量,更重要的是,這是我們解決CSRF的重要手段。

然而,跟檢查用戶是否登入一樣,html的限制無法擋住使用URL對頁面直接存取,解決之道便是使用視圖函式來做一個檢查跟限制:

#mysite/restaurants/views.py
...
def comment(request,id):
    if request.user.is_authenticated and request.user.has_perm('restaurants.can_comment'):
        ...
    else:
        return HttpReponseRedirect('/restaurants_list/')

同樣地,使用重導或顯示錯誤都可以。

當然我們也可以使用方便的修飾符,首先我們將剛剛的檢查式寫成一個function:

 def user_can_comment(user):
    return user.is_authenticated and user.has_perm('restaurants.can_comment')

接著我們可以使用@user_passes_test:

#mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required, user_passes_test
...
@user_passes_test(user_can_comment, login_url='/accounts/login/')
def comment(request,id):
...

這個修飾符需要兩個參數,一個是用來判斷權限通過與否的函式,另外一個關鍵字參數是一個login的url,他將會在權限測試失敗時將使用者重導回登入頁面,當然,該URL也可以填寫其他頁面的URL,只是使用login頁面作為重導目標跟參數名稱比較一致,另外,該修飾符會很好心地附上一個next查詢字串在重導的URL後面,期待能將使用者在正確登入後重新導向原先的頁面。

但是由於這種登入檢查+某種權限檢查是一種常態,於是發展出另外一個更便捷的修飾符:@permission_required,用法如下:

mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required, permission_required
...
@permission_required('restaurants.can_comment', login_url='/accounts/login/')
def comment(request,id):
...

這個修飾符一樣需要兩個參數,只是他的第一個參數直接給定權限的名稱,也不需要寫一個判定函式,因為他預設會檢查用戶是否登入(is_authenticated)和是否具備指定的權限。其他的部份跟@user_passes_test一模一樣。

當然別忘了,這兩個修飾符都需要匯入。

參考來源:
http://dokelung-blog.logdown.com/posts/234896-django-notes-11-permission-and-registration

沒有留言:

張貼留言