驗證與安全性

Cookies 與簽名 Cookies

您可以使用 set_cookie 方法在使用者瀏覽器中設定 cookies。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_cookie("mycookie"):
            self.set_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Cookies 並不安全,很容易被客戶端修改。如果您需要設定 cookies 來識別目前已登入的使用者,您需要簽署您的 cookies 以防止偽造。Tornado 使用 set_signed_cookieget_signed_cookie 方法支援簽名 cookies。若要使用這些方法,您需要在建立應用程式時指定一個名為 cookie_secret 的密鑰。您可以將應用程式設定作為關鍵字引數傳遞到您的應用程式。

application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

簽名 cookies 除了包含 cookie 的編碼值之外,還包含時間戳記和 HMAC 簽名。如果 cookie 過期或簽名不匹配,get_signed_cookie 將會返回 None,就像 cookie 未設定一樣。上面的範例的安全版本如下:

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_signed_cookie("mycookie"):
            self.set_signed_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Tornado 的簽名 cookies 保證完整性,但不保證機密性。也就是說,cookie 無法被修改,但其內容可以被使用者看到。cookie_secret 是一個對稱密鑰,必須保密 – 任何取得此密鑰值的人都可以產生自己的簽名 cookies。

預設情況下,Tornado 的簽名 cookies 在 30 天後過期。若要變更此設定,請使用 set_signed_cookieexpires_days 關鍵字引數 get_signed_cookiemax_age_days 引數。這兩個值會分開傳遞,因此您可以例如讓 cookie 在大多數情況下有效 30 天,但在某些敏感操作(例如變更帳單資訊)時,讀取 cookie 時使用較小的 max_age_days

Tornado 還支援多個簽名密鑰,以啟用簽名密鑰輪換。cookie_secret 必須是一個字典,其中整數密鑰版本作為鍵,而對應的密鑰作為值。目前使用的簽名密鑰必須設定為 key_version 應用程式設定,但字典中的所有其他密鑰都允許用於 cookie 簽名驗證(如果在 cookie 中設定了正確的密鑰版本)。若要實作 cookie 更新,可以使用 get_signed_cookie_key_version 查詢目前的簽名密鑰版本。

使用者驗證

目前已驗證的使用者可在每個請求處理器中以 self.current_user 取得,並在每個樣板中以 current_user 取得。預設情況下,current_userNone

若要在您的應用程式中實作使用者驗證,您需要在請求處理器中覆寫 get_current_user() 方法,以便根據例如 cookie 的值來判斷目前使用者。以下範例讓使用者只需指定暱稱即可登入應用程式,然後將暱稱儲存到 cookie 中。

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_signed_cookie("user")

class MainHandler(BaseHandler):
    def get(self):
        if not self.current_user:
            self.redirect("/login")
            return
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

class LoginHandler(BaseHandler):
    def get(self):
        self.write('<html><body><form action="/login" method="post">'
                   'Name: <input type="text" name="name">'
                   '<input type="submit" value="Sign in">'
                   '</form></body></html>')

    def post(self):
        self.set_signed_cookie("user", self.get_argument("name"))
        self.redirect("/")

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

您可以使用 Python 裝飾器 tornado.web.authenticated 要求使用者必須登入。如果請求傳送到具有此裝飾器的方法,且使用者尚未登入,則會將使用者重新導向至 login_url(另一個應用程式設定)。上面的範例可以改寫如下:

class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

settings = {
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

如果您使用 authenticated 裝飾器裝飾 post() 方法,且使用者尚未登入,伺服器會傳送 403 回應。@authenticated 裝飾器只是 if not self.current_user: self.redirect() 的簡寫,可能不適用於非基於瀏覽器的登入方案。

請查看 Tornado 部落格範例應用程式,以取得使用驗證(並將使用者資料儲存在 PostgreSQL 資料庫中)的完整範例。

第三方驗證

tornado.auth 模組實作了網路上一些最熱門網站的驗證和授權協定,包括 Google/Gmail、Facebook、Twitter 和 FriendFeed。此模組包含透過這些網站讓使用者登入的方法,以及在適用的情況下,授權存取服務的方法,以便您可以例如下載使用者的通訊錄或代表使用者發布 Twitter 訊息。

以下是一個使用 Google 進行驗證的範例處理器,將 Google 憑證儲存在 cookie 中,以便稍後存取:

class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
                               tornado.auth.GoogleOAuth2Mixin):
    async def get(self):
        if self.get_argument('code', False):
            user = await self.get_authenticated_user(
                redirect_uri='http://your.site.com/auth/google',
                code=self.get_argument('code'))
            # Save the user with e.g. set_signed_cookie
        else:
            await self.authorize_redirect(
                redirect_uri='http://your.site.com/auth/google',
                client_id=self.settings['google_oauth']['key'],
                scope=['profile', 'email'],
                response_type='code',
                extra_params={'approval_prompt': 'auto'})

