about 4 years ago

  • 匿名用戶 vs. 具名用戶
  • 註冊
    • 建立新用戶
    • 更改用戶密碼
    • 註冊用戶
  • 權限
    • 建立權限
      • 使用Meta Class來新增權限
      • 操作Permission模型來建立權限物件
    • 權限的新增、移除與判定
    • Django自帶權限
    • 使用權限
  • 群組與群組權限

我們之所以要打造一個個人化的web應用,就是為了對於不同的用戶提供不同的資訊和功能,這裡所謂"不同的用戶"可能指:

  1. 匿名用戶與具名用戶
  2. 不同的具名用戶
  3. 不同群的具名用戶

而關於如何提供這些不同用戶之間的差異化或客製化,就得靠一些判定身分的作法或是權限,我們將在這裡探討這些議題。

匿名用戶 vs. 具名用戶

我們首先討論要如何對匿名用戶與具名用戶做出差異化,我們通常會想要限制匿名用戶對於某些功能或頁面的存取。
關鍵就在於對HttpRequest物件的user屬性進行is_authenticated的判斷。
透過is_authenticated方法,我們能很容易地判定當下的使用者是匿名或是具名,詳細的作法有三,如下表

方法說明 優點 缺點
透過html來規範操作功能 對使用者來說直觀易理解 無法擋下用URL存取頁面的手段
於視圖函式中對於匿名用戶進行限制,使用重導或顯示錯誤 可以完全擋下匿名使用者的存取 太繁瑣,對使用者也不直觀
於視圖函式中對於匿名用戶進行限制,使用修飾符login_required 可以完全擋下匿名使用者的存取 對使用者不直觀

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

第二種方法,是在視圖函式中先用is_authenticated進行判斷,如果發現是匿名用戶,則馬上將其重導到其他頁面,或是回應以一個錯誤訊息的頁面。我們利用限制匿名用戶存取餐廳列表頁面做範例:

mysite/restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/index/')
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

上述代碼會讓匿名用戶被強制重導回首頁。我們也可以製作一個顯示錯誤訊息的頁面來提醒使用者,為了簡化,我們就不附上該頁面的html代碼了:

mysite/restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return render_to_response('error.html')  # 一樣要記得此模版要放置在正確的templates路徑下

    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

但我們還不滿足,我們希望當判定用戶未登入時,將其重導至login頁面,並且在登入成功後,再將其重導回來(這才是對使用者負責的設計),我們可以這樣做:

mysite/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())
...

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

第二種方法雖然解決了第一種方法的缺陷,但顯得太繁瑣了,我們需要對每個需要判定登入的視圖撰寫減查碼和重導(起碼花兩行),其實Django提供我們一種快捷的作法,那就是法三:使用login_required修飾符。

mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required  # 記得import進來!

...
login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

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

讀者在這裡也可以發現,@login_required也使用了默認的登入URL pattern "/accounts/login/"

使用@login_required固然減少了瑣碎的過程也能做到完全的防範,但是對於使用者來說畢竟不直觀,意思是說,如果在使用者面前展現的多數超連結都是登入限定的,那根本讓人一頭霧水,我何必要點了之後才知道他不可用呢(目前指的是對匿名使用者而言不可用)?所以,真正好的設計,應該是結合法1和法3,直觀上讓匿名用戶不會誤入登入限定的頁面,也避免了有心人士想透過URL直接存取頁面。

註冊

接著,在我們往下討論不同具名用戶之間的權限問題前,我們先來多新增幾個用戶(不然也沒得示範),當然我們要教大家的是如何使用代碼來進行註冊而非使用admin。其實這並不是什麼新鮮事,新增一個用戶不過是新增一個用戶模型的實例,還記得模型與資料庫的章節嗎?如果忘記了也沒關係,我們走一遍流程,大家順便複習一下:

首先先進入django shell

$ python manage.py shell

建立新用戶

接著我們必須要匯入User模型,並且利用模型管理器的create來產生一個模型物件並存入資料庫:

>>> from django.contrib.auth.models import User
>>> new_user = User.objects.create(username="peter",password="test123")
>>> new_user
<User: peter>

更改用戶密碼

如果想要更改用戶的密碼,可以用user物件裡的set_password方法,之所以不直接去修改password屬性,是因為密碼是經過加密的,我們通常無法自己處理這方面的手續。以下是簡單的修改範例:

>>> new_user.set_password("hello123")
>>> new_user.is_staff = True
>>> new_user.save()

我們在這不但更改了密碼,更改了使用者的狀態(is_staff屬性設為True可讓該使用者登入admin後台),最後別忘記使用save方法來儲存變更。現在,讀者們可以透過新的使用者登入admin後台來測試看看。

關於密碼的加密,Django用的是加入隨機值的hash演算法,讀者若有興趣,可自行找資料深入了解。

註冊用戶

