6.1 Cookie
对于RequestHandler,除了在第二章中讲到的之外,还提供了操作cookie的方法。
设置/获取
注意:Cookie 在浏览器调试时, 只有在第一次访问该网站的时候获取到时才会在Response Cookies 里面体现。如果之后该网站的Cookie信息没有变更,则重复请求时不会在Response Cookies体现。因为浏览器已经缓存了之前访问的cookie。下次请求时就会带上这个cookie。当浏览器看到第二次访问返回的Cookie有和之前缓存的一样的。所以就不在返回体中 Response Cookies里面体现了,只有Cookie中新增的key=value才会在Response Cookies体现
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 |
class SecureTestHandler(web.RequestHandler):
'''
定义请求处理类
'''
def get(self, *args, **kwargs):
'''测试安全性,手动设置cookie
'''
'''给模板传递变量'''
house_info = {
"price": 198,
"title": "宽窄巷子+160平大空间+文化保护区双地铁",
"score": 5,
"comments": 6,
"position": "北京市丰台区六里桥地铁"
}
print('SecureTestHandler')
self.set_cookie('name','zmd',path='/secure',expires=time.strptime('2020-08-06 23:59:59',"%Y-%m-%d %H:%M:%S"))
self.set_cookie('sex','man',path='/sex',expires_days=20)
# #转换为UTC时间
self.set_cookie('age','30',expires=time.mktime(time.strptime('2021-08-06 23:59:59',"%Y-%m-%d %H:%M:%S")))
name = self.get_cookie(name='name',)# 能够成功获取cookie的前提是,设置给浏览器的cookie 未过期。如果已经过期。浏览器不会报错。所以会获取失败
print(name,type(name))
self.write('get_cookie name =%a '%name)
self.render('templateSecureIndexTest.html',**house_info)
需要注意:cookie的时间有效浏览器才会保存,下次访问才会带上,如果时间过期浏览器不会保存
清除
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")
注意:执行清除cookie操作后,并不是立即删除了浏览器中的cookie,而是给cookie值置空,并改变其有效期使其失效。真正的删除cookie是由浏览器去清理的。
安全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='
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进制字符串。
将生成的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 SecureCookieHandler(web.RequestHandler): def get(self, *args, **kwargs):
'''get_secure_cookie:如果cookie存在且验证通过,返回cookie的值,否则返回None。max_age_day不同于expires_days,expires_days是设置浏览器中cookie的有效期,而max_age_day是过滤安全cookie的时间戳。'''
cookie_value = self.get_secure_cookie('count')
print('cookie_value :' ,cookie_value)
count = int(cookie_value) +1 if cookie_value else 1
self.set_secure_cookie("count", str(count)) #设置一个带签名和时间戳的cookie,防止cookie被伪造。
self.write(
'<html><head><title>Cookie计数器</title></head>'
'<body><h1>您已访问本页%d次。</h1>'
'</body></html>' % count
)
我们看签名后的cookie值:
"2|1:0|10:1476412069|5:count|4:NQ==|cb5fc1d4434971de6abf87270ac33381c686e4ec8c6f7e62130a0f8cbe5b7609"
字段说明:
- 安全cookie的版本,默认使用版本2,不带长度说明前缀
- 默认为0
- 时间戳
- cookie名
- base64编码的cookie值
- 签名值,不带长度说明前缀
注意:Tornado的安全cookie只是一定程度的安全,仅仅是增加了恶意修改的难度。Tornado的安全cookies仍然容易被窃听,而cookie值是签名不是加密,攻击者能够读取已存储的cookie值,并且可以传输他们的数据到任意服务器,或者通过发送没有修改的数据给应用伪造请求。因此,避免在浏览器cookie中存储敏感的用户数据是非常重要的
---------------------------------------------------------------------------------------------------------------------------------------------------------
6.2 XSRF 跨站请求伪造
诞生背景
先建立一个网站http://127.0.0.1/secureCookie,使用上一节中的Cookie计数器:
def get(self, *args, **kwargs):
'''get_secure_cookie:如果cookie存在且验证通过,返回cookie的值,否则返回None。max_age_day不同于expires_days,expires_days是设置浏览器中cookie的有效期,而max_age_day是过滤安全cookie的时间戳。'''
cookie_value = self.get_secure_cookie('count')
print('cookie_value :' ,cookie_value)
count = int(cookie_value) +1 if cookie_value else 1
self.set_secure_cookie("count", str(count)) #设置一个带签名和时间戳的cookie,防止cookie被伪造。
self.write(
'<html><head><title>Cookie计数器</title></head>'
'<body><h1>您已访问银行页面%d次。</h1>'
'</body></html>' % count
)
手动访问3次
再建立一个网站http://127.0.0.1:8080/modified,模拟被修改的网站页面,此页面被修改:在html的<img></img>标签src参数中嵌入了被攻击而且有漏洞的网站http://127.0.0.1/secure。在此页面访问N次,然后再看访问http://127.0.0.1/secureCookie一次看看是不是“您已访问银行页面4次”
class ModifiedHandler(RequestHandler):
'''模拟被修改的网页'''
def get(self, *args, **kwargs):
self.write('<html><head><title>被攻击的网站</title></head>'
'<body><h1>此网站的图片链接被修改了</h1>'
'<img alt="这应该是图片" src="http://127.0.0.1/secureCookie/?f=8080/">'
'</body></html>')
【突然发现我们这段get逻辑被偷偷执行了好几次。咱们明明手动只访问了4次,结果变成了14次--证明Cookie被盗用了----这就是跨站请求伪造】
在http://127.0.0.1:8080/modified网站我们模拟攻击者修改了我们的图片源地址为http://127.0.0.1/secureCookie网站的Cookie计数器页面网址。当我们访问http://127.0.0.1:8080/modified网站的时候,在我们不知道、未授权的情况下/secureCookie网站的Cookie被使用了,以至于让/secureCookie网址认为是我们自己调用了/secureCookie网站的逻辑。这就是CSRF(Cross-site request forgery)跨站请求伪造(跨站攻击或跨域攻击的一种),通常缩写为CSRF或者XSRF。
我们刚刚使用的是GET方式模拟的攻击,为了防范这种方式的攻击,任何会产生副作用的HTTP请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用HTTP POST方法(或PUT、DELETE)。但是,这并不足够:一个恶意站点可能会通过其他手段来模拟发送POST请求,保护POST请求需要额外的策略。
/secureCookie如何防范:1、对于记录数据等行为放到Post请求里面处理。
2、开启XSRF保护 自动应用于POST请求中【浏览器同源策略,不同源的访问不了另一个源里面的cookie的值】
===========如此防护!!==========
开启XSRF保护 ----POST请求专属功能
浏览器有一个很重要的概念——同源策略(Same-Origin Policy)。 所谓同源是指,域名,协议,端口相同。 不同源的客户端脚本(javascript、ActionScript)在没明确授权的情况下,不能读写对方的资源。
由于第三方站点没有访问cookie数据的权限(同源策略),所以我们可以要求每个请求包括一个特定的参数值作为令牌来匹配存储在cookie中的对应值,如果两者匹配,我们的应用认定请求有效。而第三方站点无法在请求中包含令牌cookie值,这就有效地防止了不可信网站发送未授权的请求。
开启XSRF保护
要开启XSRF保护,需要在Application的构造函数中添加xsrf_cookies = True参数:
app = web.Application(urls,
static_path=static_path,#给html中css js文件指定的获取路径=html中/static/ 这个url
template_path=os.path.join(BASE_DIR,'templates'),
# autoescape=None,
cookie_secret="2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A=", #base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
xsrf_cookies = True, #在post请求中起效 开启XSRF保护
debug=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的值----------------------------
模板应用
在模板中使用XSRF保护,只需在模板的form表单中添加 {% module xsrf_form_html() %}
如新建一个模板UseXsrfInTemplate.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模板中应用XSRF传递_xsrf隐藏变量</title>
</head>
<body>
<form method="post">
{% module xsrf_form_html() %}
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
</body>
</html>
后端
class UseXsrfInTemplateHandler(RequestHandler):
'''开启XSRF防范跨站请求攻击机制'''
def get(self, *args, **kwargs):
'''get资源时将_xsrf 值 通过html表单的形式传递给客户端,客户端只能在本次get到的页面中预设好的请求动作中使用到这个值'''
self.render("UseXsrfInTemplateIndex.html")
def post(self, *args, **kwargs):
self.write("Cookie中 _xsrf值 == form 表单中_xsrf值!! POST 请求成功!")
----------------整体访问过程----------------------------
----------结果拒绝了:form表单中POST的时候没有提供_XSRF的值进行校验!!!-----------------------
----------人为提供校验,或者使用原始网页进行提交时会自带服务端提供的_xsrf值所以会成功放行-----------------
---------------------------------------------------------------------------------------------------
原理分析:模板中添加 {% module xsrf_form_html() %}
语句帮我们做了两件事:
- 为浏览器设置了_xsrf的Cookie(注意此Cookie浏览器关闭时就会失效)
- 为模板的表单中添加了一个隐藏的输入名为_xsrf,其值为_xsrf的Cookie值
渲染后的页面原码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模板中应用XSRF传递_xsrf隐藏变量</title>
</head>
<body>
<form method="post">
<input type="hidden" name="_xsrf" value="2|6291273e|900a52732ce317bc7765d06e0ba8ab48|1583643244"/>
<input type="text" name="message"/>
<input type="submit" value="Post"/>
</form>
</body>
</html>
非模板应用之设置_xsrf:
设置_xsrf的Cookie:
对于不使用模板的应用来说,可以在任意的Handler中通过RequestHandler.xsrf_token来生成_xsrf并设置Cookie。
前端携带_xsrf方法:
前端只能通过js读取cookie的值,然后携带获取到的_xsrf 进行POST请求
设置举例
下面两种写法任选其一都可以起到设置_xsrf Cookie的作用(注意:仅在调用过self.xsrf_token的Handler中起效)。
class UseXsrfNotInTemplateHandler(RequestHandler):
'''专门用来设置_xsrf Cookie 的接口'''
def get(self, *args, **kwargs):
self.xsrf_token
self.write('RequestHandler.xsrf_token 在Cookie里设置了_xsrf值')
def post(self, *args, **kwargs):
'''测试js获取_xsrf Ajax 发起POST请求验证
:param args:
:param kwargs:
:return:
'''
self.write('Wlecome POST Data!! xsrf_token successful authentication!!! ')
class StaticFileHandler(StaticFileHandler):
"""重写StaticFileHandler,构造时触发设置_xsrf Cookie"""
def __init__(self, *args, **kwargs):
super(StaticFileHandler, self).__init__(*args, **kwargs)
self.xsrf_token
非模板应用之获取和传递_xsrf 值: ----------------->【同源网页中的JS浏览器会允许访问Cookie中的值】
对于请求携带_xsrf参数,有两种方式:
- 若请求体是表单编码格式的,可以在请求体中添加_xsrf参数
- 若请求体是其他格式的(如json或xml等),可以通过设置HTTP头X-XSRFToken来传递_xsrf值
1. 请求体携带_xsrf参数
新建一个页面UseXsrfNotInTemplateGet_xsrfFromCookie.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>js获取_sxrf 从Cookie</title>
</head>
<body>
<a href="/useXsrfNotInTemplate" 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; //如果r存在就取下标为1的值,如果不存在就置为undefined
}
var xsrf = getCookie("_xsrf");
console.log(xsrf);
//AJAX发送post请求,表单格式数据
function xsrfPost() {
$.post("http://127.0.0.1/useXsrfNotInTemplate", "_xsrf="+xsrf+"&key1=value1", function(data) {
alert(data);
});
}
</script>
</body>
</html>
2. HTTP头X-XSRFToken
新建一个页面json.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>js获取_sxrf Ajax 提交</title>
</head>
<body>
<input type="button" VALUE="发送POST请求" href="/useXsrfNotInTemplate" onclick="xsrfPost()"></input>
<script src="https://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; //如果r存在就取下标为1的值,如果不存在就置为undefined
} //AJAX发送post请求,表单格式数据
function xsrfPost() {
var xsrf = getCookie("_xsrf");
console.log(xsrf);
var data = {
key1:'value1',
key2:"value2"
};
var jsondata = JSON.stringify(data);
$.ajax({
url:'/useXsrfNotInTemplate',
method:'POST',
headers: { "X-XSRFToken":xsrf}, //将获取的数据通过ajax提交,使用"X-XSRFToken"头携带
data:jsondata,
success:function (data) {
alert("ajax 提交成功 " + data)
}
})
}
</script>
</body>
</html>
=====================用户登录验证机制=======authenticated装饰器=========================
6.3 用户验证
用户验证是指在收到用户请求后进行处理前先判断用户的认证状态(如登陆状态),若通过验证则正常处理,否则强制用户跳转至认证页面(如登陆页面)。
authenticated装饰器
为了使用Tornado的认证功能,我们需要对登录用户标记具体的处理函数。我们可以使用@tornado.web.authenticated装饰器完成它。当我们使用这个装饰器包裹一个处理方法时,Tornado将确保这个方法的主体只有在合法的用户被发现时才会调用。
class AuthenticatedIndexHandler(web.RequestHandler):
def get_current_user(self):
next = self.get_argument('logined',None)
return next
@web.authenticated
def get(self, *args, **kwargs):
self.write('This is Authenticated Index.html <br> 您已登录,欢迎访问主页!')
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()方法中,若该方法返回非假值则验证通过,否则验证失败。
login_url设置
当用户验证失败时,将用户重定向到login_url上,所以我们还需要在Application中配置login_url。
app = web.Application(urls,
static_path=static_path,#给html中css js文件指定的获取路径=html中/static/ 这个url
template_path=os.path.join(BASE_DIR,'templates'),
# autoescape=None,
cookie_secret="2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A=", #base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
xsrf_cookies = True, #在post请求中起效
login_url='/login', #登录认证失败的跳转GET界面
debug=True)
想一想,完成登陆操作后应该进入哪个页面?
在login_url后面补充的next参数就是记录的跳转至登录页面前的所在位置,所以我们可以使用next参数来完成登陆后的跳转。
修改登陆逻辑:
class LoginHandler(RequestHandler):
def get(self):
"""登陆处理,完成登陆后跳转回前一页面"""
next = self.get_argument("next", "/")
print(next)
self.write('正在尝试登陆')
self.redirect(next+"?logined=True")