over 3 years ago

  • Http協定的不足
    • 根本不知道使用者
    • 知道使用者但不會做資料比對
    • 知道使用者,也會比對,但使用者在每一次的請求中都要比對
  • Cookies - 好餅乾,不吃嗎?
    • 設置cookies
    • 讀取cookies
    • 餅乾的問題
  • Session
    • 安裝Session App
    • 使用Session
    • Session資料庫
    • Session cookie
    • Session的持續性
    • 用Session儲存模型物件

在現代的網頁應用中,個人化的web service受到群眾們的歡迎,臉書就是一個最好的例子,我們希望這個web app是認得使用者的,是能夠為使用者客製化的,本篇與下一篇的主題即是在介紹如何利用Django的機制做出個人化的網頁的基礎。

Http協定的不足

既然要認得使用者,那他首先得記住使用者,更精確的說,他必須要保持著某些資訊跟狀態。很可惜的是,單純的Http協定無法滿足我們的需求,因為他就只是非常制式地接收要求,回應要求。每一次的要求,每一次的回應都是獨立的,因為他無法記錄任何狀態,他並不能保持著上一次要求所帶來的任何有用資訊。

當然,透過GET與POST我們依然可以將資料透過新的request帶到下一個頁面,但是,這樣相當麻煩,沒有效率。

舉個例子,這就好像一家早餐店的老闆娘,他完全不在意客人是誰,只專注在客人點了什麼,然後埋首製作早餐。這種營業模式也不是說不行,但少了那麼一點人情味,通常,當老闆娘跟你說出:『先生,今天還是漢堡和大冰奶』的時候,你會有親切的感覺,而且極有可能成為忠實顧客。你看看,老闆娘記著客人的喜好或是從以往點餐的經驗中記住了點什麼,就能夠使得早餐店的經營生色不少,我們何不讓網頁的應用也能夠記住點什麼呢!

為了更清楚的說明,我們將Http協定的不足分為以下三點:

根本不知道使用者

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本不知道有使用者a
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本不知道有客人a這號人物

第一點是伺服器或是說web app根本不知道有這個人,他不但認不出客人,他根本不知道有這個客人,全天下的客人對他來說都一樣,客人就只是客人,只要我聽的懂他點餐,就好了。

這個問題的解決之道很簡單,使用資料庫便可以將客人a(使用者a)的資訊記錄下來了。

知道使用者但不會做資料比對

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本沒有確認發出請求的人
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本沒去看a,也沒想知道他是誰,雖然老闆娘知道有a這號人物

即使我們的伺服器記錄了用戶的資訊,但不做比對,也是認不出用戶的,這個時候登入或是辨識就成了解決問題的良藥。

知道使用者,也會比對,但使用者在每一次的請求中都要比對

伺服器:
使用者a發出請求1 ---> 回應1 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a 
                           |---- 伺服器並不知道兩次請求都是使用者a發出的
使用者a發出請求2 ---> 回應2 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a

早餐店:
客人a點了第一次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號
                           |---- 老闆娘並不知道兩次點餐是同一個客人,他還要客人提供那麼多資料
客人a點了第二次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號

其實只要客人願意每次都辛苦的提供比對的資訊,老闆娘是可以認得客人的,但是這並不是個好主意(至少對客人來說),就像我們不希望在同一個網站中每次要求一個新頁面時,都要重新登入一次吧,倒不如老闆娘給客人一塊餅乾說:『你拿這個餅乾給我看我就認得你了,身分證可以不用報出來』,於是在餅乾還在的一天,老闆娘都可以直接認得客人!

Cookies - 好餅乾,不吃嗎?

所以餅乾在哪呢?哈哈,餅乾就在這兒呢!大家耳熟能詳的cookie是伺服器儲存在瀏覽器的一小段訊息,每一次使用者透過瀏覽器向伺服器提出要求時,都會雙手奉上伺服器在稍早存在客戶端(瀏覽器)的cookie。透過這些cookies,伺服器便能掌握使用者的狀態。直到cookie失效那天。

cookie是儲存在瀏覽器中的小段訊息,它用來記住一些暫時資訊並且能讓使用者跨頁面使用。

一個cookie其實就只是一個鍵值對,包含了cookie的名稱和cookie的值,我們稍後也會看到,在Django中操作或設置cookies的手段便類似於使用字典。

cookies會伴隨著Http request和response往返客戶端與伺服端,有興趣的朋友可以去了解一下Http協定中cookies的傳送與設置,這邊我們會將精神放在,如何從request中取得cookies所存的資訊,以及如何在response中夾帶我們要設置在客戶端的cookies。

設置cookies

首先我們先來試試看把餅乾給瀏覽器吧,讀者們可以隨意的設定一個url pattern並且讓他對應到視圖函式set_c:

