Tornado 身份驗證和安全性

2022-03-08 10:40 更新

Cookie

您可以使用?set_cookie?方法在用戶瀏覽器中設(shè)置 cookie:

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!")

Cookie 不安全,很容易被修改。 如果您需要設(shè)置 cookie,例如,識別當(dāng)前登錄的用戶,您需要簽署您的 cookie 以防止偽造。 Tornado 支持使用 set_secure_cookie 和 get_secure_cookie 方法簽名的 cookie。 要使用這些方法,您需要在創(chuàng)建應(yīng)用程序時指定一個名為 ?cookie_secret? 的密鑰。 您可以將應(yīng)用程序設(shè)置作為關(guān)鍵字參數(shù)傳遞給您的應(yīng)用程序:

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

除了時間戳和 HMAC 簽名之外,簽名的 cookie 還包含 cookie 的編碼值。 如果 cookie 是舊的或者簽名不匹配,?get_secure_cookie? 將返回 ?None? ,就好像 cookie 沒有設(shè)置一樣。 上面示例的安全版本:

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

Tornado 的安全 cookie 保證完整性,但不保證機(jī)密性。 也就是說,cookie 無法修改,但用戶可以看到其內(nèi)容。 ?cookie_secret? 是一個對稱密鑰,必須保密——任何獲得此密鑰值的人都可以生成自己的簽名 cookie。

默認(rèn)情況下,Tornado 的安全 cookie 會在 30 天后過期。 要更改此設(shè)置,請使用 ?set_secure_cookie? 的 ?expires_days? 關(guān)鍵字參數(shù)和 ?get_secure_cookie? 的 ?max_age_days? 參數(shù)。 這兩個值分別傳遞,以便您可以例如 對于大多數(shù)用途,cookie 的有效期為 30 天,但對于某些敏感操作(例如更改帳單信息),您在讀取 cookie 時使用較小的 ?max_age_days?。

Tornado 還支持多個簽名密鑰以啟用簽名密鑰輪換。 那么 ?cookie_secret? 必須是一個字典,其中整數(shù)密鑰版本作為鍵,相應(yīng)的秘密作為值。 然后必須將當(dāng)前使用的簽名密鑰設(shè)置為 ?key_version? 應(yīng)用程序設(shè)置,但如果在 cookie 中設(shè)置了正確的密鑰版本,則允許 dict 中的所有其他密鑰進(jìn)行 cookie 簽名驗證。 要實現(xiàn) cookie 更新,可以通過 get_secure_cookie_key_version 查詢當(dāng)前的簽名密鑰版本。

用戶認(rèn)證

當(dāng)前經(jīng)過身份驗證的用戶在每個請求處理程序中作為 self.current_user 可用,在每個模板中作為 ?current_user? 可用。 默認(rèn)情況下,?current_user? 為?None?。

要在您的應(yīng)用程序中實現(xiàn)用戶身份驗證,您需要覆蓋請求處理程序中的 ?get_current_user()方法,以根據(jù)例如 cookie 的值來確定當(dāng)前用戶。 下面是一個示例,用戶只需指定昵稱即可登錄應(yīng)用程序,然后將昵稱保存在 cookie 中:

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_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_secure_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 decorator tornado.web.authenticated 登錄。 如果一個請求使用這個裝飾器發(fā)送到一個方法,并且用戶沒有登錄,他們將被重定向到 ?login_url?(另一個應(yīng)用程序設(shè)置)。 上面的例子可以重寫:

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()? 方法,并且用戶沒有登錄,服務(wù)器將發(fā)送一個 ?403? 響應(yīng)。 ?@authenticated? 裝飾器只是 ?if not self.current_user: self.redirect()? 的簡寫,可能不適用于非基于瀏覽器的登錄方案。

第三方認(rèn)證

