导航

用户指南

认证与安全

cookies与安全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!")

Cookie并不安全,客户端可以轻松修改。如果你需要设置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__")

除了时间戳和HMAC签名之外,签名cookie还包含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!")

Tornado的安全cookie确保完整性但并未对cookie密。也就是说,cookie虽然不能被修改,但客户端可以看到里面的内容。 cookie_secret是一个对称密钥,必须保密——任何获得此密钥值的人都可以生成自己的签名cookie。

默认情况下,Tornado的安全cookie将在30天后过期。要更改此设置,请使用set_secure_cookieexpires_days关键字参数和get_secure_cookiemax_age_days参数。 这两个值是分开传递的,这样设计的原因是,比如你可以对于大多数用途,设置有一个有效期为30天的cookie,但对于某些敏感操作(例如更改帐单信息),在读取cookie时使用较小的max_age_days

Tornado还支持多个签名密钥以启用签名密钥轮换。cookie_secret必须是一个以一个整数类型的版本号作为键值,并以相应的密钥作为值的字典。需要注意的是,只能使用应用中通过key_version设置的那个版本的密钥进行签名,但可以使用字典中所有其他密钥进行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装饰器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装饰器只是简写,如果不是self.current_user:self.redirect(),可能不适合非基于浏览器的登录方案。

查看Tornado Blog示例应用程序,获取使用身份验证的完整示例(并将用户数据存储在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模块文档。

跨站点请求伪造(CSRF)保护

跨站点请求伪造(XSRF)是个人Web应用程序的常见问题。有关XSRF如何工作的更多信息,请参阅Wikipedia文章

普遍接受的防止XSRF的解决方案是为每个用户提供不可预测的值,并将该值作为附加参数包含在网站上的每个表单提交中。 如果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提交的所有表单都要包含对应字段。你可以使用所有模板中提供的特殊UIModulexsrf_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 Rebinding)

DNS重新绑定是一种可以绕过同源策略并允许外部站点访问专用网络上的资源的攻击。 此攻击包含一个TTL值特别小的DNS名称,该名称在返回由攻击者控制的IP地址和受害者控制的IP地址之间交替(通常是可猜测的私有IP地址,例如127.0.0.1或192.168.1.1)

使用TLS的应用程序不容易受到此攻击(因为浏览器将显示警告并阻止自动访问,因为被DNS被修改后访问的站点与真实目标站点的证书不匹配)。

无法使用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)]),
    ])

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

最新Tornado5.11官方文档翻译(7)-用户手册-认证与安全
Tagged on:

发表评论

电子邮件地址不会被公开。 必填项已用*标注