相信各位閱讀過前面的章節都有一定的實力了,這種示範用的代碼筆者就不逐一說明其安放的位置了。

def set_c(request):
    response = HttpResponse('Set your lucky_number as 8')
    response.set_cookie('lucky_number',8)
    return response

要設置cookies,我們要使用HttpResponse物件的set_cookie方法,第一個參數指定了cookie的名稱(鍵),第二個參數指定了cookie的值,set_cookie方法還有許多的參數可以調整,重要的包含設置cookie的有效時間,大家可以參考Django的Documents,我們在此不多做贅述。

讀取cookies

那要如何讀取一個cookie的值呢?這就簡單多了,如同存取POST和GET方法所提交的數據,cookie的取值一樣用字典取值的方法向HttpRequest物件的COOKIES屬性拿取,COOKIES中儲存了所有cookies的鍵值對資訊。如下範例:

def get_c(request):
    if 'lucky_number' in request.COOKIES:
        return HttpResponse('Your lucky_number is {0}'.format(request.COOKIES['lucky_number']))
    else:
        return HttpResponse('No cookies.') 

餅乾的問題

餅乾的確是解決問題的方法,但似乎也有他的問題存在,主要有下兩種問題:

  1. cookies是儲存在瀏覽器端的,而用戶可以關閉cookies的功能這會導致許多行為無效化
  2. Http協定是明文協定,cookies在傳輸過程中容易被攔截、竄改、偽造等,並不安全

為了解決cookies的弱點,我們需要一個更強大的機制:Session

Session

session之所以能夠解決cookies的問題,便在於他是把資訊存在伺服端,等等!那我們何不直接將資料存在資料庫中呢?這一樣能讓資料跨頁面的生存呀?使用資料庫記住這些資訊當然也是可行的辦法,但是資料庫的資料是永久性的,對於這些用完即丟(當然需要持續一段時間啦XD)的暫時資訊,我們還是不勞動資料庫的大駕,畢竟使用者不會想要去刻一個模型出來用吧。(我絕對不會說Django的session還是有用到資料庫的><)

那session是怎麼跨頁面生存的呢,主要有兩種方式:

第一種呢,session會透過cookie儲存一段用以辨識的ID,cookie可以跨頁面生存,session自然也可以囉,透過這個ID,指令搞便可以去伺服端將我們需要的資料取出來。這個方式跟純粹使用cookie來儲存資訊的方式不同,因為現在這個cookie只儲存session的ID(而且是加密過的),我們的資料不會直接暴露給瀏覽器端。

第二種呢,我們一樣可以透過url查詢字符的方式,將session ID附上,這對於cookie功能被瀏覽器端關閉時特別有用。

不過Django的各位使用者,我們暫且不需要擔心這個,我們先使用預設的第一種方式,並且讓Django幫我們處理這個繁雜的手續,我們只要會用就好了。

安裝Session App

要使用session,第一步便是安裝好session的app,打開settings.py

mysite/mysite/settings.py
...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',      # 確認有安裝

    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restaurants',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',    # 確認有安裝

    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',

    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
...

如果是第一次設定,請記得一定要同步一下資料庫,才能讓session啟用:

$ python manage.py syncdb

使用Session

當我們安裝好session app之後,HttpRequest物件中就會多了session這個屬性,使用session就跟使用字典一樣容易,讓我們利用視圖函式use_session來做個簡單示範:

def use_session(request):
    request.session['lucky_number'] = 8                               # 設置lucky_number

    if 'lucky_number' in request.session:
        lucky_number = request.session['lucky_number']                # 讀取lucky_number

        response = HttpResponse('Your lucky_number is '+lucky_number)
    del request.session['lucky_number']                               # 刪除lucky_number

    return response

基本上session的用法跟字典(映射型態)完全一樣(提醒大家request.session,session請用小寫),但請遵循下列規則:

  1. 使用字串作為session的鍵值
  2. 不要任意以底線作為session鍵值字串的開頭
  3. 不要對session及其屬性賦值

Session資料庫

前面說過,session還是有用到資料庫的(不然怎麼在後端儲存資料呢!),這也是我們在安裝了session app後還需要同步資料庫的原因,我們平常透過HttpRequestsession屬性來存取這個資料庫中的資料。但其實我們也可以利用操作模型的方式來達到同樣的效果,讓我們打開shell:

$ python manage.py shell

匯入Session模型之後,便可以操作資料庫了:

>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.all()[0]
>>> s.expire_date
datetime.datetime(2014, 8, 30, 3, 55, 42, 878739, tzinfo=<UTC>)

expire_date是該session的有效期限,當超過這個時間時,session即失效。

