Django -- 從平凡到超凡

第 14 章    存取限制

再談資訊安全

(1) 未登入者存取限制

▸ Django 提供一個 Python decorator (裝飾器) 稱為 @login_required,當置於某函式前面時,使用者必須登入才能執行該函式, 這樣的存取控制是無法破解的,以下加入有關登入的相關權限控制

▸ 登入的使用者才能留言,未登入的訪客不能

article/views.py
...
from django.db.models.query_utils import Q
from django.contrib.auth.decorators import login_required

from article.models import Article, Comment
from article.forms import ArticleForm

...

@login_required
def commentCreate(request, articleId):
    ...
✶ 匯入 login_required 模組
✶ 在 commentCreate() 函式上方加上 Python 裝飾器 @login_required,登入的使用者才能執行留言函式
✶ 測試:試試看登出後,在瀏覽器輸入 localhost:8000/article/commentCreate/<articleId>/articleId 必須是一個有效的 id
--> 不應該顯示閱讀文章頁面,而應該導向首頁

▸ 登入的使用者才能登出 (沒登入,哪來的登出? ;-)

account/views.py:
...
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required

from account.forms import UserForm, UserProfileForm


@login_required
def logout(request):
    ...
✶ 測試:在未登入的情況下,在瀏覽器輸入 localhost:8000/account/logout/
--> 不應該出現「歡迎再度光臨」訊息,而應該導向首頁

∗ 設定轉向登入頁面的 URL

▸ 如果未登入使用者嘗試送出沒有權限的 Request,則將使用者轉到登入頁面,要求使用者登入

settings.py:
...

STATIC_URL = '/static/'

LOGIN_URL = '/account/login/'

# For Heroku deployment
STATIC_ROOT = 'staticfiles'
✶ 設定 LOGIN_URL 為登入的 URL,login_required() 函式會將使用者轉至此網址

∗ 測試

▸ 未登入之使用者在瀏覽器的 URL 輸入 http://.../account/logout/ 會被轉到登入頁面,且瀏覽器的 URL欄位內容為 http://.../account/login/?next=/account/logout/

▸ 未登入之使用者在瀏覽器的 URL 輸入 http://.../article/commentCreate/<articleId>/ 會被轉至登入頁面,且瀏覽器的 URL欄位內容為 http://.../account/login/?next=/article/commentCreate/<articleId>/

▸ 以上 Django 除了轉到登入頁面外,還附加原本使用者要去的網址,也就是說,我們可以再加點功能讓使用者登入成功後, 可以自動轉到該網址

∗ 增加登入後自動轉址功能

▸ 在 login view 程式的 if request.method == 'GET': 中, 取出網址中的 next 變數值,並傳至 login.html 範本

account/views.py:
def login(request):
    ...
    template = 'account/login.html'
    if request.method == 'GET':
        return render(request, template, {'nextURL':request.GET.get('next')})
    # POST
    ...
✶ 因 next 名稱是保留字,故使用 nextURL 名稱

▸ 在 login.html 範本中,將 nextURL 變數值設為隱藏輸入欄位, 在表單送出時候即可將該值傳至 view 程式

account/tempaltes/account/login.html:
  ...
  <p>密碼:<input type="password" name="password"></p>
  {% if nextURL %}
    <input type="hidden" name="nextURL" value="{{ nextURL }}">
  {% endif %}
  <p><input type="submit" value="送出"></p>
  ...

▸ 在 login view 程式的 POST 方法中取出表單的 nextURL 欄位,如果取出成功,就轉到該網址 (不顯示登入成功訊息),否則轉到首頁並顯示登入成功訊息

account/views.py:
    ...
    auth_login(request, user)
    nextURL = request.POST.get('nextURL')
    if nextURL:
        return redirect(nextURL)
    messages.success(request, '登入成功')
    return redirect('main:main')
無狀態系統

