认证与安全¶

Cookies 和 secure cookies¶

你可以使用 set_cookie 方法在用户的浏览器中设置 cookies:

  1. class MainHandler(tornado.web.RequestHandler):
  2. def get(self):
  3. if not self.get_cookie("mycookie"):
  4. self.set_cookie("mycookie", "myvalue")
  5. self.write("Your cookie was not set yet!")
  6. else:
  7. self.write("Your cookie was set!")

Cookies 是不安全的而且很容易被客户端修改. 如果你通过设置 cookies 来识别当前登陆的用户, 你需要利用签名来防止 cookies 被伪造. Tornado 利用set_secure_cookieget_secure_cookie 方法来对 cookies签名.为了使用这些方法, 你需要在创建应用程序时指定一个叫做 cookie_secret 的密匙.你可以在应用程序的设置中通过传递参数来注册密匙:

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

对 cookies 签名后就有确定的编码后的值, 还有时间戳和一个 HMAC .如果 cookes 过期或者签名不匹配, get_secure_cookie 将返回 None就如同这个 cookie 没有被设置一样. 这是一个安全版本的例子:

  1. class MainHandler(tornado.web.RequestHandler):
  2. def get(self):
  3. if not self.get_secure_cookie("mycookie"):
  4. self.set_secure_cookie("mycookie", "myvalue")
  5. self.write("Your cookie was not set yet!")
  6. else:
  7. self.write("Your cookie was set!")

Tornado 的 secure cookies 保证完整性但不保证保密性.就是说, cookie 将不会被修改, 但是它会让用户看到. cookie_secret是一个对称密钥, 所以它必须被保护起来 –任何一个人得到密钥的值就将会制造一个签名的 cookie.

默认情况下, Tornado 的 secure cookies 将会在 30 天后过期. 如果要修改这个值,使用 expiresdays 关键词参数传递给 set_secure_cookie 和_max_age_days 参数传递给 get_secure_cookie. 这两个值的传递是相互独立的,你可能会在大多数情况下会使用一个 30 天内合法的密匙, 但是对某些敏感操作(例如修改账单信息) 你可以使用一个较小的 max_age_days .

Tornado 也支持多个签名的密匙, 这样可以使用密匙轮换.这样 cookie_secret 必须是一个具有整数作为密匙版本的字典.当前正在使用的签名密匙版本必须在应用程序中被设置为 key_version如果一个正确的密匙版本在 cookie 中被设置,密匙字典中的其它密匙也可以被用来作为 cookie 的签名认证,为了实现 cookie 的更新, 可以在get_secure_cookie_key_version 中查询当前的密匙版本.

用户认证¶

当前通过认证的用户在请求处理器的 self.current_user 当中,而且还存在于模版中的 current_user. 默认情况下, current_user 的值为None.

