鍍金池/ 教程/ PHP/ 編寫安全應(yīng)用
外部服務(wù)認(rèn)證
數(shù)據(jù)庫
引言
模板擴(kuò)展
編寫安全應(yīng)用
部署Tornado
異步Web服務(wù)
表單和模

編寫安全應(yīng)用

很多時(shí)候,安全應(yīng)用是以犧牲復(fù)雜度(以及開發(fā)者的頭痛)為代價(jià)的。Tornado Web服務(wù)器從設(shè)計(jì)之初就在安全方面有了很多考慮,使其能夠更容易地防范那些常見的漏洞。安全cookies防止用戶的本地狀態(tài)被其瀏覽器中的惡意代碼暗中修改。此外,瀏覽器cookies可以與HTTP請求參數(shù)值作比較來防范跨站請求偽造攻擊。在本章中,我們將看到使防范這些漏洞更簡單的Tornado功能,以及使用這些功能的一個(gè)用戶驗(yàn)證示例。

6.1 Cookie漏洞

許多網(wǎng)站使用瀏覽器cookies來存儲(chǔ)瀏覽器會(huì)話間的用戶標(biāo)識(shí)。這是一個(gè)簡單而又被廣泛兼容的方式來存儲(chǔ)跨瀏覽器會(huì)話的持久狀態(tài)。不幸的是,瀏覽器cookies容易受到一些常見的攻擊。本節(jié)將展示Tornado是如何防止一個(gè)惡意腳本來篡改你應(yīng)用存儲(chǔ)的cookies的。

6.1.1 Cookie偽造

有很多方式可以在瀏覽器中截獲cookies。JavaScript和Flash對于它們所執(zhí)行的頁面的域有讀寫cookies的權(quán)限。瀏覽器插件也可由編程方法訪問這些數(shù)據(jù)??缯灸_本攻擊可以利用這些訪問來修改訪客瀏覽器中cookies的值。

6.1.2 安全Cookies

Tornado的安全cookies使用加密簽名來驗(yàn)證cookies的值沒有被服務(wù)器軟件以外的任何人修改過。因?yàn)橐粋€(gè)惡意腳本并不知道安全密鑰,所以它不能在應(yīng)用不知情時(shí)修改cookies。

6.1.2.1 使用安全Cookies

Tornado的setsecurecookie()和getsecurecookie()函數(shù)發(fā)送和取得瀏覽器的cookies,以防范瀏覽器中的惡意修改。為了使用這些函數(shù),你必須在應(yīng)用的構(gòu)造函數(shù)中指定cookie_secret參數(shù)。讓我們來看一個(gè)簡單的例子。

代碼清單6-1中的應(yīng)用將渲染一個(gè)統(tǒng)計(jì)瀏覽器中頁面被加載次數(shù)的頁面。如果沒有設(shè)置cookie(或者cookie已經(jīng)被篡改了),應(yīng)用將設(shè)置一個(gè)值為1的新cookie。否則,應(yīng)用將從cookie中讀到的值加1。

代碼清單6-1 安全Cookie示例:cookie_counter.py

import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.options

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        cookie = self.get_secure_cookie("count")
        count = int(cookie) + 1 if cookie else 1

        countString = "1 time" if count == 1 else "%d times" % count

        self.set_secure_cookie("count", str(count))

        self.write(
            '<html><head><title>Cookie Counter</title></head>'
            '<body><h1>You’ve viewed this page %s times.</h1>' % countString + 
            '</body></html>'
        )

if __name__ == "__main__":
    tornado.options.parse_command_line()

    settings = {
        "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
    }

    application = tornado.web.Application([
        (r'/', MainHandler)
    ], **settings)

    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

如果你檢查瀏覽器中的cookie值,會(huì)發(fā)現(xiàn)count儲(chǔ)存的值類似于MQ==|1310335926|8ef174ecc489ea963c5cdc26ab6d41b49502f2e2。Tornado將cookie值編碼為Base-64字符串,并添加了一個(gè)時(shí)間戳和一個(gè)cookie內(nèi)容的HMAC簽名。如果cookie的時(shí)間戳太舊(或來自未來),或簽名和期望值不匹配,getsecurecookie()函數(shù)會(huì)認(rèn)為cookie已經(jīng)被篡改,并返回None,就好像cookie從沒設(shè)置過一樣。

