Authentication and security
cookies and secure cookies
你可以通过 set_cookie 将 cookies 写入用户浏览器
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 不安全,可以被客户端轻易地修改。如果你需要设置 cookies 以确定当前登录用户,你应当对 cookies 做签名以防止被伪造。
tornado 支持 set_secure_cookie 和 get_secure_cookie 方法对 cookies 进行签名。
如果使用上述方法,需要制定一个秘钥名为 cookie_secret 当你创建应用的时候。您可以通过应用程序设置关键字参数。
application = tornado.web.Application([
(r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
签名的 cookies 包含 cookie 编码值和时间戳以及 HMAC 签名。如果 cookies 是旧值或者签名不匹配,get_secure_cookie 将会 return None, 等同于未设置 cookies。上面示例的安全版本
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 的安全 cookies 保证完整性,但不是机密的。cookies 不可以被修改,但是他的内容用户可以看到。
cookie_secret 是对称秘钥,必须保密。谁获得了这个关键值都可以生产自己的 cookies。
默认情况下,tornado 的 cookies 有效期是 30 天。set_secure_cookie 方法使用 expires_days 关键字重置,get_secure_cookie 使用 max_age_days 关键字重置。
这两个值是独立使用的,对大多数需求,你的 cookies 有效期是 30 天,但是对某些敏感行为,你可以使用一个小的 max_age_days 去限制 cookie 的有效期。
tornado 支持多个签名秘钥使签名秘钥旋转。cookie_secret 必须是一个字典,key 值是 int 类型(版本号),对应的 value 值为秘钥。
当前使用的签名秘钥必须被设置为 key_version 应用参数,如果 cookie 中设置正确版本的 key,字典中其他的签名秘钥被允许对 cookie 做签名校验。
实现 cookie 的更新,可以通过 get_secure_cookie_key_version 获取当前秘钥版本。
User authentication
当前验证的对象可以在每一个请求处理对象中使用 self.current_user 获取,在模板中 current_user 获取。默认的 current_user 是 None。
在应用程序中实现用户身份验证,您需要在请求处理对象中重写 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 的博客示例应用程序提供了一个完整的使用 authentication 示例 (数据存储在 MySQL 数据库中)。
Third party authentication
tornado.auth 模块针对最受欢迎的网站实现了身份验证和授权协议,比如 Google/Gmail, Facebook, Twitter, FriendFeed。
模块包含了用户登录网站的方法,在适用情况下,方法授权访问服务,这样就可以实现下载用户的地址簿或者发布一条 Twitter 消息。
下面的示例处理程序使用谷歌进行验证,将谷歌整数保存在 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 模块获取更详细信息。
Cross-site request forgery protection
跨站点请求伪造或 XSRF 是个性化的 web 应用程序的一个常见问题。
公认的解决方案去防止 XSRF 是在 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 请求。
如果你将该设置置为 on,你需要在所有 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 包含在每个请求中。
这个是我们用 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 + ")"));
}});
};
对于 PUT 以及 DELETE (包括不使用 form 表单数据的 POST 请求),XSRF 令牌也可以通过 HTTP 头中的 X-XSRFToken。
XSRF cookie 在 xsrf_form_html 使用的时候通常被设置,但是在纯 javascript 应用中不使用任何常规的表单,您可能需要手动获取 self.xsrf_token 值。
如果你想定制 XSRF 在每一个操作基类中的表现,你可以重写 RequestHandler 的 check_xsrf_cookie() 方法。
例如,你有一个 API 不需要用户 cookie 验证,你可能希望通过 check_xsrf_cookie() 不做处理来 关掉 XSRF 保护。
然而,如果你同时支持 cookie 和 non-cookie-based 验证,每当请求被 cookie 验证的时候,XSRF 保护将会被使用。
DNS Rebinding
DNS 重新绑定是一种攻击,可以绕过同源策略并且允许外部网站访问私有网络上的资源。
此攻击涉及 DNS 名称(短 TTL),在攻击者控制的 IP 和受害者控制的 IP 间交替。
使用 TLS 的应用程序不容易受到这种攻击(因为浏览器将显示证书警告不匹配块自动访问目标网站)
应用程序不能使用 TLS 依靠网络级访问控制 (例如,假设一个服务器 127.0.0.1 智能访问本地的机器)应防止 DNS 重新绑定验证主机 HTTP 头。这意味着通过限制主机名模式 (匹配 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)]),
])
除此之外,Application 的 default_host 参数以及 DefaultHostMatches 路由因为有类似的主机通配符,容易受到 DNS 重新绑定攻击,所以不可以在应用中使用。
上一篇: 2.5、User’s guide (Structure of a Tornado web application)
下一篇: 2.7、User’s guide (Running and deploying)
网友评论