认证和安全

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

普通的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除了时间戳和一个 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!")

Tornado的安全cookie保证完整性但是不保证机密性. 也就是说, cookie不能被修改 但是它的内容对用户是可见的. 密钥 cookie_secret 是一个对称的key, 而且必 须保密–任何获得这个key的人都可以伪造出自己签名的cookie.

默认情况下, Tornado的安全cookie过期时间是30天. 可以给 set_secure_cookie 使用 expires_days 关键字参数 同时 get_secure_cookie 设置 max_age_days 参数也可以达到效果. 这两个值分别通过这样(设置)你就可以达 到如下的效果, 例如大多数情况下有30天有效期的cookie, 但是对某些敏感操作(例 如修改账单信息)你可以使用一个较小的 max_age_days .

Tornado也支持多签名密钥, 使签名密钥轮换. 然后 cookie_secret 必须是一个 以整数key版本作为key, 以相对应的密钥作为值的字典. 当前使用的签名键 必须是 应用设置中 key_version 的集合. 不过字典中的其他key都允许做 cookie签名验证, 如果当前key版本在cookie集合中.为了实现cookie更新, 可以通过 get_secure_cookie_key_version 查询当前key版本.

用户认证

当前已经通过认证的用户在每个请求处理函数中都可以通过 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)

如果你使用 authenticated 装饰 post() 方法并且用户没有登录, 服务将返回一个 403 响应. @authenticated 装饰器是 if not self.current_user: self.redirect() 的简写. 可能不适合 非基于浏览器的登录方案.

通过 Tornado Blog example application 可以看到一个使用用户验证(并且在MySQL数据库中存储用户数据)的完整例子.

第三方用户验证

tornado.auth 模块实现了对一些网络上最流行的网站的身份认证和授权协议, 包括Google/Gmail, Facebook, Twitter,和FriendFeed. 该模块包括通过这些 网站登录用户的方法, 并在适用情况下允许访问该网站服务的方法, 例如, 下载 一个用户的地址簿或者在他们支持下发布一条Twitter信息.

这是个使用Google身份认证, 在cookie中保存Google的认证信息以供之后访问 的示例处理程序:

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 模块的文档以了解更多细节.

跨站请求伪造(防护)

跨站请求伪造(Cross-site request forgery), 或 XSRF, 是所有web应用程序面临的一个主要问题. 可以通过 Wikipedia 文章 来了解 更多关于XSRF的细节.

普遍接受的预防XSRF攻击的方案是让每个用户的cookie都是不确定的值, 并且 把那个cookie值在你站点的每个form提交中作为额外的参数包含进来. 如果cookie 和form提交中的值不匹配, 则请求可能是伪造的.

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 值的 POST, PUT, 或 DELETE 请求. 如果你打开这个设置, 你必须给 所有通过 POST 请求的form提交添加这个字段. 你可以使用一个特性的 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 请求(除了不使用form编码(form-encoded) 参数 的 POST 请求, XSRF token也会通过一个 X-XSRFToken 的HTTP头传递. XSRF cookie 通常在使用 xsrf_form_html 会设置, 但是在不使用正规 form的纯Javascript应用中, 你可能需要访问 self.xsrf_token 手动设置 (只读这个属性足够设置cookie了).

如果你需要自定义每一个处理程序基础的XSRF行为, 你可以复写 RequestHandler.check_xsrf_cookie(). 例如, 如果你有一个没有使用 cookie验证的API, 你可能想禁用XSRF保护, 可以通过使 check_xsrf_cookie() 不做任何处理. 然而, 如果你支持基于cookie和非基于cookie的认证, 重要的是, 当前带有cookie认证的请求究竟什么时候使用XSRF保护.