傳遞給Application構(gòu)造函數(shù)的cookie_secret值應(yīng)該是唯一的隨機(jī)字符串。在Python shell下執(zhí)行下面的代碼片段將產(chǎn)生一個(gè)你自己的值:

>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='

然而,Tornado的安全cookies仍然容易被竊聽。攻擊者可能會(huì)通過腳本或?yàn)g覽器插件截獲cookies,或者干脆竊聽未加密的網(wǎng)絡(luò)數(shù)據(jù)。記住cookie值是簽名的而不是加密的。惡意程序能夠讀取已存儲(chǔ)的cookies,并且可以傳輸他們的數(shù)據(jù)到任意服務(wù)器,或者通過發(fā)送沒有修改的數(shù)據(jù)給應(yīng)用偽造請求。因此,避免在瀏覽器cookie中存儲(chǔ)敏感的用戶數(shù)據(jù)是非常重要的。

我們還需要注意用戶可能修改他自己的cookies的可能性,這會(huì)導(dǎo)致提權(quán)攻擊。比如,如果我們在cookie中存儲(chǔ)了用戶已付費(fèi)的文章剩余的瀏覽數(shù),我們希望防止用戶自己更新其中的數(shù)值來獲取免費(fèi)的內(nèi)容。httponly和secure屬性可以幫助我們防范這種攻擊。

6.1.2.2 HTTP-Only和SSL Cookies

Tornado的cookie功能依附于Python內(nèi)建的Cookie模塊。因此,我們可以利用它所提供的一些安全功能。這些安全屬性是HTTP cookie規(guī)范的一部分,并在它可能是如何暴露其值給它連接的服務(wù)器和它運(yùn)行的腳本方面給予瀏覽器指導(dǎo)。比如,我們可以通過只允許SSL連接的方式減少cookie值在網(wǎng)絡(luò)中被截獲的可能性。我們也可以讓瀏覽器對JavaScript隱藏cookie值。

為cookie設(shè)置secure屬性來指示瀏覽器只通過SSL連接傳遞cookie。(這可能會(huì)產(chǎn)生一些困擾,但這不是Tornado的安全cookies,更精確的說那種方法應(yīng)該被稱為簽名cookies。)從Python 2.6版本開始,Cookie對象還提供了一個(gè)httponly屬性。包括這個(gè)屬性指示瀏覽器對于JavaScript不可訪問cookie,這可以防范來自讀取cookie值的跨站腳本攻擊。

為了開啟這些功能,你可以向set_cookie和set_secure_cookie方法傳遞關(guān)鍵字參數(shù)。比如,一個(gè)安全的HTTP-only cookie(不是Tornado的簽名cookie)可以調(diào)用self.set_cookie('foo', 'bar', httponly=True, secure=True)發(fā)送。

既然我們已經(jīng)探討了一些保護(hù)存儲(chǔ)在cookies中的持久數(shù)據(jù)的策略,下面讓我們看看另一種常見的攻擊載體。下一節(jié)我們將看到一種防范向你的應(yīng)用發(fā)送偽造請求的惡意網(wǎng)站。

6.2 請求漏洞

任何Web應(yīng)用所面臨的一個(gè)主要安全漏洞是跨站請求偽造,通常被簡寫為CSRF或XSRF,發(fā)音為"sea surf"。這個(gè)漏洞利用了瀏覽器的一個(gè)允許惡意攻擊者在受害者網(wǎng)站注入腳本使未授權(quán)請求代表一個(gè)已登錄用戶的安全漏洞。讓我們看一個(gè)例子。

6.2.1 剖析一個(gè)XSRF