了解了如何建立用戶之後,我們可以來研究如何實現註冊的功能,這其實相當簡單,我們需要一個處理註冊的視圖函式和一個註冊用的頁面,當然還有最重要的,用來註冊的表單。表單的實現方式有很多種,我們可以使用最原始土法煉鋼的方式,在註冊用的模版上面手動加入表單及其元件,最後利用POST方法就可以在視圖函式裡得到使用者資訊,並利用剛剛才講過的方法創建新的使用者。我們也可以將表單給模型化,以提供更優質的驗證。

但我們在這裡要用更偷懶的方式,那當然就是Django中auth應用中內建的註冊表單模型UserCreationForm:

mysite/mysite/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產生一個資料庫模型的物件並且將之存入資料庫。

其實這件事情也容易得很,一共只有兩個步驟:

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

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

而如果是第一次呼叫該視圖(沒有POST資料),則會產生一個空表單讓使用者輸入。

小小提醒,資料庫模型要產生實例時,參數使用的是以各欄位名稱為名的關鍵字引數,表單模型產生實例時,使用的是字典型態的引數。

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

mysite/templates/register.html
...
    <body>
        <h1>註冊新帳號</h1>
    
        <form action="" method="post">
            {{ form.as_p }}
            <input type="submit" value="註冊">
        </form>
    </body>
...

當然別忘了urls.py的設定:

mysite/mysite/urls.py
...
from views import here, math, new_math, welcome, index, register
...
urlpatterns = patterns('',
...
    url(r'^accounts/register/$',register),
)

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

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

權限

接著我們來介紹權限,有的時候,我們的web應用只需要區分用戶是具名的還是匿名的,但有的時候,不同的具名用戶之間,也有在資訊獲得或是功能選項上的差異,此時便要透過權限來規範各用戶間的使用情形。

其實,權限只是一個標誌,被賦予了某種權限的用戶並非自動可以使用某功能,而必須透過開發者的對權限的判定,才來決定要怎麼做,這一點跟要使用authenticate方法還滿像的。

建立權限

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

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

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

而要新增權限有兩種方式,一種透過各資料庫模型中的Meta Class來設定,另外一種可以透過操作Permission模型來建立新物件,我們將在下面示範這兩種作法:

使用Meta Class來新增權限

我們來對餐廳的評論新增一個權限,我們打開mysite/restaurants/models.py:

mysite/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自動地默默幫我們取得。

記得,由於權限也是模型,也有資料庫,所以我們必須要同步資料庫:

$ python manage.py syncdb

操作Permission模型來建立權限物件

首先進入shell:

$ python manage.py shell

接著我們匯入Comment模型和Permission模型:

>>> from restaurants.models import Comment
>>> from django.contrib.auth import Permission
>>> from djnago.contrib.contenttypes.models import ContentType
>>> content_type = ContentType.objects.get_for_model(Comment)
>>> permission = Permission.objects.create(
                     codename='can_comment',
                     name='Can comment',
                     content_type=content_type
                 )

為了取得content_type,我們使用了get_for_model方法,這個方法需要一個要綁定的模型作為引數。

用上述兩種方法新增了權限之後,讀者們可以登入admin後台,從使用者那邊,我們將可以在可選權限欄內看到新增的權限。

權限的新增、移除與判定

要對某用戶新增與移除權限最快的方式就是利用admin後台,但是我們依然要說明一下如何利用代碼進行(假設讀者已經註冊了一個用戶叫dokelung):

>>> from django.contrib.auth.models import User, Permission
>>> user = User.objects.get(username='dokelung')
>>> perm = Permission.objects.get(codename='can_comment')
>>> user.has_perm('restaurants.can_comment')
False
>>> user.user_permissions.add(perm)
>>> user.has_perm('restaurants.can_comment')
False
>>> user = User.objects.get(username='dokelung')
>>> user.has_perm('restaurants.can_comment')
True
>>> user.user_permissions.remove(perm)
>>> user = User.objects.get(username='dokelung')
>>> user.has_perm('restaurants.can_comment')
False

我們發現可以用User.user_permissions.addUser.user_permisssions.remove來為某個使用者新增或刪除一個權限,也可以用User.has_perm來查看使用者是否具備某種權限,但有一點很弔詭,經過測試之後發現,若沒有向管理器重新取得User物件的話,has_perm無法反應即刻性的結果。

下表列出了用戶權限的幾個重要方法:

方法 說明 範例
add 讓用戶新增權限,參數是一或多個Permission物件 user.user_permissions.add(perm)
remove 讓用戶刪除權限,參數是一或多個Permission物件 user.user_permissions.remove(perm)
has_perm 確認某用戶是否具備某種權限,參數是一個字串,形式為<模型名稱>.<權限的codename> user.has_perm('restaurants.can_comment')
clear 清除某用戶的所有權限 user.user_permissions.clear()