tornado.auth 模塊為網(wǎng)絡(luò)上許多最流行的站點實現(xiàn)身份驗證和授權(quán)協(xié)議,包括 Google/Gmail、Facebook、Twitter 和 FriendFeed。 該模塊包括通過這些站點登錄用戶的方法,以及在適用的情況下授權(quán)訪問服務(wù)的方法,以便您可以下載用戶的通訊錄或代表他們發(fā)布 Twitter 消息。

這是一個使用 Google 進(jìn)行身份驗證的示例處理程序,將 Google 憑據(jù)保存在 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_secure_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'})

跨站請求偽造保護(hù)

防止 XSRF 的普遍接受的解決方案是使用不可預(yù)測的值對每個用戶進(jìn)行 cookie,并將該值作為附加參數(shù)包含在您網(wǎng)站上的每個表單提交中。 如果 cookie 和表單提交中的值不匹配,那么請求很可能是偽造的。

Tornado 帶有內(nèi)置的 XSRF 保護(hù)。 要將其包含在您的站點中,請包含應(yīng)用程序設(shè)置 ?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)

如果設(shè)置了 ?xsrf_cookies?,Tornado Web 應(yīng)用程序?qū)樗杏脩粼O(shè)置 ?_xsrf? cookie,并拒絕所有不包含正確 ?_xsrf? 值的 ?POST?、?PUT? 和 ?DELETE? 請求。 如果打開此設(shè)置,則需要檢測通過 ?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 函數(shù),它會自動將 ?_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 + ")"));
    }});
};

對于 ?PUT ?和 ?DELETE ?請求(以及不使用表單編碼參數(shù)的 ?POST ?請求),XSRF 令牌也可以通過名為 ?X-XSRFToken? 的 HTTP 標(biāo)頭傳遞。 XSRF cookie 通常在使用 ?xsrf_form_html? 時設(shè)置,但在不使用任何常規(guī)表單的純 JavaScript 應(yīng)用程序中,您可能需要手動訪問 ?self.xsrf_token?(只需讀取屬性就足以將 cookie 設(shè)置為副作用) .

如果您需要基于每個處理程序自定義 XSRF 行為,您可以覆蓋 RequestHandler.check_xsrf_cookie()。 例如,如果您的 API 的身份驗證不使用 cookie,您可能希望通過使 ?check_xsrf_cookie()? 什么都不做來禁用 XSRF 保護(hù)。 但是,如果您同時支持 cookie 和非基于 cookie 的身份驗證,那么無論何時使用 cookie 對當(dāng)前請求進(jìn)行身份驗證,都必須使用 XSRF 保護(hù)。

DNS重新綁定

DNS 重新綁定是一種可以繞過同源策略并允許外部站點訪問專用網(wǎng)絡(luò)上的資源的攻擊。 這種攻擊涉及一個 DNS 名稱(具有短 TTL),該名稱在返回由攻擊者控制的 IP 地址和由受害者控制的 IP 地址(通常是可猜測的私有 IP 地址,例如 ?127.0.0.1? 或 ?192.168.1.1?)之間交替。

使用 TLS 的應(yīng)用程序不容易受到這種攻擊(因為瀏覽器會顯示阻止自動訪問目標(biāo)站點的證書不匹配警告)。

不能使用 TLS 并依賴網(wǎng)絡(luò)級訪問控制的應(yīng)用程序(例如,假設(shè) ?127.0.0.1? 上的服務(wù)器只能由本地計算機(jī)訪問)應(yīng)通過驗證 ?Host? HTTP 表頭來防止 DNS 重新綁定。 這意味著將限制性主機(jī)名模式傳遞給 HostMatches 路由器或 Application.add_handlers 的第一個參數(shù):

# 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)]),
    ])

此外,Application 和 DefaultHostMatches 路由器的?default_host?參數(shù)不得用于可能易受 DNS 重新綁定攻擊的應(yīng)用程序中,因為它與通配符主機(jī)模式具有類似的效果。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號