Python Web服務(wù)器Tornado使用小結(jié)
首先想說(shuō)的是它的安全性,這方面確實(shí)能讓我感受到它的良苦用心。這主要可以分為兩點(diǎn):
一、防范跨站偽造請(qǐng)求(Cross-site request forgery,簡(jiǎn)稱 CSRF 或 XSRF)
CSRF 的意思簡(jiǎn)單來(lái)說(shuō)就是,攻擊者偽造真實(shí)用戶來(lái)發(fā)送請(qǐng)求。
舉例來(lái)說(shuō),假設(shè)某個(gè)銀行網(wǎng)站有這樣的 URL:
http://bank.example.com/withdraw?amount=1000000&for=Eve
當(dāng)這個(gè)銀行網(wǎng)站的用戶訪問(wèn)該 URL 時(shí),就會(huì)給 Eve 這名用戶一百萬(wàn)元。用戶當(dāng)然不會(huì)輕易地點(diǎn)擊這個(gè) URL,但是攻擊者可以在其他網(wǎng)站上嵌入一張偽造的圖片,將圖片地址設(shè)為該 URL:
<img src="http://bank.example.com/withdraw?amount=1000000&for=Eve">
那么當(dāng)用戶訪問(wèn)那個(gè)惡意網(wǎng)站時(shí),瀏覽器就會(huì)對(duì)該 URL 發(fā)起一個(gè) GET 請(qǐng)求,于是在用戶毫不知情的情況下,一百萬(wàn)就被轉(zhuǎn)走了。
要防范上述攻擊很簡(jiǎn)單,不允許通過(guò) GET 請(qǐng)求來(lái)執(zhí)行更改操作(例如轉(zhuǎn)賬)即可。不過(guò)其他類型的請(qǐng)求照樣也不安全,假如攻擊者構(gòu)造這樣一個(gè)表單:
<p>轉(zhuǎn)發(fā)抽獎(jiǎng)送 iPad 啊!</p>
<input type="hidden" name="amount" value="1000000">
<input type="hidden" name="for" value="Eve">
<input type="submit" value="轉(zhuǎn)發(fā)">
</form>
不明真相的用戶點(diǎn)了下“轉(zhuǎn)發(fā)”按鈕,結(jié)果錢(qián)就被轉(zhuǎn)走了…
要杜絕這種情況,就需要在非 GET 請(qǐng)求時(shí)添加一個(gè)攻擊者無(wú)法偽造的字段,處理請(qǐng)求時(shí)驗(yàn)證這個(gè)字段是否修改過(guò)。
Tornado 的處理方法很簡(jiǎn)單,在請(qǐng)求中增加了一個(gè)隨機(jī)生成的 _xsrf 字段,并且 cookie 中也增加這個(gè)字段,在接收請(qǐng)求時(shí),比較這 2 個(gè)字段的值。
由于非本站的網(wǎng)頁(yè)是不能獲取或修改 cookie 的,這就保證了 _xsrf 無(wú)法被第三方網(wǎng)站偽造(HTTP 嗅探例外)。
當(dāng)然,用戶自己是可以隨意獲取和修改 cookie 的,不過(guò)這已經(jīng)不屬于 CSRF 的范疇了:用戶自己偽造自己所做的事情,當(dāng)然由他自己來(lái)承擔(dān)。
要使用該功能的話,需要在生成 tornado.web.Application 對(duì)象時(shí),加上 xsrf_cookies=True 參數(shù),這會(huì)給用戶生成一個(gè)名為 _xsrf 的 cookie 字段。
此外還需要你在非 GET 請(qǐng)求的表單里加上 xsrf_form_html(),如果不用 Tornado 的模板的話,在 tornado.web.RequestHandler 內(nèi)部可以用 self.xsrf_form_html() 來(lái)生成。
對(duì)于 AJAX 請(qǐng)求來(lái)說(shuō),基本上是不需要擔(dān)心跨站的,所以 Tornado 1.1.1 以前的版本并不對(duì)帶有 X-Requested-With: XMLHTTPRequest 的請(qǐng)求做驗(yàn)證。
后來(lái) Google 的工程師指出,惡意的瀏覽器插件可以偽造跨域 AJAX 請(qǐng)求,所以也應(yīng)該進(jìn)行驗(yàn)證。對(duì)此我不置可否,因?yàn)闉g覽器插件的權(quán)限可以非常大,偽造 cookie 或是直接提交表單都行。
不過(guò)解決辦法仍然要說(shuō),其實(shí)只要從 cookie 中獲取 _xsrf 字段,然后在 AJAX 請(qǐng)求時(shí)加上這個(gè)參數(shù),或者放在 X-Xsrftoken 或 X-Csrftoken 請(qǐng)求頭里即可。嫌麻煩的話,可以用 jQuery 的 $.ajaxSetup() 來(lái)處理:
$.ajaxSetup({
beforeSend: function(jqXHR, settings) {
type = settings.type
if (type != 'GET' && type != 'HEAD' && type != 'OPTIONS') {
var pattern = /(.+; *)?_xsrf *= *([^;" ]+)/;
var xsrf = pattern.exec(document.cookie);
if (xsrf) {
jqXHR.setRequestHeader('X-Xsrftoken', xsrf[2]);
}
}
}});
此外再順便談?wù)効缯灸_本(Cross-site scripting,簡(jiǎn)稱 XSS)。和 CSRF 相反的是,XSS 是利用被攻擊網(wǎng)站自身的漏洞,在該網(wǎng)站上注入攻擊者想執(zhí)行的腳本代碼,讓瀏覽該網(wǎng)站的用戶執(zhí)行。
不過(guò)只要不讓用戶隨意輸入 HTML(例如對(duì) < 和 > 進(jìn)行轉(zhuǎn)義),對(duì) HTML 元素的屬性做驗(yàn)證(例如屬性里的引號(hào)要轉(zhuǎn)義,src 和 事件處理等屬性不能隨意填寫(xiě) JavaScript 代碼等),并檢查 CSS(含 style 屬性)中的 expression 即可避免。
二、防止偽造 cookie。
前面提到的 CSRF 和 XSS 都是攻擊者在用戶不知情的情況下,冒用他的名義來(lái)進(jìn)行操作;而偽造 cookie 則是攻擊者自己主動(dòng)偽造其他用戶來(lái)進(jìn)行操作。
舉例來(lái)說(shuō),假設(shè)網(wǎng)站的登錄驗(yàn)證就是檢查 cookie 中的用戶名,只要符合的話,就認(rèn)為該用戶已登錄。那么攻擊者只要在 cookie 中設(shè)置 username=admin 之類的值,就可以冒充管理員來(lái)操作了。
要防止 cookie 被偽造,首先需要提到設(shè)置 cookie 時(shí)的兩個(gè)參數(shù):secure 和 httponly。這兩個(gè)參數(shù)并不在 tornado.web.RequestHandler.set_cookie() 的參數(shù)列表里,而是作為關(guān)鍵字參數(shù)傳遞,并在 Cookie.Morsel._reserved 中定義的。
前者是指這個(gè) cookie 只能通過(guò)安全連接傳遞(即 HTTPS),這就使得嗅探者無(wú)法截獲該 cookie;后者則要求其只能在 HTTP 協(xié)議下訪問(wèn)(即無(wú)法通過(guò) JavaScript 來(lái)獲取 document.cookie 中的該字段,并且設(shè)置后也不會(huì)通過(guò) HTTP 協(xié)議向服務(wù)器發(fā)送),這便使得攻擊者無(wú)法簡(jiǎn)單地通過(guò) JavaScript 腳本來(lái)偽造 cookie。
不過(guò)對(duì)于惡意的攻擊者,這兩個(gè)參數(shù)并不能杜絕 cookie 被偽造。為此就需要對(duì) cookie 做個(gè)簽名,一旦被修改,服務(wù)器端可以判斷出來(lái)。
Tornado 中提供了 set_secure_cookie() 這個(gè)方法來(lái)對(duì) cookie 做簽名。簽名時(shí)需要提供一串秘鑰(生成 tornado.web.Application 對(duì)象時(shí)的 cookie_secret 參數(shù)),這個(gè)秘鑰可以通過(guò)如下代碼來(lái)生成:
base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
這個(gè)參數(shù)可以隨機(jī)生成,但如果同時(shí)有多個(gè) Tornado 進(jìn)程來(lái)服務(wù)的話,或者有時(shí)會(huì)重啟的話,還是共用一個(gè)常量比較好,并且注意不要泄露。
這個(gè)簽名用的是 HMAC 算法,hash 算法采用的是 SHA1。簡(jiǎn)單來(lái)說(shuō)就是把 cookie 名、值和時(shí)間戳的 hash 作為簽名,再把“值|時(shí)間戳|簽名”作為新的值。這樣服務(wù)器端只要拿秘鑰再次加密,比較簽名是否有變化過(guò)即可判斷真?zhèn)巍?BR>值得一提的是讀源碼時(shí)還發(fā)現(xiàn)這樣一個(gè)函數(shù):
def _time_independent_equals(a, b):
if len(a) != len(b):
return False
result = 0
if type(a[0]) is int: # python3 byte strings
for x, y in zip(a, b):
result |= x ^ y
else: # python2
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0
讀了半天也沒(méi)發(fā)現(xiàn)和普通的字符串比較有什么優(yōu)點(diǎn),直到看了 StackOverflow 上的答案才知道:為了避免攻擊者通過(guò)測(cè)試比較時(shí)間來(lái)判斷正確的位數(shù),這個(gè)函數(shù)讓比較的時(shí)間比較恒定,也就杜絕了這種情況。(話說(shuō)這答案看得我各種佩服啊,搞安全的專家果然不是我那么膚淺的…)
三、接著是繼承 tornado.web.RequestHandler。
在執(zhí)行流程上,tornado.web.Application 會(huì)根據(jù) URL 尋找一個(gè)匹配的 RequestHandler 類,并初始化它。它的 __init__() 方法會(huì)調(diào)用 initialize() 方法,所以只要覆蓋后者即可,并且不需要調(diào)用父類的 initialize()。
接著根據(jù)不同的 HTTP 方法尋找該 handler 的 get/post() 等方法,并在執(zhí)行前運(yùn)行 prepare()。這些方法都不會(huì)主動(dòng)調(diào)用父類的,因此有需要時(shí),自行調(diào)用吧。
最后會(huì)調(diào)用 handler 的 finish() 方法,這個(gè)方法最好別覆蓋。它會(huì)調(diào)用 on_finish() 方法,它可以被覆蓋,用于處理一些善后的事情(例如關(guān)閉數(shù)據(jù)庫(kù)連接),但不能再向?yàn)g覽器發(fā)送數(shù)據(jù)了(因?yàn)?HTTP 響應(yīng)已發(fā)送,連接也可能已被關(guān)閉)。
順便說(shuō)下怎么處理錯(cuò)誤頁(yè)面。
簡(jiǎn)單來(lái)說(shuō),執(zhí)行 RequestHandler 的 _execute() 方法(內(nèi)部依次執(zhí)行 prepare()、get() 和 finish() 等方法)時(shí),任何未捕捉的錯(cuò)誤都會(huì)被它的 write_error() 方法捕捉,因此覆蓋這個(gè)方法即可:
def write_error(self, status_code, **kwargs):
if status_code == 404:
self.render('404.html')
elif status_code == 500:
self.render('500.html')
else:
super(RequestHandler, self).write_error(status_code, **kwargs)
由于歷史原因,你也可以覆蓋 get_error_html() 方法,不過(guò)不被推薦。
此外,你還可能沒(méi)到 _execute() 方法就出錯(cuò)了。
例如 initialize() 方法拋出了一個(gè)未捕捉的異常,這個(gè)異常會(huì)被 IOStream 捕捉到,然后直接關(guān)閉連接,不能向用戶輸出任何錯(cuò)誤頁(yè)面。
再比如沒(méi)有找到一個(gè)能處理該請(qǐng)求的 handler,就會(huì)用 tornado.web.ErrorHandler 去處理 404 錯(cuò)誤。這種情況可以替換這個(gè)類來(lái)實(shí)現(xiàn)自定義錯(cuò)誤頁(yè)面:
def get(self):
raise tornado.web.HTTPError(404)
tornado.web.ErrorHandler = PageNotFoundHandler
另一種方法就是在 Application 的 handlers 參數(shù)的最后,加上一個(gè)能捕捉任何 URL 的 handler:
# ...
('.*', PageNotFoundHandler)
])
四、接著說(shuō)說(shuō)處理登錄。
Tornado 提供了 @tornado.web.authenticated 這個(gè)裝飾器,在 handler 的 get() 等方法前加上即可。
它會(huì)依賴三處代碼:
需要定義 handler 的 get_current_user() 方法,例如:
return self.get_secure_cookie('user_id', 0)
它的返回值為假時(shí),就會(huì)跳轉(zhuǎn)到登錄頁(yè)面了。
創(chuàng)建 application 時(shí)設(shè)置 login_url 參數(shù):
[
# ...
],
login_url = '/login'
)
定義 handler 的 get_login_url() 方法。
如果不能使用默認(rèn)的 login_url 參數(shù)(例如普通用戶和管理員需要不同的登錄地址),那么可以覆蓋 get_login_url() 方法:
def get_login_url(self):
return '/admin/login'
順帶一提,跳轉(zhuǎn)到登錄頁(yè)后時(shí)會(huì)附帶一個(gè) next 參數(shù),指向登錄前訪問(wèn)的網(wǎng)址。為達(dá)到更好的用戶體驗(yàn),需要在登錄后跳轉(zhuǎn)到該網(wǎng)址:
def get(self):
if self.get_current_user():
self.redirect('/')
return
self.render('login.html')
def post(self):
if self.get_current_user():
raise tornado.web.HTTPError(403)
# check username and password
if success:
self.redirect(self.get_argument('next', '/'))
此外,我很多地方都使用了 AJAX 技術(shù),而前端懶得去處理 403 錯(cuò)誤,所以我只能改造一下 authenticated() 了:
"""Decorate methods with this to require that the user be logged in."""
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
if not self.current_user:
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest': # jQuery 等庫(kù)會(huì)附帶這個(gè)頭
self.set_header('Content-Type', 'application/json; charset=UTF-8')
self.write(json.dumps({'success': False, 'msg': u'您的會(huì)話已過(guò)期,請(qǐng)重新登錄!'}))
return
if self.request.method in ("GET", "HEAD"):
url = self.get_login_url()
if "?" not in url:
if urlparse.urlsplit(url).scheme:
# if login url is absolute, make next absolute too
next_url = self.request.full_url()
else:
next_url = self.request.uri
url += "?" + urllib.urlencode(dict(next=next_url))
self.redirect(url)
return
raise tornado.web.HTTPError(403)
return method(self, *args, **kwargs)
return wrapper
五、然后說(shuō)下獲取用戶的 IP 地址。
簡(jiǎn)單來(lái)說(shuō),在 handler 的方法里用 self.request.remote_ip 就能拿到了。
不過(guò)如果使用了反向代理,拿到的就是代理的 IP 了,這時(shí)候就需要在創(chuàng)建 HTTPServer 時(shí)增加 xheaders 的設(shè)置了:
from tornado.httpserver import HTTPServer
from tornado.netutil import bind_sockets
sockets = bind_sockets(80)
server = HTTPServer(application, xheaders=True)
server.add_sockets(sockets)
tornado.ioloop.IOLoop.instance().start()
此外,我只需要處理 IPv4,但本地測(cè)試時(shí)會(huì)拿到 ::1 這種 IPv6 地址,所以還需要設(shè)置一下:
import socket
sockets = bind_sockets(80, family=socket.AF_INET)
else:
sockets = bind_sockets(80)
六、最后再提下生產(chǎn)環(huán)境下如何提高性能。
Tornado 可以在 HTTPServer 調(diào)用 add_sockets() 前創(chuàng)建多個(gè)子進(jìn)程,利用多 CPU 的優(yōu)勢(shì)來(lái)處理并發(fā)請(qǐng)求。
簡(jiǎn)單來(lái)說(shuō),代碼如下:
if settings.IPV4_ONLY:
import socket
sockets = bind_sockets(80, family=socket.AF_INET)
else:
sockets = bind_sockets(80)
if not settings.DEBUG_MODE:
import tornado.process
tornado.process.fork_processes(0) # 0 表示按 CPU 數(shù)目創(chuàng)建相應(yīng)數(shù)目的子進(jìn)程
server = HTTPServer(application, xheaders=True)
server.add_sockets(sockets)
tornado.ioloop.IOLoop.instance().start()
注意這種方式下不能啟用 autoreload 功能(application 在創(chuàng)建時(shí),debug 參數(shù)不能為真)。
- 高性能web服務(wù)器框架Tornado簡(jiǎn)單實(shí)現(xiàn)restful接口及開(kāi)發(fā)實(shí)例
- Python Web框架Tornado運(yùn)行和部署
- Tornado Web服務(wù)器多進(jìn)程啟動(dòng)的2個(gè)方法
- python常用web框架簡(jiǎn)單性能測(cè)試結(jié)果分享(包含django、flask、bottle、tornado)
- 使用Python的Tornado框架實(shí)現(xiàn)一個(gè)Web端圖書(shū)展示頁(yè)面
- 使用Python的Tornado框架實(shí)現(xiàn)一個(gè)簡(jiǎn)單的WebQQ機(jī)器人
- Web服務(wù)器框架 Tornado簡(jiǎn)介
- Python tornado隊(duì)列示例-一個(gè)并發(fā)web爬蟲(chóng)代碼分享
- Tornado Web Server框架編寫(xiě)簡(jiǎn)易Python服務(wù)器
- Python Tornado框架輕松寫(xiě)一個(gè)Web應(yīng)用的全過(guò)程
相關(guān)文章
python polars數(shù)據(jù)科學(xué)庫(kù)對(duì)比Pandas優(yōu)勢(shì)分析
這篇文章主要為大家介紹了python polars數(shù)據(jù)科學(xué)庫(kù)對(duì)比Pandas優(yōu)勢(shì)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Linux上使用Python統(tǒng)計(jì)每天的鍵盤(pán)輸入次數(shù)
這篇文章主要介紹了Linux上使用Python統(tǒng)計(jì)每天的鍵盤(pán)輸入次數(shù),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04OpenCV實(shí)戰(zhàn)記錄之基于分水嶺算法的圖像分割
在機(jī)器視覺(jué)中,有時(shí)需要對(duì)產(chǎn)品進(jìn)行檢測(cè)和計(jì)數(shù),其難點(diǎn)無(wú)非是對(duì)于產(chǎn)品的圖像分割,這篇文章主要給大家介紹了關(guān)于OpenCV實(shí)戰(zhàn)記錄之基于分水嶺算法的圖像分割的相關(guān)資料,需要的朋友可以參考下2023-02-02Django用戶認(rèn)證系統(tǒng) 組與權(quán)限解析
這篇文章主要介紹了Django用戶認(rèn)證系統(tǒng) 組與權(quán)限解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08python 在服務(wù)器上調(diào)用數(shù)據(jù)庫(kù)特別慢的解決過(guò)程
這篇文章主要介紹了python 在服務(wù)器上調(diào)用數(shù)據(jù)庫(kù)特別慢的解決過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04Python高階函數(shù)、常用內(nèi)置函數(shù)用法實(shí)例分析
這篇文章主要介紹了Python高階函數(shù)、常用內(nèi)置函數(shù)用法,結(jié)合實(shí)例形式分析了Python高階函數(shù)與常用內(nèi)置函數(shù)相關(guān)功能、原理、使用技巧與操作注意事項(xiàng),需要的朋友可以參考下2019-12-12