Tornado - 应用安全
普通 Cookie
设置
原型
1
2
3
4
5
6
7
8
9
10self.set_cookie(
self,
name: str,
value: Union[str, bytes],
domain: str = None,
expires: Union[float, Tuple, datetime.datetime] = None,
path: str = "/",
expires_days: int = None,
**kwargs: Any
)
参数
name
:cookie 名value
:cookie 值domain
:提交 cookie 时匹配的域名path
:提交 cookie 时匹配的路径expires
:cookie 的有效期,可以是时间戳整数、时间元组、datetime 类型;为 UTC 时间。expires_days
:cookie 的有效期天数,优先级低于 expires
示例
1
2
3
4
5
6class PCookieHandler(RequestHandler):
def get(self, *args, **kwargs):
# 设置
self.set_cookie("sunck", "good")
# self.set_header("Set-Cookie", "kaige=nice; Path=/")
self.write("ok")
原理
设置 cookie 实际上是通过设置 header 的 Set-Cookie 来实现的
1
self.set_header("Set-Cookie", "kaige=nice; Path=/")
获取
原型
1
self.get_cookie(self, name: str, default: str = None)
参数
name
:要获取的 cookie 的名称default
:如果名为 name 的 cookie 不存在,则返回 default 的值
示例
1
2
3
4
5
6class GetPCookieHandler(RequestHandler):
def get(self, *args, **kwargs):
# 获取cookie
cookie = self.get_cookie("sunck", "未登录")
print("cookie =", cookie)
self.write("ok")
清除
self.clear_cookie()
:删除名为 name,并同时匹配 domain 和 path 的 cookie1
self.clear_cookie(self, name: str, path: str = "/", domain: str = None)
self.clear_all_cookies()
:删除同时匹配 path 和 domain 的所有 cookie1
self.clear_all_cookies(self, path: str = "/", domain: str = None)
⚠️注意:执行清除 cookie 操作后,并不是立即删除浏览器端的 cookie,而是给 cookie 值设置空,并改变其有限期限为失效。真正删除 cookie 是在关闭浏览器时浏览器自己去清理的。
示例
1
2
3
4
5
6
7class ClearPCookieHandler(RequestHandler):
def get(self, *args, **kwargs):
# 清除一个cookie
# self.clear_cookie("sunck")
# 清除所有cookie
self.clear_all_cookies()
self.write("ok")
安全 Cookie
概述
- Cookie 是存在客户端浏览器的数据,很容易被篡改
- Tornado 提供了一种对 Cookie 进行简易加密方式来防止 Cookie 被恶意篡改
设置
需要为应用配置一个用来给 Cookie 进行混淆加密的秘钥
1
2
3
4
5
6settings = {
'debug': True,
'static_path': os.path.join(BASE_DIRS, 'static'),
'template_path': os.path.join(BASE_DIRS, 'templates'),
'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0=',
}'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0='
生成一个秘钥
1
2
3
4
5import base64
import uuid
secret = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
print(secret)1
b'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0='
设置安全 Cookie
原型
1
2
3
4
5
6
7
8self.set_secure_cookie(
self,
name: str,
value: Union[str, bytes],
expires_days: int = 30,
version: int = None,
**kwargs: Any
)作用:设置一个带有签名和时间戳的 cookie,防止 cookie 被伪造
示例
1
2
3
4class SCookieHandler(RequestHandler):
def get(self, *args, **kwargs):
self.set_secure_cookie("zhangmanyu", "nice")
self.write("ok")查看浏览器端的 cookie 值
1
"2|1:0|10:1596855751|10:zhangmanyu|8:bmljZQ==|3d8c5a68a3ca2b7aacd073e317bf5c664ab53e6a67b8dda6f7b9b3a3c307a073"
说明
|1:0|
:1 表示:
后边有多少位;0 表示正真的数值;|
表示分隔符- 安全 cookie 的版本,默认使用版本 2
- 默认为 0
- 时间戳
- cookie 名
- base64 编码的 cookie 值
- 签名值,不带长度说明
获取
原型
1
2
3
4
5
6
7self.get_secure_cookie(
self,
name: str,
value: str = None,
max_age_days: int = 31,
min_version: int = None,
)- 如果 cookie 存在且验证通过,返回 cookie 值,反则返回 None
- max_age_days 不同于 expires_days,expires_days 设置浏览器中 cookie 的有效时间;而 max_age_days 是过滤安全 cookie 的时间戳
示例
1
2
3
4
5class GetSCookieHandler(RequestHandler):
def get(self, *args, **kwargs):
scookie = self.get_secure_cookie("zhangmanyu")
print("scookie =", scookie)
self.write("ok")
⚠️注意
- 也不是完全安全的,一定程度上增加了破解 cookie 的难度
- 以后 cookie 不要存储一些敏感性的数据
跨站请求伪造
cookie 计数
1
2
3
4
5
6
7
8
9
10class CookieNumHandler(RequestHandler):
def get(self, *args, **kwargs):
count = self.get_cookie("count", None)
if not count:
count = 1
else:
count = int(count)
count += 1
self.set_cookie("count", str(count))
self.render('cookienum.html', count=count)1
2
3
4
5
6
7
8
9
10
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cookie计数</title>
</head>
<body>
<h1>第{{ count }}访问</h1>
</body>
</html>跨站请求
1
2
3
4
5
6
7
8
9
10
11
<html lang="en">
<head>
<meta charset="utf-8">
<title>搞事情</title>
</head>
<body>
<img src="http://127.0.0.1:8848/cookienum">
<h1>去看看吧,我可能把你搞坏了!</h1>
</body>
</html>说明
- 当访问 “搞事情” 网站时,在我们不知情、为授权的情况下,“cookie 计数器” 网站 cookie 被使用,以至于 “cookie 计数器” 网址认为是它自己调用 Handler 的逻辑
- 上一个程序使用的是 GET 方式模拟的攻击,为了防止这种攻击,一般对于相对安全的操作一般是不放在 GET 请求中,常常使用的是 POST 请求
XSRF 保护
原理:同源策略,即:
- 协议相同
- 域名相同
- 端口号相同
图示
开启 XSRF 保护
1 | settings = { |
在配置文件
config.py
中添加'xsrf_cookies': True
应用
模板中应用
{% module xsrf_form_html() %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSRF</title>
</head>
<body>
<form action="/postfile" method="post">
{% module xsrf_form_html() %}
姓名:<input type="text" name="username"/>
<hr/>
密码:<input type="password" name="passwd"/>
<hr/>
<input type="submit" value="登陆"/>
</form>
</body>
</html>- 作用
- 为浏览器设置了
_xsrf
的安全 cookie,这个 cookie 在关闭浏览器后会失效 - 为模板表单添加了一个隐藏的域,名为
_xsrf
,值为_xsrf
这个 cookie 的值1
<input type="hidden" name="_xsrf" value="2|b4826962|86075754385b9af629ccee663023729a|1596862208">
- 为浏览器设置了
非模板中应用
- 手动设置
_xsrf
的 cookie1
2
3
4
5class SetXSRFCookie(RequestHandler):
def get(self, *args, **kwargs):
# 设置_xsrf的cookie
self.xsrf_token
self.finish("Ok") - 第一种:基本上不会用
手动创建的 input,并设置 name 属性值为_xsrf
,value 属性值为名为_xsrf
的 cookie 的值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSRF</title>
</head>
<body>
<form action="/postfile" method="post">
<input id="hi" type="hidden" name="_xsrf" value="">
姓名:<input type="text" name="username"/>
<hr/>
密码:<input type="password" name="passwd"/>
<hr/>
<input type="submit" value="登陆"/>
</form>
<script>
function getCookie(name) {
var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
return cook ? cook[1] : undefined
}
console.log(getCookie('_xsrf'))
document.getElementById("hi").value = getCookie("_xsrf")
</script>
</body>
</html> - 第二种:
$.post
发起 Ajax 请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSRF</title>
<script type="text/javascript" charset="UTF-8" src="{{ static_url('js/jquery.min.js') }}"></script>
</head>
<body>
姓名:<input type="text" name="username"/>
<hr/>
密码:<input type="password" name="passwd"/>
<hr/>
<button onclick="login()">登陆</button>
<script>
function getCookie(name) {
var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
return cook ? cook[1] : undefined
}
function login() {
// _xsrf=wertyu&username=sunck&passwd=23456
$.post("/postfile", "_xsrf=" + getCookie('_xsrf') + "&username=" + "sunck" + "&passwd=" + "123456789", function (data) {
alert("OK")
})
}
</script>
</body>
</html> - 第三种:
$.ajax
发起 Ajax 请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSRF</title>
<script type="text/javascript" charset="UTF-8" src="{{ static_url('js/jquery.min.js') }}"></script>
</head>
<body>
姓名:<input type="text" name="username"/>
<hr/>
密码:<input type="password" name="passwd"/>
<hr/>
<button onclick="login()">登陆</button>
<script>
function getCookie(name) {
var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
return cook ? cook[1] : undefined
}
function login() {
data = {
"username": "sunck",
"passwd": "123456"
}
var datastr = JSON.stringify(data)
$.ajax({
url: "/postfile",
method: "POST",
data: datastr,
success: function (data) {
alert("OK")
},
headers: {
"X-XSRFToken": getCookie("_xsrf")
}
})
}
</script>
</body>
</html> - 问题
需要手动添加_xsrf
的 cookie,需要在进入主页时就自动设置上_xsrf
的 cookie1
2(r'/(.*)$', index.StaticFileHandler,
{"path": os.path.join(config.BASE_DIRS, "static/html"), "default_filename": "index.html"})1
2
3
4class StaticFileHandler(tornado.web.StaticFileHandler):
def __init__(self, *args, **kwargs):
super(StaticFileHandler, self).__init__(*args, **kwargs)
self.xsrf_token1
2
3
4
5
6
7
8
9
10
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<h1>这是主页</h1>
</body>
</html>static -> html -> index.html
- 手动设置
- 指在收到用户请求后进行预先判断用户的认证状态 (是否登录),若验证通过则正常处理,否则进如到登录界面
tornado.web.authenticated
装饰器,Tornado 将确保这个方法的主体只有合法的用户才能调用get_current_user()
- 验证用户的逻辑应该写在该方法中,如果该方法返回的为 True 说明验证成功,否则验证失败
- 验证失败,请求会将访客重定向到配置中的
login_url
所指定的路由1
2
3
4
5
6
7
8settings = {
'debug': False,
'xsrf_cookies': True,
'login_url': '/login',
'static_path': os.path.join(BASE_DIRS, 'static'),
'template_path': os.path.join(BASE_DIRS, 'templates'),
'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0=',
}'login_url': '/login'
- 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="{{ url }}" method="post">
{% module xsrf_form_html() %}
姓名:<input type="text" name="username"/>
<hr/>
密码:<input type="password" name="passwd"/>
<hr/>
<input type="submit" value="登陆"/>
</form>
</body>
</html>templates -> login.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37class LoginHandler(RequestHandler):
def get(self, *args, **kwargs):
next = self.get_argument("next", "/")
url = "login?next=" + next
self.render("login.html", url=url)
def post(self, *args, **kwargs):
name = self.get_argument("username")
passwd = self.get_argument("passwd")
if name == "1" and passwd == "1":
next = self.get_argument("next", "/")
self.redirect(next + "?flag=logined")
else:
next = self.get_argument("next", "/")
self.redirect("/login?next=" + next)
class HomeHandler(RequestHandler):
def get_current_user(self):
# /home
flag = self.get_argument("flag", None)
return flag
def get(self, *args, **kwargs):
self.render("home.html")
class CartHandler(RequestHandler):
def get_current_user(self):
# /home
flag = self.get_argument("flag", None)
return flag
def get(self, *args, **kwargs):
self.render("cart.html")index.py