Authentication and security

Cookies and secure cookies

您可以使用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!")

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

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

签名的cookie除包含时间戳和HMAC签名外,还包含cookie的编码值. 如果cookie是旧的或签名不匹配,则get_secure_cookie将返回None就像未设置cookie一样. 上面示例的安全版本:

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

龙卷风的安全cookie保证完整性,但不能保证机密性. 也就是说,cookie不能被修改,但用户可以看到其内容. cookie_secret是一个对称密钥,必须保密-任何获得此密钥值的人都可以产生自己的签名cookie.

默认情况下,Tornado的安全Cookie会在30天后过期. 要改变这一点,使用expires_days关键字参数set_secure_cookie max_age_days参数get_secure_cookie . 这两个值是分别传递的,因此,例如,对于大多数用途,您可以使用有效期为30天的cookie,但是对于某些敏感操作(例如更改帐单信息),您在读取cookie时会使用较小的max_age_days .

龙卷风还支持多个签名键,以启用签名键旋转. cookie_secret然后必须是一个以整数键版本作为键,而相应的机密作为值的字典. 然后,必须将当前使用的签名密钥设置为key_version应用程序设置,但如果在cookie中设置了正确的密钥版本,则允许dict中的所有其他密钥进行cookie签名验证. 要实现cookie更新,可以通过get_secure_cookie_key_version查询当前的签名密钥版本.

User authentication

当前经过身份验证的用户在每个请求处理程序中都可以作为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装饰器 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 Blog示例应用程序 ,以获取使用身份验证(并将用户数据存储在MySQL数据库中)的完整示例.

Third party authentication

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_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 protection

Cross-site request forgery, or XSRF, is a common problem for personalized web applications. See the Wikipedia article for more information on how XSRF works.

防止XSRF的普遍接受的解决方案是使用无法预测的值对每个用户进行cookie,并将该值作为附加参数包括在网站上提交的每个表单中. 如果cookie和表单提交中的值不匹配,则可能是伪造的请求.

龙卷风带有内置的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设置,龙卷风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值. 这是我们在FriendFeed上使用的jQuery函数,用于AJAX POST请求,该_xsrf会自动将_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的HTTP标头传递XSRF令牌. 通常在使用xsrf_form_html时设置XSRF cookie,但是在不使用任何常规形式的纯Javascript应用程序中,您可能需要手动访问self.xsrf_token (仅读取属性就足以将cookie设置为副作用) .

如果需要基于每个处理程序自定义XSRF行为,则可以重写RequestHandler.check_xsrf_cookie() . 例如,如果您的API的身份验证不使用cookie,则可能需要通过使check_xsrf_cookie()不执行任何操作来禁用XSRF保护. 但是,如果同时支持cookie和基于非cookie的身份验证,则在使用cookie对当前请求进行身份验证时,务必使用XSRF保护.

DNS Rebinding

DNS重新绑定是一种攻击,可以绕过同源策略,并且允许外部站点访问专用网络上的资源. 此攻击涉及一个DNS名称(带有短TTL),该DNS名称在返回由攻击者控制的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)]),
    ])

In addition, the default_host argument to Application and the DefaultHostMatches router must not be used in applications that may be vulnerable to DNS rebinding, because it has a similar effect to a wildcard host pattern.