如何使用會話
Django 提供對匿名會話的完全支援。其會話架構讓你根據各個站點的通路者存儲和通路任意資料。它在伺服器端存儲資料并抽象Cookie 的發送和接收。Cookie 包含會話的ID —— 不是資料本身(除非你使用基于Cookie 的後端)。
啟用會話
會話是通過一個中間件實作的。
為了啟用會話功能,需要這樣做:
編輯
MIDDLEWARE_CLASSES
設定并確定它包含’
django.contrib.sessions.middleware.SessionMiddleware
‘。
django-admin startproject
建立的預設的
settings.py
已經啟用
SessionMiddleware
。
如果你不想使用會話,你也可以從
MIDDLEWARE_CLASSES
中删除
SessionMiddleware
行,并從
INSTALLED_APPS
中删除’
django.contrib.sessions
‘。它将節省一些性能消耗。
配置會話引擎
預設情況下,Django 存儲會話到你的資料庫中(使用
django.contrib.sessions.models.Session
模型)。雖然這很友善,但是在某些架構中存儲會話在其它地方會更快,是以可以配置Django 來存儲會話到你的檔案系統上或緩存中。
使用資料庫支援的會話
如果你想使用資料庫支援的會話,你需要添加’
django.contrib.sessions
’ 到你的
INSTALLED_APPS
設定中。
在配置完成之後,請運作
manage.py migrate
來安裝儲存會話資料的一張資料庫表。
使用基于緩存的會話
為了更好的性能,你可能想使用一個基于緩存的會話後端。
為了使用Django 的緩存系統來存儲會話資料,你首先需要確定你已經配置好你的緩存;詳細資訊參見緩存的文檔。
警告
你應該隻在使用Memcached 緩存系統時才使用基于緩存的會話。基于本地記憶體的緩存系統不會長時間保留資料,是以不是一個好的選擇,而且直接使用檔案或資料庫會話比通過檔案或資料庫緩存系統要快。另外,基于本地記憶體的緩存系統不是多程序安全的,是以對于生産環境可能不是一個好的選擇。
如果你在
CACHES
中定義多個緩存,Django 将使用預設的緩存。若要使用另外一種緩存,請設定
SESSION_CACHE_ALIAS
為該緩存的名字。
配置好緩存之後,對于如何在緩存中存儲資料你有兩個選擇:
- 對于簡單的緩存會話存儲,可以設定
為”SESSION_ENGINE
” 。此時會話資料将直接存儲在你的緩存中。然而,緩存資料将可能不會持久:如果緩存填滿或者緩存伺服器重新開機,緩存資料可能會被清理掉。django.contrib.sessions.backends.cache
- 若要持久的緩存資料,可以設定
SESSION_ENGINE
“。它的寫操作使用緩存 —— 對緩存的每次寫入都将再寫入到資料庫。對于讀取的會話,如果資料不在緩存中,則從資料庫讀取。django.contrib.sessions.backends.cached_db
兩種會話的存儲都非常快,但是簡單的緩存更快,因為它放棄了持久性。大部分情況下,
cached_db
後端已經足夠快,但是如果你需要榨幹最後一點的性能,并且接收讓會話資料丢失,那麼你可使用
cache
後端。
如果你使用
cached_db
會話後端,你還需要遵循
中的配置說明。
Changed in Django 1.7:
在1.7 版之前,`cached_db` 永遠使用`default`緩存而不是`SESSION_CACHE_ALIAS`。
使用基于檔案的緩存
要使用基于檔案的緩存,請設定
SESSION_ENGINE
django.contrib.sessions.backends.file
“。
你可能還想設定
SESSION_FILE_PATH
(它的預設值來自
tempfile.gettempdir()
的輸出,大部分情況是
/tmp
)來控制Django在哪裡存儲會話檔案。請保證你的Web 伺服器具有讀取和寫入這個位置的權限。
使用基于Cookie 的會話
要使用基于Cookie 的會話,請設定
SESSION_ENGINE
django.contrib.sessions.backends.signed_cookies
“。此時,會話資料的存儲将使用Django 的加密簽名 工具和
SECRET_KEY
設定。
注
建議保留SESSION_COOKIE_HTTPONLY 設定為True 以防止從JavaScript 中通路存儲的資料。
如果
沒有保密并且你正在使用
SECRET_KEY
PickleSerializer
,這可能導緻遠端執行任意的代碼。
擁有
SECRET_KEY
的攻擊者不僅可以生成篡改的會話資料而你的站點将會信任這些資料,而且可以遠端執行任何代碼,就像資料是通過pickle 序列化過的一樣。
如果你使用基于Cookie 的會話,請格外注意你的安全秘鑰對于任何可以遠端通路的系統都是永遠完全保密的。
會話資料經過簽名但沒有加密。
如果使用基于Cookie的會話,則會話資料可以被用戶端讀取。
MAC(消息認證碼)被用來保護資料不被用戶端修改,是以被篡改的會話資料将是變成不合法的。如果儲存Cookie的用戶端(例如你的浏覽器)不能儲存所有的會話Cookie或丢失資料,會話同樣會變得不合法。盡管Django 對資料進行壓縮,仍然完全有可能超過每個Cookie
常見的4096 個位元組的限制。
沒有更新保證
還要注意,雖然MAC可以保證資料的權威性(由你的站點生成,而不是任何其他人)和完整性(包含全部的資料并且是正确的),它不能保證是最新的,例如傳回給你發送給用戶端的最新的資料。這意味着對于某些會話資料的使用,基于Cookie 可能讓你受到重播攻擊。其它方式的會話後端在伺服器端儲存每個會話并在使用者登出時使它無效,基于Cookie 的會話在使用者登出時不會失效。是以,如果一個攻擊者盜取使用者的Cookie,它們可以使用這個Cookie 來以這個使用者登入即使使用者已登出。Cookies 隻能被當做是“過期的”,如果它們比你的SESSION_COOKIE_AGE要舊。
性能
最後,Cookie 的大小對
你的網站的速度 有影響。
在視圖中使用會話
當
SessionMiddleware
激活時,每個
HttpRequest
對象 —— 傳遞給Django 視圖函數的第一個參數 —— 将具有一個
session
屬性,它是一個類字典對象。
你可以在你的視圖中任何地方讀取并寫入
request.session
。你可以多次編輯它。
class backends.base.SessionBase
這是所有會話對象的基類。它具有以下标準的字典方法:
__getitem__(key)
例如:
fav_color = request.session['fav_color']
__setitem__(key, value)
request.session['fav_color'] = 'blue'
__delitem__(key)
del request.session['fav_color']
。如果給出的key 在會話中不存在,将抛出
KeyError
__contains__(key)
'fav_color' in request.session
get(key, default=None)
fav_color = request.session.get('fav_color', 'red')
pop(key)
fav_color = request.session.pop('fav_color')
keys()
items()
setdefault()
clear()
它還具有這些方法:
flush()
删除目前的會話資料并删除會話的Cookie。這用于確定前面的會話資料不可以再次被使用者的浏覽器通路(例如,
django.contrib.auth.logout()
函數中就會調用它)。
Changed in Django 1.8:
删除會話Cookie 是Django 1.8 中的新行為。以前,該行為用于重新生成會話中的值,這個值會在Cookie 中發回給使用者。
set_test_cookie()
設定一個測試的Cookie 來驗證使用者的浏覽器是否支援Cookie。因為Cookie 的工作方式,隻有到使用者的下一個頁面才能驗證。更多資訊參見下文的設定測試的Cookie。
test_cookie_worked()
傳回
True
或
False
,取決于使用者的浏覽器時候接受測試的Cookie。因為Cookie的工作方式,你必須在前面一個單獨的頁面請求中調用
set_test_cookie()
。更多資訊參見下文的設定測試的Cookie。
delete_test_cookie()
删除測試的Cookie。使用這個函數來自己清理。
set_expiry(value)
設定會話的逾時時間。你可以傳遞一系列不同的值:
-
是一個整數,會話将在這麼多秒沒有活動後過期。例如,調用value
将使得會話在5分鐘後過期。request.session.set_expiry(300)
- 若果value 是一個
datetime
對象,會話将在這個指定的日期/時間過期。注意timedelta
和datetime
值隻有在你使用Ptimedelta
時才可序列化。ickleSerializer
-
為0,那麼使用者會話的Cookie将在使用者的浏覽器關閉時過期。value
-
為value
,那麼會話轉向使用全局的會話過期政策。None
過期的計算不考慮讀取會話的操作。會話的過期從會話上次修改的時間開始計算。
get_expiry_age()
傳回會話離過期的秒數。對于沒有自定義過期的會話(或者設定為浏覽器關閉時過期的會話),它将等于
SESSION_COOKIE_AGE
該函數接收兩個可選的關鍵字參數:
-
:會話的最後一次修改時間,類型為一個modification
對象。預設為目前的時間。datetime
-
:會話的過期資訊,類型為一個expiry
對象、一個整數(以秒為機關)或datetime
。預設為通過None
儲存在會話中的值,如果沒有則為set_expiry()
None
get_expiry_date()
傳回過期的日期。對于沒有自定義過期的會話(或者設定為浏覽器關閉時過期的會話),它将等于從現在開始
SESSION_COOKIE_AGE
秒後的日期。
這個函數接受與
get_expiry_age()
一樣的關鍵字參數。
get_expire_at_browser_close()
True
False
,取決于使用者的會話Cookie在使用者浏覽器關閉時會不會過期。
clear_expired()
從會話的存儲中清除過期的會話。這個類方法被
clearsessions
調用。
cycle_key()
建立一個新的會話,同時保留目前的會話資料。
django.contrib.auth.login()
調用這個方法來減緩會話的固定。
會話的序列化
在1.6 版以前,在儲存會話資料到後端之前Django 預設使用pickle 來序列化它們。如果你使用的是簽名的Cookie 會話後端 并且
SECRET_KEY
被攻擊者知道(Django 本身沒有漏洞會導緻它被洩漏),攻擊者就可以在會話中插入一個字元串,在unpickle 之後可以在伺服器上執行任何代碼。在網際網路上這個攻擊技術很簡單并很容易查到。盡管Cookie 會話的存儲對Cookie 儲存的資料進行了簽名以防止篡改,
SECRET_KEY
的洩漏會立即使得可以執行遠端的代碼。
這種攻擊可以通過JSON而不是pickle序列化會話資料來減緩。為了幫助這個功能,Django 1.5.3 引入一個新的設定,
SESSION_SERIALIZER
,來自定義會話序列化的格式。為了向後相容,這個設定在Django 1.5.x 中預設為
django.contrib.sessions.serializers.PickleSerializer
,但是為了增強安全性,在Django 1.6 中預設為
django.contrib.sessions.serializers.JSONSerializer
。即使在編寫你自己的序列化方法講述的說明中,我們也強烈建議依然使用JSON 序列化,特别是在你使用的是Cookie 後端時。
綁定的序列化方法
class serializers.JSONSerializer
對
django.core.signing
中的JSON 序列化方法的一個包裝。隻可以序列基本的資料類型。
另外,因為JSON 隻支援字元串作為鍵,注意使用非字元串作為
request.session
的鍵将不工作:
>>> # initial assignment
>>> request.session[0] = 'bar'
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0] # KeyError
>>> request.session['0']
'bar'
參見
編寫你自己的序列化器一節以獲得更多關于JSON 序列化的限制。
class serializers.PickleSerializer
支援任意Python 對象,但是正如上面描述的,可能導緻遠端執行代碼的漏洞,如果攻擊者知道了
SECRET_KEY
注意,與
PickleSerializer
不同,
JSONSerializer
不可以處理任意的Python 資料類型。這是常見的情況,需要在便利性和安全性之間權衡。如果你希望在JSON 格式的會話中存儲更進階的資料類型比如
datetime
Decimal
,你需要編寫一個自定義的序列化器(或者在儲存它們到
request.session
中之前轉換這些值到一個可JSON 序列化的對象)。雖然序列化這些值相當簡單直接 (
django.core.serializers.json.DateTimeAwareJSONEncoder
可能幫得上忙),編寫一個解碼器來可靠地取出相同的内容卻能困難。例如,傳回一個
datetime
時,它可能實際上是與
datetime
格式碰巧相同的一個字元串)。
你的序列化類必須實作兩個方法,
dumps(self, obj)
loads(self, data)
來分别序列化和去序列化會話資料的字典。
會話對象指南
在request.session
上使用普通的Python 字元串作為字典的鍵。這主要是為了友善而不是一條必須遵守的規則。
以一個下劃線開始的會話字典的鍵被Django保留作為内部使用。
不要新的對象覆寫
request.session
,且不要通路或設定它的屬性。要像Python 字典一樣使用它。
例子
下面這個簡單的視圖在一個使用者送出一個評論後設定
has_commented
變量為
True
。它不允許一個使用者多次送出評論:
def post_comment(request, new_comment):
if request.session.get('has_commented', False):
return HttpResponse("You've already commented.")
c = comments.Comment(comment=new_comment)
c.save()
request.session['has_commented'] = True
return HttpResponse('Thanks for your comment!')
登入站點一個“成員”的最簡單的視圖:
def login(request):
m = Member.objects.get(username=request.POST['username'])
if m.password == request.POST['password']:
request.session['member_id'] = m.id
return HttpResponse("You're logged in.")
else:
return HttpResponse("Your username and password didn't match.")
…下面是登出一個成員的視圖,已經上面的login():
def logout(request):
try:
del request.session['member_id']
except KeyError:
pass
return HttpResponse("You're logged out.")
标準的
django.contrib.auth.logout()
函數實際上所做的内容比這個要多一點以防止意外的資料洩露。它調用的
request.session
的
flush()
方法。我們使用這個例子來示範如何利用會話對象來工作,而不是一個完整的
logout()
實作。
設定測試的Cookie
為了友善,Django 提供一個簡單的方法來測試使用者的浏覽器時候接受Cookie。隻需在一個視圖中調用
request.session
set_test_cookie()
方法,并在接下來的視圖中調用
test_cookie_worked()
—— 不是在同一個視圖中調用。
由于Cookie的工作方式,在
set_test_cookie()
test_cookie_worked()
之間這種笨拙的分離是必要的。當你設定一個Cookie,直到浏覽器的下一個請求你不可能真實知道一個浏覽器是否接受了它。
使用
delete_test_cookie()
來自己清除測試的Cookie是一個很好的實踐。請在你已經驗證測試的Cookie 已經工作後做這件事。
下面是一個典型的使用示例:
def login(request):
if request.method == 'POST':
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
return HttpResponse("You're logged in.")
else:
return HttpResponse("Please enable cookies and try again.")
request.session.set_test_cookie()
return render_to_response('foo/login_form.html')
在視圖外使用會話
這一節中的示例直接從中導
django.contrib.sessions.backends.db
對象。在你的代碼中,你應該從
入SessionStore
指定的會話引擎中導入
SESSION_ENGINE
,如下所示:
SessionStore
>>> from importlib import import_module
>>> from django.conf import settings
>>> SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
在視圖的外面有一個API 可以使用來操作會話的資料:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore()
>>> # stored as seconds since epoch since datetimes are not serializable in JSON.
>>> s['last_login'] = 1376587691
>>> s.save()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
>>> s['last_login']
1376587691
為了減緩會話固話攻擊,不存在的會話的鍵将重新生成:
>>> from django.contrib.sessions.backends.db import SessionStore
>>> s = SessionStore(session_key='no-such-session-here')
>>> s.save()
>>> s.session_key
'ff882814010ccbc3c870523934fee5a2'
如果你使用的是
django.contrib.sessions.backends.db
後端,每個會話隻是一個普通的Django 模型。
Session
模型定義在
django/contrib/sessions/models.py
中。因為它是一個普通的模型,你可以使用普通的Django 資料庫API 來通路會話:
>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)
注意,你需要調用
get_decoded()
以獲得會話的字典。這是必需的,因為字典是以編碼後的格式儲存的:
>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}
會話何時儲存
預設情況下,Django 隻有在會話被修改時才會儲存會話到資料庫中 —— 即它的字典中的任何值被指派或删除時:
# Session is modified.
request.session['foo'] = 'bar'
# Session is modified.
del request.session['foo']
# Session is modified.
request.session['foo'] = {}
# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session['foo']['bar'] = 'baz'
上面例子的最後一種情況,我們可以通過設定會話對象的
modified
屬性顯式地告訴會話對象它已經被修改過:
request.session.modified = True
若要修改這個預設的行為,可以設定
SESSION_SAVE_EVERY_REQUEST
True
。當設定為
True
時,Django 将對每個請求儲存會話到資料庫中。
注意會話的Cookie 隻有在一個會話被建立或修改後才會發送。如果
SESSION_SAVE_EVERY_REQUEST
True
,會話的Cookie 将在每個請求中發送。
類似地,會話Cookie 的
expires
部分在每次發送會話Cookie 時更新。
如果響應的狀态碼時500,則會話不會被儲存。
浏覽器時長的會話 VS. 持久的會話
你可以通過
SESSION_EXPIRE_AT_BROWSER_CLOSE
設定來控制會話架構使用浏覽器時長的會話,還是持久的會話。
預設情況下,
SESSION_EXPIRE_AT_BROWSER_CLOSE
設定為
False
,表示會話的Cookie 儲存在使用者的浏覽器中的時間為
SESSION_COOKIE_AGE
。如果你不想讓大家每次打開浏覽器時都需要登入時可以這樣使用。
SESSION_EXPIRE_AT_BROWSER_CLOSE
True
,Django 将使用浏覽器時長的Cookie —— 使用者關閉他們的浏覽器時立即過期。如果你想讓大家在每次打開浏覽器時都需要登入時可以這樣使用。
這個設定是一個全局的預設值,可以通過顯式地調
request.session
set_expiry()
方法來覆寫,在上面的在視圖中使用會話中有描述。
某些浏覽器(例如Chrome)提供一種設定,允許使用者在關閉并重新打開浏覽器後繼續使用會話。在某些情況下,這可能幹擾設定并導緻會話在浏覽器關閉後不會過期。在測試啟用
SESSION_EXPIRE_AT_BROWSER_CLOSE
設定的Django 應用時請注意這點。
SESSION_EXPIRE_AT_BROWSER_CLOSE
清除存儲的會話
随着使用者在你的網站上建立新的會話,會話資料可能會在你的會話存儲倉庫中積累。如果你正在使用資料庫作為後端,
django_session
資料庫表将持續增長。如果你正在使用檔案作為後端,你的臨時目錄包含的檔案數量将持續增長。
要了解這個問題,考慮一下資料庫後端發生的情況。當一個使用者登入時,Django 添加一行到
django_session
資料庫表中。每次會話資料更新時,Django 将更新這行。如果使用者手工登出,Django 将删除這行。但是如果該使用者不登出,該行将永遠不會删除。以檔案為後端的過程類似。
Django 不提供自動清除過期會話的功能。是以,定期地清除會話是你的任務。Django 提供一個清除用的管理指令來滿足這個目的:
clearsessions
。建議定義調用這個指令,例如作為一個每天運作的Cron 任務。
注意,以緩存為後端不存在這個問題,因為緩存會自動删除過期的資料。以cookie 為後端也不存在這個問題,因為會話資料通過使用者的浏覽器儲存。
設定
一些
Django 設定讓你可以控制會話的行為:
- SESSION_CACHE_ALIAS
- SESSION_COOKIE_AGE
- SESSION_COOKIE_DOMAIN
- SESSION_COOKIE_HTTPONLY
- SESSION_COOKIE_NAME
- SESSION_COOKIE_PATH
- SESSION_COOKIE_SECURE
- SESSION_ENGINE
- SESSION_EXPIRE_AT_BROWSER_CLOSE
- SESSION_FILE_PATH
- SESSION_SAVE_EVERY_REQUEST
會話的安全
一個站點下的子域名能夠在用戶端為整個域名設定Cookie。如果子域名不收信任的使用者控制且允許來自子域名的Cookie,那麼可能發生會話固定。
例如,一個攻擊者可以登入
good.example.com
并為他的賬号擷取一個合法的會話。如果該攻擊者具有
bad.example.com
的控制權,那麼他可以使用這個域名來發送他的會話ID給你,因為子域名允許在
*.example.com
上設定Cookie。當你通路
good.example.com
時,你将被登入成攻擊者而沒有注意到并輸入你的敏感的個人資訊(例如,信用卡資訊)到攻擊者的賬号中。
另外一個可能的攻擊是,如果
good.example.com
設定它的
SESSION_COOKIE_DOMAIN
.example.com
” ,這将導緻來自該站點的會話Cookie 被發送到
bad.example.com
技術細節
- 當使用
時,會話字典接收任何可json 序列化的值,當使用JSONSerializer
時接收任何pickleable 的Python對象。更多資訊參見PickleSerializer
子產品。pickle
- 會話資料存儲在資料中名為
的表中。django_session
- Django 隻發送它需要的Cookie。如果你沒有設定任何會話資料,它将不會發送會話Cookie。
URL 中的會話ID
Django 會話架構完全地、唯一地基于Cookie。它不像PHP一樣,實在沒辦法就把會話的ID放在URL 中。這是一個故意的設計。這個行為不僅使得URL變得醜陋,還使得你的網站易于受到通過”Referer” 頭部竊取會話ID的攻擊。
譯者: Django 文檔協作翻譯小組 ,原文: Sessions 本文以 CC BY-NC-SA 3.0 協定釋出,轉載請保留作者署名和文章出處。 人手緊缺,有興趣的朋友可以加入我們,完全公益性質。交流群:467338606。