iT邦幫忙

0

Django學習紀錄 14.權限與註冊

之前有在用Django寫一些小網站,現在暑假想說再來複習一下之前買的這本書
https://ithelp.ithome.com.tw/upload/images/20190724/20118889bj9fH1vhuR.jpg
於是我就把它寫成一系列的文章,也方便查語法
而且因為這本書大概是2014年出的,如今Django也已經出到2.多版
有些內容也變得不再支援或適用,而且語法或許也改變了
所以我會以最新版的Python和Django來修正這本書的內容跟程式碼

目錄:django系列文章-Django學習紀錄

14. 權限與註冊

14.1 匿名用戶vs.具名用戶

利用is_authenticated的判斷
分成三種方式:

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

第一種方法,就算沒有登入,利用URL:/restaurants_list/還是可以進入餐廳列表的頁面
第二種方法,在視圖函式中先用is_authenticated進行判斷,如果發現是匿名用戶則重導至其他頁面或是回應一個錯誤訊息的頁面
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())

或是回應一個錯誤訊息的模板

def list_restaurants(request):
    if not request.user.is_authenticated:
        return render_to_response('error.html')
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html', locals())

不過我們希望當判定用戶未登入時,重導至login頁面
登入成功後,再重導回來

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

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

...
from django .contrib.auth.decorators import login_required
...
@login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html', locals())
...

或是在urls.py裡裝飾視圖

...
from django .contrib.auth.decorators import login_required
...
urlpatterns = [
    ...
    path('restaurants_list/', login_required(list_restaurants)),
    ...
]
...

以上代碼和第二種方法是等效的,@login_required會檢查使用者是否登入,若已登入,正常執行修飾的視圖函式,反之會重導至/accounts/login/,並附上一個查詢字串,以該頁面的URL作為查詢字串中next欄位的值,@login_required也使用了默認的登入URL:/accounts/login/
使用@login_required固然減少了瑣碎的過程,也能做到完全的防範,但是對於使用者來說畢竟不直觀,意思是說,如果在使用者面前展現的多數超連結都是登入限定的,那根本讓人一頭霧水,我何必要點了之後才知道它不可用呢(目前指的是對匿名使用者而言不可用)?所以,真正好的設計,應該是結合方法1和方法3,直觀上讓匿名用戶不會誤入登入限定的頁面,也避免了有心人士想透過URL直接存取頁面
https://ithelp.ithome.com.tw/upload/images/20190722/201188890O9j4J6mkQ.png

14.2 註冊

我們來示範一下如何用程式碼進行註冊
進入django shell

manage.py shell

14.2.1 建立新用戶

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

14.2.2 更改用戶密碼

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

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

我們不但更改了密碼,還更改了使用者的狀態(is_staff屬性設為True可讓該使用者登入admin後台),最後別忘記使用save方法來儲存變更
關於密碼的加密,Django用的是加入隨機值的hash演算法,若有興趣,可自行深入了解

14.2.3 註冊用戶

使用auth應用內建的註冊表單模型UserCreationForm
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(request, 'register.html', locals())

UserCreationForm是繼承自forms.ModelForm的表單模型,ModelForm是一種特殊形式的表單,當我們發現表單欄位與某資料庫模型的欄位相同時(通常會發生在該表單的填寫就是為了產生某資料庫模型的物件),我們可以使用ModelForm避免一些不必要的手續
從ModelForm產生一個資料庫模型的物件並且將之存入資料庫的步驟:
1.填入欄位資料並創造表單物件
2.使用save方法以表單中的內容生成資料並存入資料庫
我們會發現UserCreationForm就是遵循這個模式,我們將request.POST這個類字典當做引數(當然裡面包含了該表單所需的各個欄位內容:帳號跟密碼)來生成一個表單物件form,在驗證了內容的合法性後,使用save方法生成一個User物件並存入資料庫,最後重導回/accounts/login/讓通過註冊的用戶可以立即登入網站
若是輸入不合法,則會回到註冊頁面,並且透過表單模型使得各欄位不需重填
而如果是第一次呼叫該視圖(沒有POST資料),則會產生一個空表單讓使用者輸入
資料庫模型要產生實例時,參數使用的是以各欄位名稱為名的關鍵字引數,表單模型產生實例時,使用的是字典型態的引數
mysite/templates/register.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>註冊</title>
    </head>
    <body>
        <h1>註冊新帳號</h1>
        <form action="" method="post">{% csrf_token %}
            {{ form.as_p }}
            <input type="submit" value="註冊">
        </form>
    </body>
</html>

urls.py

...
from restaurants.views import register
...
urlpatterns = [
    ...
    path('accounts/register/', register),
    ...
]
...

index.html

...
    {% else %}
                <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
            {% endif %}
            <p>若您還沒有帳號,請<a href="/accounts/register/">註冊</a></p>
...

https://ithelp.ithome.com.tw/upload/images/20190722/20118889jEddc5H8fZ.png
https://ithelp.ithome.com.tw/upload/images/20190722/20118889yk5EYxq3ZR.png

