Django中和時區(qū)相關的安全問題詳解
在開發(fā)國際化網站的時候,難免會與時區(qū)打交道,通用CMS更是如此,畢竟其潛在用戶可能是來自于全球各地的。Django在時區(qū)這個問題上下了不少功夫,但是很多資深的開發(fā)者都有可能尚未完全屢清楚Django中各種時間的實際意義和使用方法,導致寫出錯誤的代碼;作為安全研究人員,時區(qū)問題也可能和一些安全問題掛鉤,比如優(yōu)惠券的過期時間、訂單的下單與取消時間等,如果沒有考慮時區(qū)問題,有可能將導致一些邏輯漏洞。
本文就從多個常用模塊開始,了解一下Django中的時區(qū)究竟是怎么回事,以及在時間的比較中可能出現的一些邏輯錯誤。
從“兩種時間”說起
我們都知道,在Python中表示“時間”的對象是datetime.datetime
。
其實在Python中,這個對象被分成了兩個類型:
- aware datetime
- naive datetime
他們的區(qū)別是:如果datetime
對象的tzinfo
屬性有設置時區(qū)值,則這個對象是一個aware datime;否則它是一個naive datetime。
舉個例子,我們平時在編寫Python腳本的時候,使用下面這行代碼獲取當前時間:
from datetime import datetime t = datetime.now()
此時,t是一個naive datetime,因為我們沒有給他設置時區(qū):
naive的中文意思大家應該都很熟悉,這里的大概意思就是“simple”,這是一個很簡單、原始的時間對象。實際上就是指,計算機不知道這個時間,他的時區(qū)究竟是什么,它可能代表著北京時間,也可能是UTC時間,因為我們沒有指定時區(qū),我們無法“假設”其是計算機系統所在的時區(qū),也無法“假設”其是UTC時區(qū)。也就是說,計算機拿到了一個naive datetime,是無法準確地定位到某一個時間點的,也無法直接轉換成一個unix時間戳。
那么相對的,aware datetime就是計算機能準確知道其時區(qū)的時間對象,他是一個準確的時間點,就落在時間軸上的某個地方,不管從哪個時區(qū)看,這個點都是絕對固定的。所以,我們可以將一個aware datetime轉換成unix時間戳。
有的同學可能比較好奇,你說naive datetime無法轉換成時間戳,那么為什么這個對象有一個timestamp()
方法呢:
原因我們查文檔可以得出結論,如果對象是naive datetime,則會以當前系統本地時區(qū)為準。
Django的時區(qū)配置
回到Django。由于Django是一個國際化框架,時區(qū)相關處理自然是其必不可少的組成部分。Django的配置項中,有下面兩個選項與時區(qū)相關:
USE_TZ
TIME_ZONE
USE_TZ
用來指定整個項目是否使用時區(qū),TIME_ZONE
是默認時區(qū)的值。
如果USE_TZ
的值設置為False,那么Django項目中所有時間都使用naive datetime(除非有明確指定時區(qū)的情況)。也就是說,網站內存儲和使用的時間全部是TIME_ZONE
的值所指定的時區(qū)。
這樣做有一些弊端:
- 數據庫中保存的是naive datetime,導致在跨區(qū)域遷移數據的時候,可能無法準確定位到某個時間點
- 國際化企業(yè)可能面向不同國家有不同的網站,但后臺數據庫相同,此時究竟使用哪個時區(qū)保存和展示時間,將引起混亂
- 即使是同一個網站的用戶,他們可能來自于全球各地,查看到的時間卻是統一的服務器時間,對于高交互式的應用十分不友好
- 即使網站面向的用戶僅來自于某一個地區(qū),也會涉及到“夏時令”(Daylight Saving Time)相關的問題,每年可能將會導致兩次時間誤差
默認情況下,用django-admin
生成的項目,其設置中USE_TZ
等于True,這也是Django官方建議的配置。此時,在網站內部存儲與使用的是UTC時間,而與用戶交互時使用TIME_ZONE
或手工的時區(qū)。
我們后文中也以Django的默認配置USE_TZ=True
為前提條件,否則也沒有討論的必要了。
Django的時間函數
Django的包django.utils.timezone
中有下面幾個常用的時間相關函數:
- now(),返回當前的UTC時間
- localtime(),返回當前的本地時間(默認是TIME_ZONE配置指定的時區(qū)時間)
- is_aware(),傳入的時間是否是aware datetime
- is_naive(),傳入的時間是否是naive datetime
- make_aware(),將naive時間轉換成aware時間
- make_naive(),將aware時間轉換成naive時間
因為開啟了USE_TZ
,Django內部操作時間時都應該使用aware時間,否則會出現異常。所以,我們在獲取當前時間的時候,一定要使用Django自帶的now()
或localtime()
函數,而不能使用Python的datetime.datetime.now()
函數。
數據庫存儲的時間
我們在使用ORM的DatetimeField時,常常會有這樣的疑慮:我們究竟應該給DatetimeField傳入哪個時區(qū)的時間呢?
可以做個試驗,編寫下面這個model:
class Archive(models.Model): title = models.CharField('title', max_length=256) now_time = models.DateTimeField(default=timezone.now) local_time = models.DateTimeField(default=timezone.localtime)
這個model有三個屬性,title是他的名字,now_time和local_time是兩個時間,他們的默認值分別是timezone.now和timezone.localtime。
也就是說,默認情況下,now_time字段傳入的是UTC時區(qū)的當前時間,local_time字段傳入的是本地時區(qū)的當前時間,我這里是Asia/Shanghai
。
然后,我們創(chuàng)建一個Archive對象:
可以發(fā)現,不管我們使用a.now_time
還是a.local_time
,讀取到的datetime對象的tzinfo都是UTC。
這也印證了Django文檔中說到的,不管傳入的時間對象時區(qū)是什么,其內部存儲的時間均為UTC時區(qū)。但是,值得注意的是,如果我們傳入了一個不帶時區(qū)的naive datetime,將會出現一個警告,并使用默認時區(qū)填充其tzinfo:
模板中展示的時間
對于網站的用戶來說,他們想看到的時間顯然不是UTC時間,而是某一個具體時區(qū)的時間。比如,我的網站幾乎全部是中國用戶,那么展示時使用的時區(qū)應該是Asia/Shanghai
。
這一部分的轉換,Django放在的模板引擎中。
Django在渲染模板變量時,將會遇到兩種與時間有關的情況:
<p>origin value: {{ object.now_time }}</p> <p>date filter: {{ object.now_time | date:'Y-m-d H:i:s' }}</p>
前者是直接將時間渲染到頁面中,后者是通過date這樣的模板filter處理后渲染在頁面中。這兩種情況在內部處理方式略有不同此處不細表,總體而言,任意模板中變量的渲染,都會被轉換時區(qū)。
那么,脫離模板引擎,我們會得到怎樣的結果呢?
在流行的前后端分離架構中,后端服務器通常只提供JSON格式的接口給前端,那么,我們編寫下面這樣一個view,看看返回值是什么:
from django.shortcuts import get_object_or_404 from django.http.response import JsonResponse from django.utils import timezone from . import models def json(request): object = get_object_or_404(models.Archive, pk=1) data = dict( id=object.pk, now_time=object.now_time, local_time=timezone.localtime(object.local_time) ) return JsonResponse(data=data)
返回對象的now_time,我直接將object.now_time
返回;返回對象的local_time,我將數據庫值轉換成本地時間timezone.localtime(object.local_time)
返回。
我前文說過,這兩個值在數據庫中的值是完全相等的,不過在json返回中,now_time是UTC時間,而local_time是北京時間:
也就是說,在前后端分離的網站中,如果直接使用Model的字段,那么前端需要負責進行時區(qū)的轉換,否則將會出現時間的偏差。
時間的校驗和比較
在一些業(yè)務場景下,我們可能會涉及到時間的校驗和比較,如:
- 付費服務、商品、用戶的有效期檢查
- 活動的開始與結束時間檢查
- 訂單、商品的收貨、取消時間檢查
我們就以付費用戶為例:用戶購買了30天的VIP會員,我們需要給用戶表中設置一個過期時間,比如下面這個model。
from django.db import models from django.utils import timezone class Account(models.Model): username = models.CharField(max_length=256) password = models.CharField(max_length=64) created_time = models.DateTimeField(default=timezone.now) expired_time = models.DateTimeField()
如果某個用戶某一個時刻對網站進行訪問,我們如何判斷他是否具有VIP權限呢?
通常情況下我們有兩種常見的判斷方法。一是,用戶訪問時,直接從model中取出這個對象,然后和now()
進行比較:
這種情況下,當前時間不管是now()
還是localtime()
都不影響比較的結果,因為兩個datetime對象在比較時會考慮時差。
另一種情況是,通過ORM的queryset進行比較,等于在數據庫層面進行操作:
if models.Account.objects.filter(expired_time__gt=timezone.now()).exists(): # doing sth
Django也幫我們考慮過這種情況,即使此時我們使用本地時間timezone.localtime()
進行查詢,系統也會將其轉換成UTC時間傳入SQL語句:
但是,如果我們使用到了和日期、時間有關的lookups,將產生相反的結果。
怎么理解這個問題呢,我們還是來舉個例子。比如,網站以用戶注冊當天的日子作為“會員日”(比如1月2日注冊的會員,以后每月的2日都是他的會員日),會員日這一天會給這個用戶贈送優(yōu)惠券。
那么,發(fā)送優(yōu)惠券時,我們如何篩選網站內會員日是今日的所有用戶?
下面這個filter是否正確?
models.Account.objects.filter(created_time__day=timezone.now().day).all()
答案是否定的,我們應該使用timezone.localtime()
表示今天,而非timezone.now()
:
models.Account.objects.filter(created_time__day=timezone.localtime().day).all()
這是為什么呢?你不是說數據庫中存儲的都是UTC時間嗎,為何會使用到timezone.localtime()
?
原因是,Django在使用日期、時間有關的lookups時,會在數據庫層面對時間進行時區(qū)的轉換再進行比較,所以我們需要使用本地時間而不是UTC時間。
可以看看原始的SQL語句:
可見,SQL語句中使用了django_datetime_extract('day', "sample_account"."created_time", 'Asia/Shanghai', 'UTC')
將UTC時間轉換成了北京時間,因此后面比較的時候,也應該使用北京時間。
這一點需要格外注意。時間比較的不謹慎,說小點是一個Bug,說大點就是漏洞,畢竟很多涉及到時間比較的情景,都是非常需要嚴謹的。
所以,我們總結一下:
- 任何比較都使用aware時間,不能使用naive時間
- 時間屬性直接比較時,使用任何aware時間均可(會被自動轉換成UTC)
- queryset查詢,不涉及__day、__date、__year等時間lookups時,使用任何aware時間均可(會被自動轉換成UTC)
- queryset查詢,涉及到時間lookups時,使用本地時間
到此這篇關于Django中和時區(qū)相關的安全問題詳解的文章就介紹到這了,更多相關Django時區(qū)安全問題內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python中defaultdict與lambda表達式用法實例小結
這篇文章主要介紹了Python中defaultdict與lambda表達式用法,結合實例形式分析了Python中defaultdict與lambda表達式的功能、使用方法及相關注意事項,需要的朋友可以參考下2018-04-04