假設(shè)Alice是Burt's Books的一個(gè)普通顧客。當(dāng)她在這個(gè)在線商店登錄帳號(hào)后,網(wǎng)站使用一個(gè)瀏覽器cookie標(biāo)識(shí)她。現(xiàn)在假設(shè)一個(gè)不擇手段的作者,Melvin,想增加他圖書的銷量。在一個(gè)Alice經(jīng)常訪問的Web論壇中,他發(fā)表了一個(gè)帶有HTML圖像標(biāo)簽的條目,其源碼初始化為在線商店購物的URL。比如:

<img src="http://store.burts-books.com/purchase?title=Melvins+Web+Sploitz" />

Alice的瀏覽器嘗試獲取這個(gè)圖像資源,并且在請求中包含一個(gè)合法的cookies,并不知道取代小貓照片的是在線商店的購物URL。

6.2.2 防范請求偽造

有很多預(yù)防措施可以防止這種類型的攻擊。首先你在開發(fā)應(yīng)用時(shí)需要深謀遠(yuǎn)慮。任何會(huì)產(chǎn)生副作用的HTTP請求,比如點(diǎn)擊購買按鈕、編輯賬戶設(shè)置、改變密碼或刪除文檔,都應(yīng)該使用HTTP POST方法。無論如何,這是良好的RESTful做法,但它也有額外的優(yōu)勢用于防范像我們剛才看到的惡意圖像那樣瑣碎的XSRF攻擊。但是,這并不足夠:一個(gè)惡意站點(diǎn)可能會(huì)通過其他手段,如HTML表單或XMLHTTPRequest API來向你的應(yīng)用發(fā)送POST請求。保護(hù)POST請求需要額外的策略。

為了防范偽造POST請求,我們會(huì)要求每個(gè)請求包括一個(gè)參數(shù)值作為令牌來匹配存儲(chǔ)在cookie中的對應(yīng)值。我們的應(yīng)用將通過一個(gè)cookie頭和一個(gè)隱藏的HTML表單元素向頁面提供令牌。當(dāng)一個(gè)合法頁面的表單被提交時(shí),它將包括表單值和已存儲(chǔ)的cookie。如果兩者匹配,我們的應(yīng)用認(rèn)定請求有效。

由于第三方站點(diǎn)沒有訪問cookie數(shù)據(jù)的權(quán)限,他們將不能在請求中包含令牌cookie。這有效地防止了不可信網(wǎng)站發(fā)送未授權(quán)的請求。正如我們看到的,Tornado同樣會(huì)讓這個(gè)實(shí)現(xiàn)變得簡單。

6.2.3 使用Tornado的XSRF保護(hù)

你可以通過在應(yīng)用的構(gòu)造函數(shù)中包含xsrf_cookies參數(shù)來開啟XSRF保護(hù):

settings = {
    "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
    "xsrf_cookies": True
}
application = tornado.web.Application([
    (r'/', MainHandler),
    (r'/purchase', PurchaseHandler),
], **settings)

當(dāng)這個(gè)應(yīng)用標(biāo)識(shí)被設(shè)置時(shí),Tornado將拒絕請求參數(shù)中不包含正確的xsrf值的POST、PUT和DELETE請求。Tornado將會(huì)在幕后處理xsrf cookies,但你必須在你的HTML表單中包含XSRF令牌以確保授權(quán)合法請求。要做到這一點(diǎn),只需要在你的模板中包含一個(gè)xsrfformhtml調(diào)用即可:

<form action="/purchase" method="POST">
    {% raw xsrf_form_html() %}
    <input type="text" name="title" />
    <input type="text" name="quantity" />
    <input type="submit" value="Check Out" />
</form>

6.2.3.1 XSRF令牌和AJAX請求

AJAX請求也需要一個(gè)xsrf參數(shù),但不是必須顯式地在渲染頁面時(shí)包含一個(gè)xsrf值,而是通過腳本在客戶端查詢?yōu)g覽器獲得cookie值。下面的兩個(gè)函數(shù)透明地添加令牌值給AJAX POST請求。第一個(gè)函數(shù)通過名字獲取cookie,而第二個(gè)函數(shù)是一個(gè)添加_xsrf參數(shù)到傳遞給postJSON函數(shù)數(shù)據(jù)對象的便捷函數(shù)。