>>> s.session_data
u'MDZmZGI2ZjZlNTNjYjc2MTlmMDMxM2Y5NTRlNGYzZTg2M2Q0NWJhNjp7Il9hdXRoX3VzZXJfYmFja2VuZCI6ImRqYW5nby5jb250cmliLmF1dGguYmFja2VuZHMuTW9kZWxCYWNrZW5kIiwiX2F1dGhfdXNlcl9pZCI6MX0='
>>> s.get_decoded()
{u'_auth_user_backend': u'django.contrib.auth.backends.ModelBackend',
 u'_auth_user_id': 1}

session_data是經過編碼的,我們必須利用get_decoded()方法來取得編碼後的資料,而這會是一個字典:

Session cookie

我們在上面說過,session ID 是透過cookie來保存的,這個cookie就叫session cookie。我們可以透過sessionid這個cookie名稱來取得cookie,而他的值就是編碼後的session ID。我們透過撰寫一個測試的視圖函數session_test來做一個示範:

from django.contrib.sessions.models import Session

def session_test(request):
    sid = request.COOKIES['sessionid']
    s = Session.objects.get(pk=sid)
    s_info = 'Session ID:' + sid + '<br>Expire_date:' + str(s.expire_date) + 
             '<br>Data:' + str(s.get_decoded())
    return HttpResponse(s_info)

首先我們利用request.COOKIES來取得sessionid這個cookie的值,也就是session ID,接著我們利用模型的get方法去找到在資料庫中的sessions(pk這個屬性指的就是session id),最後我們把expire_date和session的內容都印出來。

我們可以看到他顯示出來的頁面會長的像下面這樣:

Session ID:vtnbu3bpi4eaw634b0wlhv333duzf8am
Expire_date:2014-10-06 03:57:47.741875+00:00
Data:{}

上述方法不是唯一能取得session ID的方法,畢竟session不一定要依附著cookie,我們可以直接讀取session的屬性:session_key來取得session ID:

def session_test(request):
    sid = request.COOKIES['sessionid']
    sid2 = request.session.session_key
    s = Session.objects.get(pk=sid)
    s_info = 'Session ID:' + sid + '<br>SessionID2:' + sid2 + '<br>Expire_date:' + 
              str(s.expire_date) + '<br>Data:' + str(s.get_decoded())
    return HttpResponse(s_info)

結果會發現,兩個方法拿到的session ID一樣。

Session ID:vtnbu3bpi4eaw634b0wlhv333duzf8am
SessionID2:vtnbu3bpi4eaw634b0wlhv333duzf8am
Expire_date:2014-10-06 03:57:47.741875+00:00
Data:{}

這種預設以cookie記錄session ID來達成跨頁面的方式雖然方便,但讀者們還是要確認使用者端的cookie功能是否被開啟,我們可以透過以下步驟來測試:

  1. 利用HttpRequest.session.set_test_cookie()設置測試cookie
  2. 利用HttpRequest.session.test_cookie_worked()來檢查cookie是否被允許使用
  3. 利用HttpRequest.session.delete_test_cookie()來刪除測試cookie

如果cookie未被開啟,那我們得利用url查詢的方式,去傳遞session ID(不過這會相當麻煩)。

Session的持續性

Session的持續性和有效性可以透過settings.py中的參數:SESSION_EXPIRE_AT_BROWSER_CLOSESESSION_COOKIE_AGE來設定。

參數 意義 預設值
SESSION_EXPIRE_AT_BROWSER_CLOSE 決定session是否在瀏覽器關閉時結束 False
SESSION_COOKIE_AGE session(cookie)的有效時間 1,209,600秒,兩週

如果SESSION_EXPIRE_AT_BROWSER_CLOSE被設為True,那當使用者將瀏覽器關閉時,該session自動結束。

用Session儲存模型物件

最後最後要來談到一個很重要的議題,我們常常會希望利用session暫時保存一個或多個模型物件(資料)來跨頁面使用,但是Django的預設狀態會出現錯誤,我們可以來試試看:

mysite/restaurants/views.py
...
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    request.session['restaurants'] = restaurants  # 試著利用session保存模型物件

    return render_to_response('restaurants_list.html',locals())
...

馬上便出現一個TypeError:

TypeError at /restaurants_list/
[<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>] is not JSON serializable

該怎麼辦呢?別擔心,打開settings.py將SERIALIZER改成PickleSerializer吧:

mysite/mysite/settings.py
...
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

本節介紹了cookie和session這兩個幫助資料進行跨頁面生存的兩大機制,有了他們做基礎,我們便不需要每次都要使用url查詢配合GET方法傳遞資料,也不需要每次用表單+POST方法來從使用者那裏獲取資訊,我們能記也能傳。下一節將會介紹如何使用這些機制來實作個人化的web,包含登入、登出與註冊。

← Django筆記(8) - 表單的驗證與模型化 Django筆記(10) - 用戶的登入與登出 →
 
comments powered by Disqus