目前你的 polls.urls
模組中,每個 url
都設定了 name
:
有沒有想過,如果在不同 App 的 urls
模組中,也有重複的 name
設定值該怎麼辦?實際上,你可以為每個 App 的 urls
模組設定不同的名稱空間(Namespace),來避免名稱衝突的問題發生,在接下來的練習 16 中,也要來看看如何建立一個簡易表單。
練習 16:建立 URL 名稱空間與簡易表單
在目前的 mysite/urls.py 檔案中,urlpatterns
前加上 app_name = 'polls'
:
from django.conf.urls import url
from . import views
app_name = 'polls'
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
接下來,你就可以在模版中使用這個名稱空間設定,例如,修改 polls/index.html 模版:
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
接著要來建立一個簡易表單了,修改 polls/detail.html,如下包括 HTML 的 <form>
標籤內容:
<h1>{{ question.question_text }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>
在 polls/views.py 中增加以下內容與修改 results
及 vote
,讓 results
可以根據請求的 question_id
與指定的模版檔案繪製畫面,而 results
用以取得 question_id
更新選項結果:
from django.template import loader
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from .models import Choice, Question
# .. 其他程式碼
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
其中 try..except..else
的部份,如果 try
區塊中沒有任何的錯誤發生,則會執行 else
區塊,而 reverse('polls:results', args=(question.id,))
會傳回像是 polls/3/results/
的字串,也就是當選項設定完成後,直接重新導向至問題的投票結果頁面。
當然,我們必須在樣版資料夾中建立 polls/results.html 模版檔案:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
接著你可以試著連結網站,在上頭作些投票,你應該可以看到以下結果:
簡介 CSRF
在練習 16 中看到了個 {% csrf_token %}
,這是什麼?CSRF 全名 Cross-Site Request Forgery,中文常翻為跨站請求攻擊或跨站偽造請求,這是利用 Web 應用程式在設計 HTTP 請求時,因為考量不周全造成的漏洞,從而進行攻擊的手法,通常是在 Web 應用程式站外的其他頁面中,包括惡意程式碼或鏈結,當使用者已通過驗證且會話(Session)未過期時,瀏覽該頁面或點選該惡意鏈結,就會造成攻擊成功的可能性。
一個 CSRF 攻擊的情境範例會像是 …
- Bob 登入了 www.webapp.com,並且會話尚未過期。
- Bob 瀏覽了另一個頁面,這個頁面中包括了惡意駭客置入的
<img src="http://www.webapp.com/project/1/destroy">
,然後 Bob 的某個專案就莫名奇妙被刪除了。 - Bob 瀏覽的頁面是不是跟 www.webapp.com 同一個網站並不重要,也許是在另一個論譠、Blog 或特意發給 Bob 的郵件中。
瀏覽器遇到 <img>
時,就會自動以 GET 請求 src
指定的網址,就這個情境來說,攻擊要能成立的前題,是 /project/1/destroy 這樣的請求就能刪除專案,這很顯然是 URL 設計時的不良,加上應用程式沒有在重大操作之前,進一步確認使用者身份與意圖而導致。
對於 HTTP 請求,有些人會有 GET 不安全,而 POST 比較安全的錯誤觀念,乍看這個例子好像是如此,實際上,也可以透過 POST 來發動類似的請求。例如:
<a href="http://www.harmless.com/" onclick="
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://www.example.com/account/destroy';
f.submit();
return false;">好康在這裡</a>
就算不點選,只要滑鼠略過圖也可以 …
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
就算沒有任何滑鼠操作,現在只要利用一些 JavaScript 寫些 Ajax 請求,都有可能讓這類攻擊發生 …
先前談過,CSRF 是利用 Web 應用程式在設計 HTTP 請求時,因為考量不周全造成的漏洞,因此,防範方式就是認真思考 HTTP 請求方法之使用。單就 <form>
的 method
允許設置的 GET 與 POST 來說,至少要想一下:
- GET 應用於等冪(Idempotent)操作,相同請求重複多次都必須有相同結果,就 GET 而言,語義上也是「取得」資訊,因此 GET 請求不建議用於改變應用程式狀態。
- POST 應用於非等冪操作,同樣請求重複多次,可能會產生不同結果,也就是會改變應用程式狀態。
實際上要考量的不只有等幂性,還有請求方法是否安全(Safe),進一步地,在 REST 架構設計下,還有更多的 HTTP 請求方法(像是 PUT、DELETE 等)考量,可參考〈重新認識HTTP請求方法〉。
如果 GET 確實地應用於等冪操作,對於非 GET 請求,通常會用個安全代碼,在 Django 中,這可由 {% csrf_token %}
來產生,當你瀏覽表單時,{% csrf_token %}
會產生隱藏欄位,當中包括了一組安全代碼,例如:
Django 應用程式接受請求時,必須同時在請求中找到這組安全代碼,從而確認請求是來自於同一站上的表單。
這個例子並不單只是如何防範 CSRF,主要想表達的是,安全其實是現代設計應用程式時應主動納入的考量,現在有不少框架也將安全納為特色之一,讓開發者不用煩惱安全防護實作時的枝微末節。