这一节我们介绍应用安全与认证,其实中间省略了一个数据库。对于tornado来说,读取数据库的数据,性能的瓶颈还是在数据库上面。关于数据库,我在<<web框架--flask>>中介绍了sqlalchemy,这是一个工业级的orm,可以看看,这里就不介绍了。直接进入今天的主题内容。
1.cookie
ookie是储存在客户端的键值对,保存了用户的信息。我们都知道http协议时无状态的,只知道有人链接就行进行交互,但是却不知道是谁。于是这个时候cookie就出现了,当我们第一次访问的时候,服务端就会创建一个cookie然后返回给我们,当我们下次再访问的时候就带着之前的cookie过去就行了,所以我们明明没有输入用户名和密码,却自动登录了,就是因为cookie。当我们换一个浏览器,或者清理垃圾,把cookie删除了,那么就又需要重新登录了。既然提到cookie就要说到session,session是我们人工引入的一个抽象概念,session的实现需要依赖于cookie。当我们的身份信息比较重要时,那么将信息存储在cookie中会不安全,所以session就出现了,服务端把不把敏感信息返回了,而是返回一个随机密串,这个随机密串是由当前的session经过序列化再加密得到的,也就是session id,将这个session id作为cookie值返回给客户端。当客户端下次再访问的时候,只需要带着session id过来就行,将session id解密反序列化,判断用户是否登录。所以session是我们人工引入的一个抽象概念,session的实现依赖于cookie。
那么在tornado中如何设置cookie呢?首先在tornado中,可以设置两种cookie,一个是普通cookie,另一种是加密cookie
设置普通cookie
设置普通cookie
self.set_cookie(name, value, domain=None, expires=None, path="/", expires_days=None, **kwargs)
参数:
name:cookies名
value:cookie值
domain:提交cookie时匹配的域名
path:提交cookie时匹配的路径
expires:cookie的有效期,可以是时间戳整数,时间元素,datetime类型。为UTC时间
expires_days:cookie的有效期天数,优先级低于expires
import tornado.web
class SatoriHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.set_cookie("satori", "love")
# 实际上set_cookie本质上是通过set_header来实现的
self.set_header("Set-Cookie", "koishi=love")
self.write("6666"
获取cookie
import tornado.web
class SatoriHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.set_cookie("satori", "love")
cookie = self.get_cookie("satori")
self.write(cookie)
清除cookie
self.clear_cookie(name, path="/", domain=None) # 删除名为name,同时匹配path和domain的cookie
self.clear_all_cookie(path="/", domain=None) # 删除同时匹配path和domain的所有cookie
注意:执行清除cookie的操作时,并不是tornado去删除浏览器上的cookie,而是将cookie的值设为空,并将有效时间改为失效。真正删除cookie是由浏览器进行操作的
此外再介绍一下HTTP-only和SSL-Cookies
Tornado的cookie功能依附于Python内建的Cookie模块。因此,我们可以利用它所提供的一些安全功能。这些安全属性是HTTP cookie规范的一部分,并在它可能是如何暴露其值给它连接的服务器和它运行的脚本方面给予浏览器指导。比如,我们可以通过只允许SSL连接的方式减少cookie值在网络中被截获的可能性。我们也可以让浏览器对JavaScript隐藏cookie值。
为cookie设置secure属性来指示浏览器只通过SSL连接传递cookie。(这可能会产生一些困扰,但这不是Tornado的安全cookies,更精确的说那种方法应该被称为签名cookies。)从Python 2.6版本开始,Cookie对象还提供了一个httponly属性。包括这个属性指示浏览器对于JavaScript不可访问cookie,这可以防范来自读取cookie值的跨站脚本攻击。
为了开启这些功能,你可以向set_cookie和set_secure_cookie方法传递关键字参数。比如,一个安全的HTTP-only cookie(不是Tornado的签名cookie)可以调用self.set_cookie('foo', 'bar', httponly=True, secure=True)发送。
设置安全cookie
首先需要设置一个随机密串用来给cookie进行混淆加密,然后写在配置文件的settings里,"cookie_secret": "xxxx"
import os
import uuid
import base64
cookie_secret = str(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), encoding="utf-8")
'''
cRi40+RmRy6iVoLoIeK03KMsjfu1T0hzonLWSEtDb2o=
'''
options = {"port": 7777}
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
settings = {"static_path": os.path.join(BASE_DIR, "static"),
"template_path": os.path.join(BASE_DIR, "templates"),
"static_url_prefix": "/static/",
"compiled_static_cache": True,
"compiled_template_cache": True,
"server_traceback": True,
"cookie_secret": cookie_secret # 要在settings中注册cookie_secret
}
然后使用self.set_secure_cookie(设置安全cookie)进行设置,可以防止cookie被伪造,参数和设置普通cookie是相同的。
import tornado.web
class SatoriHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.set_secure_cookie("satori", "love")
cookie = self.get_cookie("satori")
self.write(cookie)
可以看到浏览器中显示的不再是satori = love,而是"2|1:0|10:1535867275|6:satori|8:bG92ZQ==|e8ec0f270440d95b2d004e54633cf7730d9ab4ca3ea8ed5a6fffdbc7ea61b952"; expires=Tue, 02 Oct 2018 05:47:55 GMT; Path=/
说明:安全cookie使用的版本,默认使用版本2;默认为0;时间戳;cookie名;base64编码的cookie值;签名值,不带长度说明
获取安全cookie
self.get_secure_cookies(name, value=None, max_age_days=31, min_version=None), 当然self.get_cookie也是可以的。
name:cookie名称
value:如果获取不到返回None
max_age_days:不同于expires_days,expires_days表示设置浏览器中cookie的有效时间。而max_age_days是过滤安全cookie的时间戳。
但是安全cookie也不是绝对安全的,只是在一定程度增加了破解的难度,还是不要在cookie中存储敏感信息为好
2.xsrf
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
举个栗子:古明地恋这个时候正在浏览银行账户,古明地觉在古明地恋的站点上编写了一个取款的form表单提交的链接,并将此链接作为图片src,如果银行保存的古明地恋的cookie还没有过期,那么在点击图片的时候就会带上cookie将form表单提交,这样在古明地恋不知情的情况下便完成了取款
那么如何防范请求伪造呢?
有很多预防措施可以防止这种类型的攻击。首先你在开发应用时需要深谋远虑。任何会产生副作用的HTTP请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用HTTP POST方法。无论如何,这是良好的RESTful做法,但它也有额外的优势用于防范像我们刚才看到的恶意图像那样琐碎的XSRF攻击。但是,这并不足够:一个恶意站点可能会通过其他手段,如HTML表单或XMLHTTPRequest API来向你的应用发送POST请求。保护POST请求需要额外的策略。
为了防范伪造POST请求,我们会要求每个请求包括一个参数值作为令牌来匹配存储在cookie中的对应值。我们的应用将通过一个cookie头和一个隐藏的HTML表单元素向页面提供令牌。当一个合法页面的表单被提交时,它将包括表单值和已存储的cookie。如果两者匹配,我们的应用认定请求有效。
由于第三方站点没有访问cookie数据的权限,他们将不能在请求中包含令牌cookie。这有效地防止了不可信网站发送未授权的请求。正如我们看到的,Tornado同样会让这个实现变得简单。
xsrf保护:同源策源
开启xsrf保护,可以在配置中添加
模板中应用
可以form表单中添加{%module xsrf_form_html()%},当然也要在配置文件settings中注册"xsrf_cookies": True
作用:
1.为浏览器设置了名为_xsrf的cookie,这个cookie在关闭浏览器之后会失效。
2.为模板表单添加了一个隐藏的域,名为_xsrf,值为_xsrf的cookie值
非模板中应用
手动创建一个input,并将name的属性值设置为_xsrf,value值为_xsrf的cookie值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form>
<input type="hidden" id="hi" name="_xsrf" value="">
name:<input type="text" name="username">
passwd:<input type="password" name="passwd">
<input type="submit" value="submit">
</form>
<script>
function getCookie(name) {
var cook=document.cookie.match("\b"+name+"=([^;]*)\b");
return cook?cook[1]:undefined
}
document.getElementById("hi").value = getCookie("_xsrf")
</script>
</body>
</html>
发起ajax请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form>
name:<input type="text" name="username">
passwd:<input type="password" name="passwd">
<button onclick="login()">login</button>
</form>
<script>
function getCookie(name) {
var cook=document.cookie.match("\b"+name+"=([^;]*)\b");
return cook?cook[1]:undefined
}
function login() {
$.post("提交的地址",
"_xsrf="+getCookie("_xsrf")+"&username="+"用户名"+"&password="+"密码",
function(data){
alert("ok")
}
)
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form>
name:<input type="text" name="username">
passwd:<input type="password" name="passwd">
<button onclick="login()">login</button>
</form>
<script>
function getCookie(name) {
var cook=document.cookie.match("\b"+name+"=([^;]*)\b");
return cook?cook[1]:undefined
}
function login() {
data = {
"username": "xxxx",
"password": "xxxx",
};
var datastr = json.stringify(data);
$.ajax({
url: "提交的地址",
method: "POST",
data: datastr,
success: function(data){
alert("ok")
},
headers: {
"X-XSRFToken": getCookie("_xsrf")
}
})
}
</script>
</body>
</html>
需要手动添加_xsrf的cookie,需要在进入主页时就自动设置_xsrf的cookie,因此可以使用tornado.web下的StaticFileHandler。但是我们无法直接在StaticFileHandler中添加逻辑,因此我们想到可以自定义一个类,然后继承StaticFileHandler。
import tornado.web
class StaticFileHandler(tornado.web.StaticFileHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.xsrf_token
3.用户认证
作用:指在受到用户请求后,进行预先判断的用户状态(是否登录),若验证通过则正常处理,否则返回到登录页面
方法:tornado.web.authenticated装饰器,tornado将确保所装饰函数的主题只有合法的用户才能使用
get_current_user(),验证用户的逻辑应该写在该方法中,如果返回结果为True,验证成功,否则验证失败。当验证失败会将用户返回到指定的路由。这个指定的路由需要再配置文件的settings中进行设置,"login_url": "your_url"
import os
import uuid
import base64
cookie_secret = str(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), encoding="utf-8")
'''
cRi40+RmRy6iVoLoIeK03KMsjfu1T0hzonLWSEtDb2o=
'''
options = {"port": 7777}
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
settings = {"static_path": os.path.join(BASE_DIR, "static"),
"template_path": os.path.join(BASE_DIR, "templates"),
"static_url_prefix": "/static/",
"compiled_static_cache": True,
"compiled_template_cache": True,
"server_traceback": True,
"cookie_secret": cookie_secret, # 要在settings中注册cookie_secret
"xsrf_cookies": True,
"login_url": "/satori"
}
application.py
import tornado.web
from views import view
import config
import os
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/satori", view.SatoriHandler),
(r"/koishi", view.KoishiHandler),
(r"/mashiro", view.MashiroHandler),
(r"/(.*)$", view.StaticFileHandler, {"path": os.path.join(config.BASE_DIR,"static/html"),
"default_filename": "index.html"})
]
super(Application, self).__init__(handlers=handlers, **config.settings)
view.py
import tornado.web
class StaticFileHandler(tornado.web.StaticFileHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.xsrf_token
class SatoriHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write("<h1>satori</h1>")
class KoishiHandler(tornado.web.RequestHandler):
def get_current_user(self):
return None
@tornado.web.authenticated
def get(self, *args, **kwargs):
self.write("<h1>koishi</h1>")
class MashiroHandler(tornado.web.RequestHandler):
def get_current_user(self):
return True
@tornado.web.authenticated
def get(self):
self.write("<h1>mashiro</h1>")
我访问/koishi和/mashiro,会触发get请求,但是get请求被认证装饰器装饰,所以会执行get_current_user(),如果返回为True,那么执行get,如果返回为False,那么会重定向到配置文件中settings的"login_url"所对应的url
访问localhost:7777/koishi
当我访问/koishi, 由于验证不通过,所以跳转到了satori。而且/satori后面还跟着?next=%2Fkoishi,也说明是由/koishi跳转过来的
但我访问/mashiro,由于验证通过,所以执行对应的get请求,显示mashiro
关于应用安全与认证就介绍到这里,下一节将介绍tornado高性能的第二个杀手锏(第一个是基于epoll的IO多路复用),异步