歡迎大家訪問我的個人網站《劉江的博客和教程》:www.liujiangblog.com
主要分享Python 及Django教程以及相關的博客
django支持匿名會話。它將數據存放在服務器端,並抽象cookies的發送和接收過程。cookie包含一個會話ID而不是數據本身(除非你使用的是基於后端的cookie)。
3.3.8.1 啟用會話
Django通過一個中間件來實現會話功能。要啟用會話就要先啟用該中間件。
編輯MIDDLEWARE設置,確保存在django.contrib.sessions.middleware.SessionMiddleware這一行。默認情況在新建的項目中它是存在的。
如果你不想使用會話功能,那么在settings文件中,將SessionMiddleware從MIDDLEWARE中刪除,將django.contrib.sessions從 INSTALLED_APPS中刪除就OK了。
3.3.8.2 配置會話引擎
默認情況下,django將會話數據保存在數據庫內(通過使用django.contrib.sessions.models.Session模型)。當然,你也可以將數據保存在文件系統或緩存內。
1. 基於數據庫的會話
先在INSTALLED_APPS設置中,確保django.contrib.sessions的存在,然后運行manage.py migrate命令在數據庫內創建sessions表。
2. 基於緩存的會話
從性能角度考慮,也許你想使用基於緩存的會話。
首先,你得先配置好你的緩存,請參考3.11節查看詳細。
警告:
本地緩存不是多進程安全的,因此對於生產環境不是一個好的選擇。
如果你定義有多個緩存,django將使用默認的那個。如果你想用其它的,請將SESSION_CACHE_ALIAS參數設置為那個緩存的名字。
配置好緩存后,你可以選擇兩種保存數據的方法:
- 一是將SESSION_ENGINE設置為"django.contrib.sessions.backends.cache",簡單的對會話進行保存。但是這種方法不是很可靠,因為當緩存數據存滿時將清除部分數據,或者遇到緩存服務器重啟。
- 為了數據安全保障,你可以將SESSION_ENGINE設置為"django.contrib.sessions.backends.cached_db"。這種方式在每次緩存的時候會同時將數據在數據庫內寫一份。當緩存不可用時,會話會從數據庫內讀取數據。
兩種方法都很迅速,但是第一種簡單的緩存更快一些,因為它忽略了數據的持久性。如果你使用緩存+數據庫的方式,你同樣需要按上面小節所述對數據庫進行配置。
3. 基於文件的會話
將SESSION_ENGINE設置為"django.contrib.sessions.backends.file"。同時,你必須查看
SESSION_FILE_PATH配置(默認根據tempfile.gettempdir()生產,就像/tmp目錄),確保你的文件存儲目錄,以及Web服務器對該目錄具有讀寫權限。
4. 基於cookie的會話
將SESSION_ENGINE設置為"django.contrib.sessions.backends.signed_cookies"。Django將使用加密簽名工具和安全秘鑰設置保存會話的數據。
注意:
建議將SESSION_COOKIE_HTTPONLY設置為True,阻止javascript對會話數據的訪問。
3.3.8.3 在視圖中使用會話
當會話中間件啟用后,傳遞給視圖request參數的HttpRequest對象將包含一個session屬性,就像一個字典對象一樣。
你可以在視圖的任何地方讀寫request.session屬性,或者多次編輯使用它。
class backends.base.SessionBase
# 這是所有會話對象的基類,包含標准的字典方法:
__getitem__(key)
Example: fav_color = request.session[’fav_color’]
__setitem__(key, value)
Example: request.session[’fav_color’] = ’blue’
__delitem__(key)
Example: del request.session[’fav_color’]. 如果不存在會拋出異常
__contains__(key)
Example: ’fav_color’ in request.session
get(key, default=None)
Example: fav_color = request.session.get(’fav_color’, ’red’)
pop(key, default=__not_given)
Example: fav_color = request.session.pop(’fav_color’, ’blue’)
keys()
items()
setdefault()
clear()
# 它還有下面的方法:
flush()
# 刪除當前的會話數據和會話cookie。經常用在用戶退出后,刪除會話。
set_test_cookie()
# 設置一個測試cookie,用於探測用戶瀏覽器是否支持cookies。由於cookie的工作機制,你只有在下次用戶請求的時候才可以測試。
test_cookie_worked()
# 返回True或者False,取決於用戶的瀏覽器是否接受測試cookie。你必須在之前先調用set_test_cookie()方法。
delete_test_cookie()
# 刪除測試cookie。
set_expiry(value)
# 設置cookie的有效期。可以傳遞不同類型的參數值:
• 如果值是一個整數,session將在對應的秒數后失效。例如request.session.set_expiry(300) 將在300秒后失效.
• 如果值是一個datetime或者timedelta對象, 會話將在指定的日期失效
• 如果為0,在用戶關閉瀏覽器后失效
• 如果為None,則將使用全局會話失效策略
失效時間從上一次會話被修改的時刻開始計時。
get_expiry_age()
# 返回多少秒后失效的秒數。對於沒有自定義失效時間的會話,這等同於SESSION_COOKIE_AGE.
# 這個方法接受2個可選的關鍵字參數
• modification:會話的最后修改時間(datetime對象)。默認是當前時間。
•expiry: 會話失效信息,可以是datetime對象,也可以是int或None
get_expiry_date()
# 和上面的方法類似,只是返回的是日期
get_expire_at_browser_close()
# 返回True或False,根據用戶會話是否是瀏覽器關閉后就結束。
clear_expired()
# 刪除已經失效的會話數據。
cycle_key()
# 創建一個新的會話秘鑰用於保持當前的會話數據。django.contrib.auth.login() 會調用這個方法。
1. 會話序列化
Django默認使用JSON序列化會話數據。你可以在SESSION_SERIALIZER設置中自定義序列化格式,甚至寫入警告說明。但是我們強烈建議你還是使用JSON,尤其是以cookie的方式進行會話時。
舉個例子,這里有一個使用pickle序列化會話數據的攻擊場景。如果你使用的是已簽名的Cookie會話並且SECRET_KEY被攻擊者知道了(通過其它手段),攻擊者就可以在會話中插入一個字符串,在pickle反序列化時,可以在服務器上執行危險的代碼。在因特網上這個攻擊技術很簡單並很容易使用。盡管Cookie會話會對數據進行簽名以防止篡改,但是SECRET_KEY的泄漏卻使得一切前功盡棄。
綁定的序列化方法
class serializers.JSONSerializer
對 django.core.signing中JSON序列化方法的一個包裝。只可以序列化基本的數據類型。另外,JSON只支持以字符串作為鍵值,使用其它的類型會導致異常
>>> # initial assignment
>>> request.session[0] = 'bar'
>>> # subsequent requests following serialization & deserialization
>>> # of session data
>>> request.session[0] # KeyError
>>> request.session['0']
'bar'
同樣,無法被JSON編碼的,例如非UTF8格式的字節’\xd9’一樣是無法被保存的,它會導致UnicodeDecodeError異常。
class serializers.PickleSerializer
支持任意類型的python對象,但是就像前面說的,可能導致遠端執行代碼的漏洞,如果攻擊者知道了SECRET_KEY。
編寫你自己的序列化方法
你的序列化類必須分別實現dumps(self, obj)和loads(self, data)方法,用來實現序列化和反序列化會話數據字典。
2. 會話對象使用建議
- 使用普通的python字符串作為request.session字典的鍵值。這不是一條硬性規則而是為方便起見。
- 以一個下划線開始的會話字典的鍵被Django保留作為內部使用。
- 不要用新對象覆蓋request.session,不要訪問或設置它的屬性。像一個python字典一樣的使用它。
3. 范例
這個簡單的視圖設置一個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.")
下面則是一個退出登錄的視圖,與上面的相關:
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()實現。
3.3.8.4 設置測試cookie
為了方便,Django 提供一個簡單的方法來測試用戶的瀏覽器是否接受Cookie。只需在一個視圖中調用request.session的set_test_cookie()方法,並在隨后的視圖中調用test_cookie_worked()獲取測試結果(True或False)。注意,不能在同一個視圖中調用這兩個方法。
造成這種分割調用的原因是cookie的工作機制。當你設置一個cookie時,你無法立刻得到結果,知道瀏覽器發送下一個請求。
在測試后,記得使用delete_test_cookie()方法清除測試數據。
下面是一個典型的范例:
from django.http import HttpResponse
from django.shortcuts import render
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(request, 'foo/login_form.html')
3.8.3.5 在視圖外使用session
注意:
在下面的例子中,我們直接從django.contrib.sessions.backends.db中導入了SessionStore對象。在你的實際代碼中,你應該采用下面的導入方法,根據SESSION_ENGINE的設置進行導入,如下所示:
>>> 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.create()
>>> s.session_key
'2b1189a188b44ad18c35e113ac6ceead'
>>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead')
>>> s['last_login']
1376587691
SessionStore.create()用於創建一個新的會話。save()方法用於保存一個已經存在的會話。create方法會調用save方法並循環直到生成一個未使用的session_key。直接調用save方法也可以創建一個新的會話,但在生成session_key的時候有可能和已經存在的發生沖突。
如果你使用的是django.contrib.sessions.backends.db模式,那么每一個會話其實就是一個普通的Django模型,你可以使用普通的Django數據庫API訪問它。會話模型的定義在django/contrib/sessions/models.py文件里。例如:
>>> 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}
3.3.8.6 何時保存會話
默認情況下,只有當會話字典的任何值被指定或刪除的時候,Django才會將會話內容保存到會話數據庫內。
# 會話被修改
request.session['foo'] = 'bar'
# 會話被修改
del request.session['foo']
# 會話被修改
request.session['foo'] = {}
# 會話沒有被修改,只是修改了request.session['foo']
request.session['foo']['bar'] = 'baz'
要理解上面最后一種情況有點費勁。我們可以通過設置會話對象的modified屬性值,顯式地告訴會話對象它已經被修改過:request.session.modified = True
要改變上面的默認行為,將SESSION_SAVE_EVERY_REQUEST設置為True,那么每一次單獨的請求過來,Django都會保存會話到數據庫。
注意,會話的Cookie只有在一個會話被創建或修改后才會再次發送。如果SESSION_SAVE_EVERY_REQUEST為True,每個請求都會發送cookie。
類似地,會話Cookie的失效部分在每次發送會話Cookie時都會更新。
如果響應的狀態碼為500,則會話不會被保存。
3.3.8.7 瀏覽器生存期間會話 VS 持久會話
默認情況下,SESSION_EXPIRE_AT_BROWSER_CLOSE設置為False,也就是說cookie保存在用戶的瀏覽器內,直到失效日期,這樣用戶就不必每次打開瀏覽器后都要再登錄一次。
相反的SESSION_EXPIRE_AT_BROWSER_CLOSE設置為True,則意味着瀏覽器一關閉,cookie就失效,每次重新打開瀏覽器,你就得重新登錄。
這個設置是一個全局的默認值,可以通過顯式地調request.session的set_expiry()方法來覆蓋,前面我們已經描述過了。
注意:有些瀏覽器(比如Chrome)具有在關閉后重新打開瀏覽器,會話依然保持的功能。這會與Django的SESSION_EXPIRE_AT_BROWSER_CLOSE設置發生沖突。請一定要小心。
3.3.8.8 清除已保存的會話
隨着用戶的訪問,會話數據會越來越龐大。如果你使用的是數據庫保存模式,那么django_session表的內容會逐漸增長。如果你使用的是文件模式,那么你的臨時目錄內的文件數量會不斷增加。
造成這個問題的原因是,如果用戶手動退出登錄,Django將自動刪除會話數據,但是如果用戶不退出登錄,那么對應的會話數據不會被刪除。
Django沒有提供自動清除失效會話的機制。因此,你必須自己完成這項工作。Django提供了一個命令clearsessions用於清除會話數據,建議你基於這個命令設置一個周期性的自動清除機制。
不同的是,使用緩存模式的會話不需要你清理數據,因為緩存系統自己有清理過期數據的機制。使用cookie模式的會話也不需要,因為數據都存在用戶的瀏覽器內,不用你幫忙。
3.3.8.9 設置
這里有一些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
- SESSION_SERIALIZER
3.3.8.10 會話安全
一個站點下的子域名能夠在為整個域名的客戶設置Cookie。如果子域名被不受信任的用戶控制,那么可能發生會話安全問題。
例如,一個攻擊者可以登錄good.example.com並為他的賬號獲取一個合法的會話。如果該攻擊者控制了bad.example.com域名,那么他就可以使用這個域名來發送他的會話秘鑰給你,因為子域名允許在*.example.com上設置Cookie。當你訪問good.example.com時,你有可能以攻擊者的身份登錄,然后無意中泄露了你的個人敏感信息(例如信用卡信息)到攻擊者的賬號中。攻擊者自然就獲得了這些信息。
另外一個可能的攻擊是,如果good.example.com設置它的SESSION_COOKIE_DOMAIN為".example.com" ,這可能導致來自該站點的會話Cookie被發送到bad.example.com。
3.3.8.11 技術細節
- 會話字典接收任意的json序列化值,或者任何可通過pickle序列化的python對象
- 會話數據被保存在一張名為django_session的表內
- Django 只發送它需要的Cookie。如果你沒有設置任何會話數據,它不會發送任何Cookie
SessionStore對象
在會話內部,Django使用一個與會話引擎對應的會話保存對象。根據管理,這個會話保存對象命名為SessionStore,位於SESSION_ENGINE設置指定的模塊內。
所有Django支持的SessionStore類都繼承SessionBase類,並實現了下面的數據操作方法:
- exists()
- create()
- save()
- delete()
- load()
- clear_expored()
為了創建一個自定義會話引擎或修改一個現成的引擎,你也許需要創建一個新的類,它繼承SessionBase類或任何其他已經存在的SessionStore類。
3.3.8.12 擴展基於數據庫的會話引擎
Django 1.9版本以后才有的功能。
要創建一個自定義的基於數據庫的會話引擎,需要繼承AbstractBaseSession類或者SessionStore類。
AbstractBaseSession和BaseSessionManager可以從django.contrib.sessions.base_session內導入,因此不一定非要在INSTALLED_APPS中包含django.contrib.sessions。
class base_session.AbstractBaseSession # 抽象會話基類
session_key
主鍵。最多40個字符。目前是一個32為隨機數字或字母組合字符串。
session_data
一個包含了編碼過的的或序列化過的會話字典的字符串
expire_date
失效日期
get_session_store_class()
這是一個類方法。返回一個會話保存類。
get_decoded()
返回解碼后的會話數據。通過會話保存類進行解碼。
你也可以自定義模型管理器,通過編寫一個BaseSessionManager的子類。
class base_session.BaseSessionManager
encode(session_dict)
通過會話保存類,將會話字典序列化或編碼成一個字符串
save(session_key, session_dict, expire_date)
根據一個提供的session秘鑰保存會話數據,或者刪除一個空的會話
通過重寫下面這些SessionStore類的方法和屬性,可以進行自定制:
class backends.db.SessionStore
實現基於數據庫的會話保存
get_model_class()
這是一個類方法。如果有需要,重寫這個方法並返回一個自定義的會話模型。
create_model_instance(data)
返回一個會話模型對象的新實例,它代表當前會話的狀態。重寫這個方法,你將獲得在它被保存之前,修改會話模型數據的能力。
class backends.cached_db.SessionStore
實現基於緩存和數據庫的會話保存
cache_key_prefix
在會話秘鑰前添加一個前綴,用於構造緩存鍵值字符串。
范例
下面的例子展示一個自定義的基於數據庫的會話引擎,包括一個額外的數據列用於儲存用戶的ID。
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.contrib.sessions.base_session import AbstractBaseSession
from django.db import models
class CustomSession(AbstractBaseSession):
account_id = models.IntegerField(null=True, db_index=True)
@classmethod
def get_session_store_class(cls):
return SessionStore
class SessionStore(DBStore):
@classmethod
def get_model_class(cls):
return CustomSession
def create_model_instance(self, data):
obj = super(SessionStore, self).create_model_instance(data)
try:
account_id = int(data.get('_auth_user_id'))
except (ValueError, TypeError):
account_id = None
obj.account_id = account_id
return obj
如果你是通過從Django內置的cached_db會話保存遷移到自定義的cached_db,你應該重寫緩存鍵值的前綴,以防止命名空間的沖突,如下所示:
class SessionStore(CachedDBStore):
cache_key_prefix = 'mysessions.custom_cached_db_backend'
# ...
3.3.8.13 URLs中的會話IDs
Django的會話框架完全地、唯一地基於Cookie。它不像PHP一樣,把會話的ID放在URL中。它不僅使得URL變得丑陋,還使得你的網站易於受到通過"Referer"頭部進行竊取會話ID的攻擊。