Tornado 身份验证和安全性
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_cookie 和 get_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_secret
是一个对称密钥,必须保密——任何获得此密钥值的人都可以生成自己的签名 cookie。
默认情况下,Tornado 的安全 cookie 会在 30 天后过期。 要更改此设置,请使用 set_secure_cookie
的 expires_days
关键字参数和 get_secure_cookie
的 max_age_days
参数。 这两个值分别传递,以便您可以例如 对于大多数用途,cookie 的有效期为 30 天,但对于某些敏感操作(例如更改帐单信息),您在读取 cookie 时使用较小的 max_age_days
。
Tornado 还支持多个签名密钥以启用签名密钥轮换。 那么 cookie_secret
必须是一个字典,其中整数密钥版本作为键,相应的秘密作为值。 然后必须将当前使用的签名密钥设置为 key_version
应用程序设置,但如果在 cookie 中设置了正确的密钥版本,则允许 dict 中的所有其他密钥进行 cookie 签名验证。 要实现 cookie 更新,可以通过 get_secure_cookie_key_version 查询当前的签名密钥版本。
用户认证
当前经过身份验证的用户在每个请求处理程序中作为 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 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.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'})
跨站请求伪造保护
防止 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
值的 POST
、PUT
和 DELETE
请求。 如果打开此设置,则需要检测通过 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
值。 这是我们在 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 + ")"));
}});
};
对于 PUT
和 DELETE
请求(以及不使用表单编码参数的 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 重新绑定是一种可以绕过同源策略并允许外部站点访问专用网络上的资源的攻击。 这种攻击涉及一个 DNS 名称(具有短 TTL),该名称在返回由攻击者控制的 IP 地址和由受害者控制的 IP 地址(通常是可猜测的私有 IP 地址,例如 127.0.0.1
或 192.168.1.1
)之间交替。
使用 TLS 的应用程序不容易受到这种攻击(因为浏览器会显示阻止自动访问目标站点的证书不匹配警告)。
不能使用 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)]),
])
此外,Application 和 DefaultHostMatches 路由器的default_host
参数不得用于可能易受 DNS 重新绑定攻击的应用程序中,因为它与通配符主机模式具有类似的效果。