Django自帶權限

如果使用了Django的authapp,則Django會自動對所有專案中被安裝的APP底下所有的模型創建以下三種權限:

1. add權限
2. change權限
3. delete權限

這三種權限對於admin後台來說,分別會給使用者帶來以下操作上的限制:

  1. 擁有某資料庫模型add(新增)權限的用戶才能在admin中檢視該模型的新增表單或新增一個實體物件(資料)。
  2. 擁有某資料庫模型change(修改)權限的用戶才能在admin中檢視該模型的修改表單或修改一個實體物件(資料)。
  3. 擁有某資料庫模型delete(刪除)權限的用戶才能在admin中刪除一個實體物件(資料)。

要注意的是,這三個權限對於admin來說都只限制了用戶對於某種模型的操作,也就是說,我們沒有辦法指定一個用戶能否對其中一筆特定的資料有某種權限,只能指定某個用戶對該種資料有某種權限。

舉例來說,我們允許用戶對於餐廳評論有新增的權限,但無法允許用戶只對第三筆餐廳評論有某種權限。

還有一點要知道,除了在admin系統之外,以上這些自帶權限都只是一種標誌,他究竟規範了什麼還有賴使用者自行定義,簡單的來說,就是Django提供給我們一些標誌,至於看到標誌可以幹嘛或不能幹嘛,我們可以自己決定,當然,在admin中,Django已經幫我們決定了。

使用權限

使用權限的道理跟判定用戶是否具名一樣,要靠我們自己的判斷,而對於不同的權限到底對於web app的操作有哪些差異,也需要靠我們自己來決定跟撰寫,關於不同權限的用戶之間要如何做出差異化,有一下手法(類似如何對匿名用戶與具名用戶做出差異化):

方法說明 優點 缺點
透過html來規範操作功能 對使用者來說直觀易理解 無法擋下用URL存取頁面的手段
於視圖函式中利用權限對用戶進行限制,使用重導或顯示錯誤 可以完全擋下不具權限使用者的存取 太繁瑣,對使用者也不直觀
於視圖函式中對於匿名用戶進行限制,使用修飾符user_passes_testpermission_required 可以完全擋下匿名使用者的存取 對使用者不直觀

我們一樣對這三種方法,各進行一次示範,但在開始之前,請讀者起碼先註冊兩個用戶,一個擁有can_comment權限,而另外一個沒有。

首先我們可以在html頁面上直接判定權限,假設我們只允許擁有can_comment權限的具名用戶進行評價的話:

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一模一樣。

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

還有一個議題是如何檢查使用者呼叫通用視圖(這是啥,可以吃嗎?)的權限,由於還沒介紹通用視圖,我們也留到以後再說。

群組與群組權限

本篇最後一個議題是有關於群組的,我們可以將使用者加入到不同的群組,並且統一賦予群組一些權限,讓管理上更方便。
舉例來說:使用者a如果加入了x群組和y群組,他將會自動獲得這兩個群組所有的權限。

我們用以下的代碼做示範,請大家先進入django shell:

>>> from django.contrib.auth.models import User, Group, Permission
>>> user = User.objects.get(name='dokelung')
>>> p1 = Permissions.objects.get(codename='add_comment')
>>> p2 = Permissions.objects.get(codename='can_comment')
>>> g1 = Group.objects.create(name='group1')              # 新增一個新群組group1

>>> g2 = Group.objects.create(name='group2')              # 新增一個新群組group2

>>> g1.permissions.add(p1)                                # 為group1群組增加add_comment權限

>>> g2.permissions.add(p2)                                # 為group2群組增加can_comment權限

>>> user.groups.add(g1,g2)                                # 將dokelung加入group1群組與group2群組

>>> user = User.objects.get(name='dokelung')              # 重新獲取user

>>> user.has_perm('restaurants.add_comment')
True
>>> user.has_perm('restaurants.can_comment')
True

我們會發現,群組的權限屬性叫做permissions而非group_permissions,千萬別搞錯。
而一樣要注意的是,user必須重新由管理器獲取才能正確顯示權限。

群組一些常用的群組方法如下表:

方法 說明 範例
add 讓用戶加入群組,參數是一或多個群組 user.groups.add(group)
remove 讓用戶離開群組,參數是一或多個群組 user.groups.remove(group)
clear 讓用戶離開所有群組 user.groups.clear()

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

本篇花了大量的篇幅詳細介紹了關於用戶系統的註冊和權限問題,再加上上一篇講到的用戶登出入的機制,便可以打造個人化的web應用了。現在,我們對於Django的各項基本要素和功能都有了個初步的了解,接下來我們要逐步的強化我們的專案,並且將整個MTV架構的進階議題帶給大家。讓我們接著往前進吧!

← Django筆記(10) - 用戶的登入與登出 Django筆記 - 目錄 →
 
comments powered by Disqus