function getCookie(name) {
    var c = document.cookie.match("\\b" + name + "=([^;]*)\\b");
    return c ? c[1] : undefined;
}

jQuery.postJSON = function(url, data, callback) {
    data._xsrf = getCookie("_xsrf");
    jQuery.ajax({
        url: url,
        data: jQuery.param(data),
        dataType: "json",
        type: "POST",
        success: callback
    });
}

這些預(yù)防措施需要思考很多,而Tornado的安全cookies支持和XSRF保護(hù)減輕了應(yīng)用開發(fā)者的一些負(fù)擔(dān)??梢钥隙ǖ氖?,內(nèi)建的安全功能也非常有用,但在思考你應(yīng)用的安全性方面需要時(shí)刻保持警惕。有很多在線Web應(yīng)用安全文獻(xiàn),其中一個(gè)更全面的實(shí)踐對策集合是Mozilla的安全編程指南。

6.3 用戶驗(yàn)證

既然我們已經(jīng)看到了如何安全地設(shè)置和取得cookies,并理解了XSRF攻擊背后的原理,現(xiàn)在就讓我們看一個(gè)簡單用戶驗(yàn)證系統(tǒng)的演示示例。在本節(jié)中,我們將建立一個(gè)應(yīng)用,詢問訪客的名字,然后將其存儲(chǔ)在安全cookie中,以便之后取出。后續(xù)的請求將認(rèn)出回客,并展示給她一個(gè)定制的頁面。你將學(xué)到login_url參數(shù)和tornado.web.authenticated裝飾器的相關(guān)知識(shí),這將消除在類似應(yīng)用中經(jīng)常會(huì)涉及到的一些頭疼的問題。

6.3.1 示例:歡迎回來

在這個(gè)例子中,我們將只通過存儲(chǔ)在安全cookie里的用戶名標(biāo)識(shí)一個(gè)人。當(dāng)某人首次在某個(gè)瀏覽器(或cookie過期后)訪問我們的頁面時(shí),我們展示一個(gè)登錄表單頁面。表單作為到LoginHandler路由的POST請求被提交。post方法的主體調(diào)用setsecurecookie()來存儲(chǔ)username請求參數(shù)中提交的值。

代碼清單6-2中的Tornado應(yīng)用展示了我們本節(jié)要討論的驗(yàn)證函數(shù)。LoginHandler類渲染登錄表單并設(shè)置cookie,而LogoutHandler類刪除cookie。

代碼清單6-2 驗(yàn)證訪客:cookies.py

import tornado.httpserver
import tornado.ioloop
import tornado.web
import tornado.options
import os.path

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("username")

class LoginHandler(BaseHandler):
    def get(self):
        self.render('login.html')

    def post(self):
        self.set_secure_cookie("username", self.get_argument("username"))
        self.redirect("/")

class WelcomeHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        self.render('index.html', user=self.current_user)

class LogoutHandler(BaseHandler):
    def get(self):
    if (self.get_argument("logout", None)):
        self.clear_cookie("username")
        self.redirect("/")

if __name__ == "__main__":
    tornado.options.parse_command_line()

    settings = {
        "template_path": os.path.join(os.path.dirname(__file__), "templates"),
        "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
        "xsrf_cookies": True,
        "login_url": "/login"
    }

    application = tornado.web.Application([
        (r'/', WelcomeHandler),
        (r'/login', LoginHandler),
        (r'/logout', LogoutHandler)
    ], **settings)

    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

代碼清單6-3和6-4是應(yīng)用templates/目錄下的文件。

代碼清單6-3 登錄表單:login.html

<html>
    <head>
        <title>Please Log In</title>
    </head>

    <body>
        <form action="/login" method="POST">
            {% raw xsrf_form_html() %}
            Username: <input type="text" name="username" />
            <input type="submit" value="Log In" />
        </form>
    </body>
</html>

代碼清單6-4 歡迎回客:index.html

<html>
    <head>
        <title>Welcome Back!</title>
    </head>
    <body>
        <h1>Welcome back, {{ user }}</h1>
    </body>
</html>

6.3.2 authenticated裝飾器