为了在你的应用程序中实现用户认证, 你需要覆盖请求控制器中的 get_current_user() 方法来确认怎样获取当前登陆的用户, 例如, 从 cookie 的值中获取该信息.下面这个例子展示了通过用户的昵称来确定用户身份, 值被保存在 cookies 中:

  1. class BaseHandler(tornado.web.RequestHandler):
  2. def get_current_user(self):
  3. return self.get_secure_cookie("user")
  4.  
  5. class MainHandler(BaseHandler):
  6. def get(self):
  7. if not self.current_user:
  8. self.redirect("/login")
  9. return
  10. name = tornado.escape.xhtml_escape(self.current_user)
  11. self.write("Hello, " + name)
  12.  
  13. class LoginHandler(BaseHandler):
  14. def get(self):
  15. self.write('<html><body><form action="/login" method="post">'
  16. 'Name: <input type="text" name="name">'
  17. '<input type="submit" value="Sign in">'
  18. '</form></body></html>')
  19.  
  20. def post(self):
  21. self.set_secure_cookie("user", self.get_argument("name"))
  22. self.redirect("/")
  23.  
  24. application = tornado.web.Application([
  25. (r"/", MainHandler),
  26. (r"/login", LoginHandler),
  27. ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

你可以使用 Python装饰器 (decorator)tornado.web.authenticated 来获取登陆的用户.如果你的方法被这个装饰器所修饰, 若是当前的用户没有登陆, 则用户会被重定向到login_url (在应用程序设置中).上面的例子也可以这样写:

  1. class MainHandler(BaseHandler):
  2. @tornado.web.authenticated
  3. def get(self):
  4. name = tornado.escape.xhtml_escape(self.current_user)
  5. self.write("Hello, " + name)
  6.  
  7. settings = {
  8. "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
  9. "login_url": "/login",
  10. }
  11. application = tornado.web.Application([
  12. (r"/", MainHandler),
  13. (r"/login", LoginHandler),
  14. ], **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 身份认证的例子,在 cookie 中保存 Google 的认证信息用来进行后续的操作:

  1. class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
  2. tornado.auth.GoogleOAuth2Mixin):
  3. @tornado.gen.coroutine
  4. def get(self):
  5. if self.get_argument('code', False):
  6. user = yield self.get_authenticated_user(
  7. redirect_uri='http://your.site.com/auth/google',
  8. code=self.get_argument('code'))
  9. # Save the user with e.g. set_secure_cookie
  10. else:
  11. yield self.authorize_redirect(
  12. redirect_uri='http://your.site.com/auth/google',
  13. client_id=self.settings['google_oauth']['key'],
  14. scope=['profile', 'email'],
  15. response_type='code',
  16. extra_params={'approval_prompt': 'auto'})

详情可查看 tornado.auth 模块文档.

跨站请求伪造防护¶

跨站请求伪造(Cross-site requestforgery),XSRF, 是一个 web 应用程序要面临的常规问题 . 详见Wikipedia文章 查看关于 XSRF 的详细信息.

一个普遍被接受的防护 XSRF 做法是让每一个用户 cookie 都保存不可预测的值,然后把那个值通过 form 额外的提交到你的站点. 如果 cookie 和 form 中提交的值不匹配,那么请求很有可能是伪造的.

Tornado 内置有 XSRF 保护. 你需要在应用程序中设置 xsrf_cookies:

  1. settings = {
  2. "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
  3. "login_url": "/login",
  4. "xsrf_cookies": True,
  5. }
  6. application = tornado.web.Application([
  7. (r"/", MainHandler),
  8. (r"/login", LoginHandler),
  9. ], **settings)

如果设置了 xsrf_cookies , Tornado web 应用程序将会为每一个用户设置一个 _xsrf cookie来拒绝所有与 _xsrf 的值不匹配的POST, PUT, 和 DELETE 请求.如果你将此设置打开, 你必须给每个通过 POST 提交表单中添加这个字段.你可以通过特殊的 UIModule xsrf_form_html() 来实现这些, 在模版中是可用的:

  1. <form action="/new_message" method="post">
  2. {% module xsrf_form_html() %}
  3. <input type="text" name="message"/>
  4. <input type="submit" value="Post"/>
  5. </form>

如果你提交一个 AJAX POST 请求, 你的每次请求需要在你的 JavaScript 中添加一个_xsrf 的值. 这是一个我们在 FriendFeed 中用到的一个通过 AJAXPOST 方法来自动添加 _xsrf 值的 jQuery 函数:

  1. function getCookie(name) {
  2. var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
  3. return r ? r[1] : undefined;
  4. }
  5.  
  6. jQuery.postJSON = function(url, args, callback) {
  7. args._xsrf = getCookie("_xsrf");
  8. - .ajax({url: url, data: - .param(args), dataType: "text", type: "POST",
  9. success: function(response) {
  10. callback(eval("(" + response + ")"));
  11. }});
  12. };

对于 PUTDELETE 请求 (除了不像 POST 请求用到 form 编码参数),XSRF token 会通过 HTTP 首部中的 X-XSRFToken 字段来传输.XSRF cookie 在 xsrf_form_html 被使用时设置, 但是在一个非通常形式的纯 JavaScript 应用程序中, 你可能需要手动设置 self.xsrf_token(仅通过读取这个属性就足以有效设置 cookie 了).

如果你需要对每一个基本的控制器自定义 XSRF 行为, 你一个覆盖RequestHandler.check_xsrf_cookie(). 例如,如果你有一个不是通过 cookie 来认证的 API, 你可能需要让check_xsrf_cookie() 不做任何事来禁用 XSRF 的保护功能.然而, 如果你既支持 cookie 认证又支持 非基于 cookie 的认证,这样当前请求通过 cookie 认证的 XSRF 保护就会十分的重要.

原文:

https://tornado-zh-cn.readthedocs.io/zh_CN/latest/guide/security.html