請參閱 tornado.auth 模組文件,以了解更多詳細資訊。

跨網站請求偽造保護

跨網站請求偽造 (XSRF) 是個人化網頁應用程式的常見問題。

防止 XSRF 的普遍接受的解決方案是為每個使用者設定一個不可預測的值的 cookie,並將該值作為額外引數包含在您網站上的每個表單提交中。如果 cookie 和表單提交中的值不匹配,則該請求很可能是偽造的。

Tornado 具有內建的 XSRF 保護。若要將其包含在您的網站中,請包含應用程式設定 xsrf_cookies

settings = {
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

如果設定了 xsrf_cookies,Tornado 網頁應用程式將為所有使用者設定 _xsrf cookie,並拒絕所有不包含正確 _xsrf 值的 POSTPUTDELETE 請求。如果您開啟此設定,您需要對所有透過 POST 提交的表單進行檢測,使其包含此欄位。您可以使用所有樣板中可用的特殊 UIModule xsrf_form_html() 來執行此操作。

<form action="/new_message" method="post">
  {% module xsrf_form_html() %}
  <input type="text" name="message"/>
  <input type="submit" value="Post"/>
</form>

如果您提交 AJAX POST 請求,您還需要檢測您的 JavaScript,以將 _xsrf 值包含在每個請求中。這是我們在 FriendFeed 中用於 AJAX POST 請求的 jQuery 函數,它會自動將 _xsrf 值新增到所有請求。

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

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

對於 PUTDELETE 請求(以及不使用表單編碼引數的 POST 請求),XSRF 權杖也可以透過名為 X-XSRFToken 的 HTTP 標頭傳遞。XSRF cookie 通常在使用 xsrf_form_html 時設定,但在不使用任何常規表單的純 JavaScript 應用程式中,您可能需要手動存取 self.xsrf_token(僅讀取屬性就足以設定 cookie 作為副作用)。

如果您需要在每個處理器的基礎上自訂 XSRF 行為,您可以覆寫 RequestHandler.check_xsrf_cookie()。例如,如果您有一個不使用 cookie 進行驗證的 API,您可能想要停用 XSRF 保護,方法是讓 check_xsrf_cookie() 不執行任何操作。但是,如果您同時支援基於 cookie 和非基於 cookie 的驗證,則在使用 cookie 驗證目前請求時,務必使用 XSRF 保護。

DNS 重新繫結

DNS 重新繫結是一種可以繞過同源策略並允許外部網站存取私人網路資源的攻擊。此攻擊涉及一個 DNS 名稱(具有短的 TTL),它會在返回由攻擊者控制的 IP 位址和由受害者控制的 IP 位址(通常是可猜測的私人 IP 位址,例如 127.0.0.1192.168.1.1)之間交替。

使用 TLS 的應用程式不會受到此攻擊的影響(因為瀏覽器會顯示憑證不符警告,阻止自動存取目標網站)。

無法使用 TLS 並依賴網路層級存取控制的應用程式(例如,假設 127.0.0.1 上的伺服器只能由本機存取),應透過驗證 Host HTTP 標頭來防範 DNS 重綁定攻擊。這表示將限制性主機名稱模式傳遞給 HostMatches 路由器或 Application.add_handlers 的第一個參數。

# BAD: uses a default host pattern of r'.*'
app = Application([('/foo', FooHandler)])

# GOOD: only matches localhost or its ip address.
app = Application()
app.add_handlers(r'(localhost|127\.0\.0\.1)',
                 [('/foo', FooHandler)])

# GOOD: same as previous example using tornado.routing.
app = Application([
    (HostMatches(r'(localhost|127\.0\.0\.1)'),
        [('/foo', FooHandler)]),
    ])

此外,可能容易受到 DNS 重綁定攻擊的應用程式不應使用 Applicationdefault_host 參數和 DefaultHostMatches 路由器,因為它與使用萬用字元主機模式的效果類似。