為了使用Tornado的認(rèn)證功能,我們需要對登錄用戶標(biāo)記具體的處理函數(shù)。我們可以使用@tornado.web.authenticated裝飾器完成它。當(dāng)我們使用這個(gè)裝飾器包裹一個(gè)處理方法時(shí),Tornado將確保這個(gè)方法的主體只有在合法的用戶被發(fā)現(xiàn)時(shí)才會(huì)調(diào)用。讓我們看看例子中的WelcomeHandler吧,這個(gè)類只對已登錄用戶渲染index.html模板。

class WelcomeHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        self.render('index.html', user=self.current_user)

在get方法被調(diào)用之前,authenticated裝飾器確保currentusr屬性有值。(我們將簡短的討論這個(gè)屬性。)如果currentuser值為假(None、False、0、""),任何GET或HEAD請求都將把訪客重定向到應(yīng)用設(shè)置中l(wèi)ogin_url指定的URL。此外,非法用戶的POST請求將返回一個(gè)帶有403(Forbidden)狀態(tài)的HTTP響應(yīng)。

如果發(fā)現(xiàn)了一個(gè)合法的用戶,Tornado將如期調(diào)用處理方法。為了實(shí)現(xiàn)完整功能,authenticated裝飾器依賴于currentuser屬性和loginurl設(shè)置,我們將在下面看到具體講解。

6.3.2.1 current_user屬性

請求處理類有一個(gè)currentuser屬性(同樣也在處理程序渲染的任何模板中可用)可以用來存儲(chǔ)為當(dāng)前請求進(jìn)行用戶驗(yàn)證的標(biāo)識(shí)。其默認(rèn)值為None。為了authenticated裝飾器能夠成功標(biāo)識(shí)一個(gè)已認(rèn)證用戶,你必須覆寫請求處理程序中默認(rèn)的getcurrent_user()方法來返回當(dāng)前用戶。

實(shí)際的實(shí)現(xiàn)由你決定,不過在這個(gè)例子中,我們只是從安全cookie中取出訪客的姓名。很明顯,你希望使用一個(gè)更加魯棒的技術(shù),但是出于演示的目的,我們將使用下面的方法:

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("username")

盡管這里討論的例子并沒有在存儲(chǔ)和取出用戶密碼或其他憑證上有所深入,但本章中討論的技術(shù)可以以最小的額外努力來擴(kuò)展到查詢數(shù)據(jù)庫中的認(rèn)證。

6.3.2.2 login_url設(shè)置

讓我們簡單看看應(yīng)用的構(gòu)造函數(shù)。記住這里我們傳遞了一個(gè)新的設(shè)置給應(yīng)用:loginurl是應(yīng)用登錄表單的地址。如果getcurrent_user方法返回了一個(gè)假值,帶有authenticated裝飾器的處理程序?qū)⒅囟ㄏ驗(yàn)g覽器的URL以便登錄。

settings = {
    "template_path": os.path.join(os.path.dirname(__file__), "templates"),
    "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
    "xsrf_cookies": True,
    "login_url": "/login"
}
application = tornado.web.Application([
    (r'/', WelcomeHandler),
    (r'/login', LoginHandler),
    (r'/logout', LogoutHandler)
], **settings)

當(dāng)Tornado構(gòu)建重定向URL時(shí),它還會(huì)給查詢字符串添加一個(gè)next參數(shù),其中包含了發(fā)起重定向到登錄頁面的URL資源地址。你可以使用像self.redirect(self.get_argument('next', '/'))這樣的行來重定向登錄后用戶回到的頁面。

6.4 總結(jié)

我們在本章中看到了兩種幫助你的Tornado應(yīng)用安全的技術(shù),以及一個(gè)如何使用<var>@tornado.web.authenticated</var>實(shí)現(xiàn)用戶認(rèn)證的例子。在外部服務(wù)認(rèn)證,我們將看到在那些像Facebook和Twitter一樣需要外部Web服務(wù)認(rèn)證的應(yīng)用中如何擴(kuò)展我們這里談?wù)摰母拍睢?/p>