Tornado基础:安全应用
Cookie#
对于 RequestHandler,除了在初始 Tornado 中讲到的之外,还提供了操作 cookie 的方法。
设置#
set_cookie(name, value, domain=None, expires=None, path=’/‘, expires_days=None)
参数说明:
参数名 | 说明 |
---|---|
name | cookie 名 |
value | cookie 值 |
domain | 提交 cookie 时匹配的域名 |
path | 提交 cookie 时匹配的路径 |
expires | cookie 的有效期,可以是时间戳整数、时间元组或者 datetime 类型,为 UTC 时间 |
expires_days | cookie 的有效期,天数,优先级低于 expires |
import datetime
class IndexHandler(RequestHandler):
def get(self):
self.set_cookie("n1", "v1")
self.set_cookie("n2", "v2", path="/new", expires=time.strptime("2016-11-11 23:59:59","%Y-%m-%d %H:%M:%S"))
self.set_cookie("n3", "v3", expires_days=20)
# 利用time.mktime将本地时间转换为UTC标准时间
self.set_cookie("n4", "v4", expires=time.mktime(time.strptime("2016-11-11 23:59:59","%Y-%m-%d %H:%M:%S")))
self.write("OK")
原理#
设置 cookie 实际就是通过设置 header 的 Set-Cookie 来实现的。
class IndexHandler(RequestHandler):
def get(self):
self.set_header("Set-Cookie", "n5=v5; expires=Fri, 11 Nov 2016 15:59:59 GMT; Path=/")
self.write("OK")
获取#
get_cookie(name, default=None)
获取名为 name 的 cookie,可以设置默认值。
class IndexHandler(RequestHandler):
def get(self):
n3 = self.get_cookie("n3")
self.write(n3)
清除#
clear_cookie(name, path=’/‘, domain=None)
删除名为 name,并同时匹配 domain 和 path 的 cookie。
clear_all_cookies(path=’/‘, domain=None)
删除同时匹配 domain 和 path 的所有 cookie。
class ClearOneCookieHandler(RequestHandler):
def get(self):
self.clear_cookie("n3")
self.write("OK")
class ClearAllCookieHandler(RequestHandler):
def get(self):
self.clear_all_cookies()
self.write("OK")
{{<admonition warning “注意” true >}}
执行清除 cookie 操作后,并不是立即删除了浏览器中的 cookie,而是给 cookie 值置空,并改变其有效期使其失效。真正的删除 cookie 是由浏览器去清理的。
{{< /admonition >}}
安全 Cookie#
Cookie 是存储在客户端浏览器中的,很容易被篡改。Tornado 提供了一种对 Cookie 进行简易加密签名的方法来防止 Cookie 被恶意篡改。
使用安全 Cookie 需要为应用配置一个用来给 Cookie 进行混淆的秘钥 cookie_secret,将其传递给 Application 的构造函数。我们可以使用如下方法来生成一个随机字符串作为 cookie_secret 的值。
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
'2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A='
{{<admonition tip “提示” true >}}
Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。三个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节需要用 4 个可打印字符来表示。
uuid, 通用唯一识别码(英语:Universally Unique Identifier,简称 UUID),是由一组 32 个 16 进制数字所构成(两个 16 进制数是一个字节,总共 16 字节),因此 UUID 理论上的总数为 16^32=2^128,约等于 3.4 x 10^38。也就是说若每纳秒产生 1 兆个 UUID,要花 100 亿年才会将所有 UUID 用完。
uuid 模块的 uuid4 () 函数可以随机产生一个 uuid 码,bytes 属性将此 uuid 码作为 16 字节字符串。
{{< /admonition >}}
将生成的 cookie_secret 传入 Application 构造函数:
app = tornado.web.Application(
[(r"/", IndexHandler),],
cookie_secret = "2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A="
)
获取和设置#
set_secure_cookie(name, value, expires_days=30)#
设置一个带签名和时间戳的 cookie,防止 cookie 被伪造。
get_secure_cookie(name, value=None, max_age_days=31)#
如果 cookie 存在且验证通过,返回 cookie 的值,否则返回 None。max_age_day 不同于 expires_days,expires_days 是设置浏览器中 cookie 的有效期,而 max_age_day 是过滤安全 cookie 的时间戳。
class IndexHandler(RequestHandler):
def get(self):
cookie = self.get_secure_cookie("count")
count = int(cookie) + 1 if cookie else 1
self.set_secure_cookie("count", str(count))
self.write(
'<html><head><title>Cookie计数器</title></head>'
'<body><h1>您已访问本页%d次。</h1>' % count +
'</body></html>'
)
我们看签名后的 cookie 值:
"2|1:0|10:1476412069|5:count|4:NQ==|cb5fc1d4434971de6abf87270ac33381c686e4ec8c6f7e62130a0f8cbe5b7609"
字段说明:
- 安全 cookie 的版本,默认使用版本 2,不带长度说明前缀
- 默认为 0
- 时间戳
- cookie 名
- base64 编码的 cookie 值
- 签名值,不带长度说明前缀
{{<admonition warning “注意” true >}}
Tornado 的安全 cookie 只是一定程度的安全,仅仅是增加了恶意修改的难度。Tornado 的安全 cookies 仍然容易被窃听,而 cookie 值是签名不是加密,攻击者能够读取已存储的 cookie 值,并且可以传输他们的数据到任意服务器,或者通过发送没有修改的数据给应用伪造请求。因此,避免在浏览器 cookie 中存储敏感的用户数据是非常重要的。
{{< /admonition >}}
XSRF#
跨站请求伪造#
先建立一个网站 127.0.0.1:8000,使用上一节中的 Cookie 计数器:
class IndexHandler(RequestHandler):
def get(self):
cookie = self.get_secure_cookie("count")
count = int(cookie) + 1 if cookie else 1
self.set_secure_cookie("count", str(count))
self.write(
'<html><head><title>Cookie计数器</title></head>'
'<body><h1>您已访问本页%d次。</h1>' % count +
'</body></html>'
)
再建立一个网站 127.0.0.1:9000,
class IndexHandler(RequestHandler):
def get(self):
self.write('<html><head><title>被攻击的网站</title></head>'
'<body><h1>此网站的图片链接被修改了</h1>'
'<img alt="这应该是图片" src="http://127.0.0.1:8000/?f=9000/">'
'</body></html>'
)
在 9000 网站我们模拟攻击者修改了我们的图片源地址为 8000 网站的 Cookie 计数器页面网址。当我们访问 9000 网站的时候,在我们不知道、未授权的情况下 8000 网站的 Cookie 被使用了,以至于让 8000 网址认为是我们自己调用了 8000 网站的逻辑。这就是 CSRF(Cross-site request forgery)跨站请求伪造(跨站攻击或跨域攻击的一种),通常缩写为 CSRF 或者 XSRF。
我们刚刚使用的是 GET 方式模拟的攻击,为了防范这种方式的攻击,任何会产生副作用的 HTTP 请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用 HTTP POST 方法(或 PUT、DELETE)。但是,这并不足够:一个恶意站点可能会通过其他手段来模拟发送 POST 请求,保护 POST 请求需要额外的策略。
XSRF 保护#
{{<admonition tip “提示” true >}}
浏览器有一个很重要的概念 —— 同源策略 (Same-Origin Policy)。 所谓同源是指,域名,协议,端口相同。 不同源的客户端脚本 (javascript、ActionScript) 在没明确授权的情况下,不能读写对方的资源。
{{< /admonition >}}
由于第三方站点没有访问 cookie 数据的权限(同源策略),所以我们可以要求每个请求包括一个特定的参数值作为令牌来匹配存储在 cookie 中的对应值,如果两者匹配,我们的应用认定请求有效。而第三方站点无法在请求中包含令牌 cookie 值,这就有效地防止了不可信网站发送未授权的请求。
开启 XSRF 保护#
要开启 XSRF 保护,需要在 Application 的构造函数中添加 xsrf_cookies 参数:
app = tornado.web.Application(
[(r"/", IndexHandler),],
cookie_secret = "2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A=",
xsrf_cookies = True
)
当这个参数被设置时,Tornado 将拒绝请求参数中不包含正确的_xsrf 值的 POST、PUT 和 DELETE 请求。
class IndexHandler(RequestHandler):
def post(self):
self.write("hello itcast")
用不带_xsrf 的 post 请求时,报出了 HTTP 403: Forbidden ('_xsrf' argument missing from POST)
的错误。
模板应用#
在模板中使用 XSRF 保护,只需在模板中添加
module xsrf_form_html()
如新建一个模板 index.html
<!DOCTYPE html>
<html>
<head>
<title>测试XSRF</title>
</head>
<body>
<form method="post">
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
</body>
</html>
后端
class IndexHandler(RequestHandler):
def get(self):
self.render("index.html")
def post(self):
self.write("hello itcast")
模板中添加的语句帮我们做了两件事:
- 为浏览器设置了_xsrf 的 Cookie(注意此 Cookie 浏览器关闭时就会失效)
- 为模板的表单中添加了一个隐藏的输入名为_xsrf,其值为_xsrf 的 Cookie 值
渲染后的页面原码如下:
<!DOCTYPE html>
<html>
<head>
<title>测试XSRF</title>
</head>
<body>
<form method="post">
<input type="hidden" name="_xsrf" value="2|543c2206|a056ff9e49df23eaffde0a694cde2b02|1476443353"/>
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
</body>
</html>
非模板应用#
对于不使用模板的应用来说,首先要设置_xsrf 的 Cookie 值,可以在任意的 Handler 中通过获取 self.xsrf_token 的值来生成_xsrf 并设置 Cookie。
下面两种方式都可以起到设置_xsrf Cookie 的作用。
class XSRFTokenHandler(RequestHandler):
"""专门用来设置_xsrf Cookie的接口"""
def get(self):
self.xsrf_token
self.write("Ok")
class StaticFileHandler(tornado.web.StaticFileHandler):
"""重写StaticFileHandler,构造时触发设置_xsrf Cookie"""
def __init__(self, *args, **kwargs):
super(StaticFileHandler, self).__init__(*args, **kwargs)
self.xsrf_token
对于请求携带_xsrf 参数,有两种方式:
- 若请求体是表单编码格式的,可以在请求体中添加_xsrf 参数
- 若请求体是其他格式的(如 json 或 xml 等),可以通过设置 HTTP 头 X-XSRFToken 来传递_xsrf 值
请求体携带_xsrf 参数#
新建一个页面 xsrf.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试XSRF</title>
</head>
<body>
<a href="javascript:;" onclick="xsrfPost()">发送POST请求</a>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
//获取指定Cookie的函数
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
//AJAX发送post请求,表单格式数据
function xsrfPost() {
var xsrf = getCookie("_xsrf");
$.post("/new", "_xsrf="+xsrf+"&key1=value1", function(data) {
alert("OK");
});
}
</script>
</body>
</html>
HTTP 头 X-XSRFToken#
新建一个页面 json.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>测试XSRF</title>
</head>
<body>
<a href="javascript:;" onclick="xsrfPost()">发送POST请求</a>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
//获取指定Cookie的函数
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
//AJAX发送post请求,json格式数据
function xsrfPost() {
var xsrf = getCookie("_xsrf");
var data = {
key1:1,
key1:2
};
var json_data = JSON.stringify(data);
$.ajax({
url: "/new",
method: "POST",
headers: {
"X-XSRFToken":xsrf,
},
data:json_data,
success:function(data) {
alert("OK");
}
})
}
</script>
</body>
</html>
用户验证#
简介#
用户验证是指在收到用户请求后进行处理前先判断用户的认证状态(如登陆状态),若通过验证则正常处理,否则强制用户跳转至认证页面(如登陆页面)。
authenticated 装饰器#
为了使用 Tornado 的认证功能,我们需要对登录用户标记具体的处理函数。我们可以使用 @tornado.web.authenticated 装饰器完成它。当我们使用这个装饰器包裹一个处理方法时,Tornado 将确保这个方法的主体只有在合法的用户被发现时才会调用。
class ProfileHandler(RequestHandler):
@tornado.web.authenticated
def get(self):
self.write("这是我的个人主页。")
get_current_user () 方法#
装饰器 @tornado.web.authenticated 的判断执行依赖于请求处理类中的 self.current_user 属性,如果 current_user 值为假(None、False、0、”” 等),任何 GET 或 HEAD 请求都将把访客重定向到应用设置中 login_url 指定的 URL,而非法用户的 POST 请求将返回一个带有 403(Forbidden)状态的 HTTP 响应。
在获取 self.current_user 属性的时候,tornado 会调用 get_current_user () 方法来返回 current_user 的值。也就是说,我们验证用户的逻辑应写在 get_current_user () 方法中,若该方法返回非假值则验证通过,否则验证失败。
class ProfileHandler(RequestHandler):
def get_current_user(self):
"""在此完成用户的认证逻辑"""
user_name = self.get_argument("name", None)
return user_name
@tornado.web.authenticated
def get(self):
self.write("这是我的个人主页。")
login_url 设置#
当用户验证失败时,将用户重定向到 login_url 上,所以我们还需要在 Application 中配置 login_url。
class LoginHandler(RequestHandler):
def get(self):
"""在此返回登陆页面"""
self.write("登陆页面")
app = tornado.web.Application(
[
(r"/", IndexHandler),
(r"/profile", ProfileHandler),
(r"/login", LoginHandler),
],
"login_url":"/login"
)
在 login_url 后面补充的 next 参数就是记录的跳转至登录页面前的所在位置,所以我们可以使用 next 参数来完成登陆后的跳转。
修改登陆逻辑:
class LoginHandler(RequestHandler):
def get(self):
"""登陆处理,完成登陆后跳转回前一页面"""
next = self.get_argument("next", "/")
self.redirect(next+"?name=logined")