身份验证和安全性

Cookies 和安全 cookies

您可以在用户浏览器中使用 set_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!")

cookies是不安全的,客户机可以很容易地修改。如果需要将cookie设置为(例如,标识当前登录的用户),则需要对cookie进行签名以防止伪造。Tornado支持用 set_secure_cookieget_secure_cookie 方法。要使用这些方法,需要指定一个名为 cookie_secret 创建应用程序时。可以将应用程序设置作为关键字参数传递给应用程序:

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

签名的cookie除了包含时间戳和 HMAC 签名。如果cookie是旧的或者签名不匹配, get_secure_cookie 将返回 None 就好像曲奇没做好一样。上述示例的安全版本:

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的安全cookies保证完整性,但不保密。也就是说,不能修改cookie,但是用户可以看到它的内容。这个 cookie_secret 是对称密钥,必须保密——任何获得该密钥值的人都可以生成自己的签名cookie。

默认情况下,Tornado的安全cookies将在30天后过期。要更改此设置,请使用 expires_days 关键字参数 set_secure_cookie and 这个 max_age_days 参数 get_secure_cookie . 这两个值是分开传递的,这样您就可以拥有一个在大多数情况下有效30天的cookie,但是对于某些敏感操作(如更改账单信息),您可以使用一个较小的 max_age_days 读饼干的时候。

Tornado还支持多个签名密钥以启用签名密钥旋转。 cookie_secret 然后必须是一个dict,以整数键版本作为键,相应的秘密作为值。然后必须将当前使用的签名密钥设置为 key_version 如果在cookie中设置了正确的密钥版本,则允许对dict中的所有其他密钥进行cookie签名验证。要实现cookie更新,可以通过以下方式查询当前签名密钥版本: get_secure_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_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 . 如果一个请求使用这个修饰器转到一个方法,而用户没有登录,那么他们将被重定向到 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)

如果你装饰 post() 方法 authenticated 如果用户没有登录,服务器将发送一个 403 反应。这个 @authenticated 装饰师只是 if not self.current_user: self.redirect() 并且可能不适用于非基于浏览器的登录方案。

退房 Tornado Blog example application 对于使用身份验证的完整示例(并将用户数据存储在MySQL数据库中)。

第三方认证

这个 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_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'})

tornado.auth 模块文档了解更多详细信息。

跨站点请求伪造保护

Cross-site request forgery, or XSRF, is a common problem for personalized web applications. See the Wikipedia article 有关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 Web应用程序将设置 _xsrf 所有用户的cookie并拒绝所有用户 POSTPUTDELETE 不包含正确的 _xsrf 价值。如果启用此设置,则需要检测通过 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 每个请求的值。这就是 jQuery 我们在FriendFeed for Ajax中使用的函数 POST 自动添加 _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 不使用表单编码参数的请求),也可以通过名为 X-XSRFToken . XSRF cookie通常在以下情况下设置: xsrf_form_html 使用,但在纯JavaScript应用程序中,该应用程序不使用可能需要访问的任何常规表单。 self.xsrf_token 手动(仅读取属性就足以将cookie设置为副作用)。

如果需要基于每个处理程序自定义XSRF行为,可以重写 RequestHandler.check_xsrf_cookie() . 例如,如果您有一个认证不使用cookie的API,您可能希望通过 check_xsrf_cookie() 什么也不做。但是,如果您同时支持基于cookie和非cookie的身份验证,那么在当前请求通过cookie进行身份验证时使用xsrf保护是很重要的。

DNS再绑定

DNS rebinding 是一种可以绕过同一源策略并允许外部站点访问专用网络上的资源的攻击。这种攻击涉及一个DNS名称(有一个短的TTL),它交替返回由攻击者控制的IP地址和由受害者控制的IP地址(通常是可猜测的专用IP地址,如 127.0.0.1192.168.1.1

使用TLS的应用程序是 not 易受攻击(因为浏览器将显示阻止自动访问目标站点的证书不匹配警告)。

无法使用TLS并依赖网络级访问控制的应用程序(例如,假设服务器 127.0.0.1 只能由本地计算机访问)应通过验证 Host HTTP报头。这意味着将限制性主机名模式传递给 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)]),
    ])

此外, default_host 参数 Application 以及 DefaultHostMatches 路由器不能用于可能易受DNS重新绑定攻击的应用程序,因为它具有与通配符主机模式类似的效果。