瀏覽器與伺服器之間的通訊協定稱為 HTTP 協定,HTTP 是一種無狀態的協定 (Stateless protocol), 也就是說,伺服器不會保留瀏覽器請求裡的任何資訊,每次連結都像全新的連結一般,因此稱為無狀態。此外, 在雲端的環境中,從同一部瀏覽器發出的兩次請求也有可能分別由不同的伺服器來處理,請求的狀態更不可能保存。

保留伺服器狀態有下列幾種方法:

(2) 非管理者存取限制

∗ 某些 view 函式只有管理者才能執行

▸ 自行撰寫 admin_required 裝飾器,當置於某函式前面時, 使用者必須以管理者身份登入才能執行該函式,這樣的存取控制也是是無法破解的:

main/views.py:
from django.shortcuts import render, redirect
from django.contrib import messages
from django.urls.base import reverse


def about(request):
    ...


def admin_required(func):
    def auth(request, *args, **kwargs):
        if not request.user.is_superuser:
            messages.error(request, '請以管理者身份登入')
            return redirect(reverse('account:login') + '?next=' + request.get_full_path())
        return func(request, *args, **kwargs)
    return auth
✶ 如果使用者並非超級使用者,要求以管理者身份登入,並加上 next 參數,以便登入轉址
# 首先匯入 redirectmessages、與 reverse, 其中 reverse 的功能是將具名 URL 轉為實際 URL 格式
# redirect(...):轉址函式的的參數為「登入網址再串接登入成功後所需轉的網址」
✶ 裝飾器函式較難理解,在此不做深入解釋,有興趣的讀者請自行了解 Python 裝飾器的寫法

▸ 以下的 view 函式都需管理者才能執行,因此都需加上 @admin_required 裝飾器: articleCreate(), articleUpdate(), 及 articleDelete()

article/views.py:
...

from article.models import Article, Comment
from article.forms import ArticleForm
from main.views import admin_required

...

@admin_required
def articleCreate(request):
    ...

@admin_required
def articleUpdate(request, articleId):
    ...

@admin_required
def articleDelete(request, articleId):
    ...
✶ 測試:登出或以非管理者登入並輸入網址 http://localhost:8000/article/articleCreate/ 的結果:
loginAskAdmin

(3) 網頁的存取限制

▸ 「新增文章」及「刪除」按鈕,應該只有管理者才看得到

article/tempaltes/article/article.html:
...
</form>
{% if user.is_superuser %}
  <a class="btn inlineBlock" href="{% url 'article:articleCreate' %}">新增文章</a>
{% endif %}
<br><br><hr>
...
      <h3 class="inlineBlock"><a href="{% url 'article:articleRead' item.id %}">%{{ item.title %}}</a></h3>
      {% if user.is_superuser %}
        <form class="inlineBlock" method="post" action="{% url 'article:articleDelete' item.id %}">
          {% csrf_token %}
          <input class="btn deleteConfirm" type="submit" value="刪除">
        </form>
      {% endif %}
      <p>發表時間:%{{ item.pubDateTime|date:'Y-m-d H:i' %}}</p>
...

▸ 文章修改只有管理者才看得到

article/tempaltes/article/articleRead.html:
...
<h3 class="inlineBlock">%{{ article.title %}}</h3>
{% if user.is_superuser %}
  <a class="btn inlineBlock" href="{% url 'article:articleUpdate' article.id %}">修改</a>
{% endif %}
<p>發表時間:%{{ article.pubDateTime|date:'Y-m-d H:i' %}}</p>
...

▸ 測試:分別以訪客、非管理者、管理者身份執行各種系統功能

(4) 小結

∗ 上推專案到 Github

▸ Right click project --> Team --> Commit --> Commit message: Chapter 14 finished --> Commit and Push

∗ 練習

▸ 整份教材至此結束,好吧,本章就不練習了!好好休息一下、回顧一下、並且再次品味一下 Django, 衷心盼望您能從此份教材得到許多收穫,感謝。

上一章