Skip to main content

认证和安全

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_secure_cookieget_secure_cookie 方法签名的Cookie。要使用这些方法,您需要在创建应用程序时指定一个名为 cookie_secret 的密钥。您可以将应用程序设置作为关键字参数传递给应用程序:

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

签名的cookie包含cookie的编码值以及时间戳和 HMAC 签名。如果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_secret 是一个对称密钥,必须保密 - 任何获得此密钥值的人都可以生成自己签名的cookie。

默认情况下,Tornado的安全Cookie会在30天后过期。要改变这一点,请使用 expires_days 关键字参数 set_secure_cookie and get_secure_cookiemax_age_days 参数。这两个值单独传递,以便您可以有大多数用途的有效期为30天的Cookie,但对于某些敏感操作(例如更改结算信息),您在阅读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装饰器 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博客示例应用程序 一个使用身份验证的完整示例(并在MySQL数据库中存储用户数据)。

第三方认证

tornado.auth 模块为网络上的一些最热门网站(包括Google/Gmail,Facebook,Twitter和FriendFeed)实施身份验证和授权协议。该模块包括通过这些网站记录用户的方法,以及在适当的情况下授权访问服务的方法,以便例如可以代表用户的地址簿下载或发布Twitter消息。

以下是使用Google进行身份验证的示例处理程序,将Google凭据保存在Cookie中以供日后访问:

class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
                               tornado.auth.GoogleOAuth2Mixin):
    @tornado.gen.coroutine
    def get(self):
        if self.get_argument('code', False):
            user = yield 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:
            yield 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如何工作的更多信息,请参阅 维基百科文章

防止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 值包含在每个请求中。这是 jQuery 函数,我们在FriendFeed中使用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 请求),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保护。