身份验证和安全

Cookie 和签名 Cookie

您可以使用 set_cookie 方法在用户的浏览器中设置 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 不安全,并且可以轻松地被客户端修改。如果您需要设置 Cookie 来识别当前登录用户等,您需要对 Cookie 进行签名以防止伪造。Tornado 通过 set_signed_cookieget_signed_cookie 方法支持签名 Cookie。要使用这些方法,您需要在创建应用程序时指定名为 cookie_secret 的密钥。您可以将应用程序设置作为关键字参数传递给您的应用程序。

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

签名 Cookie 除了时间戳和 HMAC 签名外,还包含 Cookie 的编码值。如果 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 的签名 Cookie 保证完整性,但不保证机密性。也就是说,Cookie 无法修改,但其内容可以被用户看到。 cookie_secret 是一个对称密钥,必须保密 - 任何获得此密钥值的人都可以生成自己的签名 Cookie。

默认情况下,Tornado 的签名 Cookie 在 30 天后过期。要更改此设置,请使用 expires_days 关键字参数传递给 set_signed_cookie 以及 max_age_days 参数传递给 get_signed_cookie。这两个值是分别传递的,这样您就可以例如有一个对大多数目的有效的 30 天 Cookie,但对于某些敏感操作(例如更改账单信息)您在读取 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 模块实现了 Web 上一些最受欢迎网站的身份验证和授权协议,包括 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)是个性化 Web 应用程序的常见问题。

普遍接受的防止 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 Web 应用程序将为所有用户设置 _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()。例如,如果您有一个 API 的身份验证不使用 Cookie,您可能希望通过使 check_xsrf_cookie() 不执行任何操作来禁用 XSRF 保护。但是,如果您同时支持基于 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)]),
    ])

此外,Applicationdefault_host 参数和 DefaultHostMatches 路由器不得在可能容易受到 DNS 重绑定攻击的应用程序中使用,因为它们的作用类似于通配符主机模式。