身份验证和安全¶
用户身份验证¶
当前认证的用户在每个请求处理程序中都可用,表示为 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_signed_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_signed_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 博客示例应用程序 以获取使用身份验证(并将用户数据存储在 PostgreSQL 数据库中)的完整示例。
第三方身份验证¶
tornado.auth
模块实现了 Web 上一些最受欢迎网站的身份验证和授权协议,包括 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_signed_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'})
有关更多详细信息,请参见 tornado.auth
模块文档。
跨站点请求伪造保护¶
跨站点请求伪造(或 XSRF)是个性化 Web 应用程序的常见问题。
普遍接受的防止 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
的 default_host
参数和 DefaultHostMatches
路由器不得在可能容易受到 DNS 重绑定攻击的应用程序中使用,因为它们的作用类似于通配符主机模式。