14.3 權限

14.3.1 建立權限

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

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

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

使用Meta Class來新增權限

models.py

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

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

manage.py makemigrations
manage.py migrate

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

首先進入django shell

manage.py shell
>>> from restaurants.models import Comment
>>> from django.contrib.auth.models 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方法,以一個要綁定的模型作為參數

14.3.2 權限的新增、移除與判定

我們示範一下如何用代碼操作
假設已經註冊了一個用戶叫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無法反應出即刻性的結果
這並不奇怪,因為Django對於增加或刪除權限的動作都會直接作用在資料庫上
被取出來的實例反而不受影響,所以我們有必要重新取得user物件
用戶權限的幾個常用方法:

方法 說明 範例
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()

14.3.3 Django自帶權限

如果使用了Django的auth應用,則Django會自動對專案中,已安裝應用底下所有的模型創建以下三種權限:
1.add權限
2.change權限
3.delete權限
這三種權限對於admin後台來說,分別會給使用者帶來以下操作上的限制:
擁有某資料庫模型add(新增)權限的用戶才能在admin中檢視該模型的新增表單或新增一個實體物件(資料)。
擁有某資料庫模型change(修改)權限的用戶才能在admin中檢視該模型的修改表單或修改一個實體物件(資料)。
擁有某資料庫模型delete(刪除)權限的用戶才能在admin中刪除一個實體物件(資料)。
要注意的是,這三個權限對於admin來說都只限制了用戶對於某種模型的操作,也就是說,我們沒有辦法指定一個用戶能否對其中一筆特定的資料有某種權限,只能指定某個用戶對該種資料有某種權限
舉例來說,我們允許用戶對於餐廳評論有新增的權限,但無法允許用戶只對第三筆餐廳評論有某種權限
還有一點要知道,除了在admin系統之外,以上這些自帶權限都只是一種標誌,他究竟規範了什麼還有賴使用者自行定義,簡單的來說,就是Django提供給我們一些標誌,至於看到標誌可以幹嘛或不能幹嘛,我們可以自己決定,當然,在admin中,Django已經幫我們決定了

14.3.4 使用權限

對於不同權限的用戶之間要如何做出差異化有以下手法:

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

我們先註冊兩個用戶,一個擁有can_comment權限,而另外一個沒有
如果只允許擁有can_comment權限的具名用戶進行評價
restaurants/templates/restaurants_list.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title> Restaurants</title>
        <style>
            table, th, td{
                border: 1px solid black;
            }
        </style>
    </head>
    <body>
        <h2>餐廳列表</h2>
        <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> <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>
    </body>
</html>

修改views.py,改用render,這樣才能將{{perms}}變量傳遞給模板

def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    print(request.user.user_permissions.all())
    return render(request, 'restaurants_list.html', locals())

變量perms的使用方法為:{{ perms.<app名稱>.<權限名稱> }}
接著示範第二種方法,以便能擋下用URL存取頁面的手段
views.py

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

使用重導或回傳錯誤訊息的模板皆可
我們也可以使用修飾符
views.py

...
from django.contrib.auth.decorators import login_required, user_passes_test
...
def user_can_comment(user):
    return user.is_authenticated and user.has_perm('restaurants.can_comment')

@user_passes_test(user_can_comment, login_url='/accounts/login/')
def comment(request,id):
...

這個修飾符需要兩個參數,一個是用來判斷權限通過與否的函式,另外一個關鍵字參數是一個login的URL,它將會在權限測試失敗時將使用者重導回登入頁面,當然,該URL也可以填寫其他頁面的URL,只是使用login頁面作為重導目標跟參數名稱比較一致
另外,該修飾符會很好心地附上一個next查詢字串在重導的URL後面,讓使用者在正確登入後重新導向原先的頁面
https://ithelp.ithome.com.tw/upload/images/20190722/20118889C9YaDmsSMV.png
由於這種登入檢查+某種權限檢查是一種常態,於是發展出另外一個更便捷的修飾符:@permission_required,用法:

...
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)和是否具備指定的權限

14.4 群組與群組權限

將使用者加入到不同的群組,並且統一賦予群組一些權限,可以讓管理上更方便
舉例來說:使用者a如果加入了x群組和y群組,他將會自動獲得這兩個群組所有的權限
進入Django shell

>>> from django.contrib.auth.models import User, Group, Permission
>>> user = User.objects.get(username='dokelung')
>>> p1 = Permission.objects.get(codename='add_comment')
>>> p2 = Permission.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(username='dokelung') # 重新獲取user
>>> user.has_perm('restaurants.add_comment')
True
>>> user.has_perm('restaurants.can_comment')
True

常用的群組方法:

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

不過通常為了方便,操作權限與群組都會使用admin
而不必打這麼多代碼

上一篇:Django學習紀錄 13.用戶的登入與登出

下一篇:Django學習紀錄 15.模板進階技巧


尚未有邦友留言

立即登入留言