模板继承
用前面的方法,已经能够很顺利地编写模板了。读者如果留心一下,会觉得每个模板都有相同的部分内容。在python中,有一种被称之为“继承”的机制(请阅读本教程第贰季第肆章中的[类(4)(./209.md)中有关“继承”讲述]),它的作用之一就是能够让代码重用。
在tornado的模板中,也能这样。
先建立一个文件,命名为base.html,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Learning Python</title>
</head>
<body>
<header>
{% block header %}{% end %}
</header>
<content>
{% block body %}{% end %}
</content>
<footer>
{% set website = "<a href='http://www.itdiffer.com'>welcome to my website</a>" %}
{% raw website %}
</footer>
<script src="{{static_url("js/jquery.min.js")}}"></script>
<script src="{{static_url("js/script.js")}}"></script>
</body>
</html>
接下来就以base.html为父模板,依次改写已经有的index.html和user.html模板。
index.html代码如下:
{% extends "base.html" %}
{% block header %}
<h2>登录页面</h2>
<p>用用户名为:{{user}}登录</p>
{% end %}
{% block body %}
<form method="POST">
<p><span>UserName:</span><input type="text" id="username"/></p>
<p><span>Password:</span><input type="password" id="password" /></p>
<p><input type="BUTTON" value="登录" id="login" /></p>
</form>
{% end %}
user.html的代码如下:
{% extends "base.html" %}
{% block header %}
<h2>Your informations are:</h2>
{% end %}
{% block body %}
<ul>
{% for one in users %}
<li>username:{{one[1]}}</li>
<li>password:{{one[2]}}</li>
<li>email:{{one[3]}}</li>
{% end %}
</ul>
{% end %}
看以上代码,已经没有以前重复的部分了。{% extends "base.html" %}
意味着以base.html为父模板。在base.html中规定了形式如同{% block header %}{% end %}
这样的块语句。在index.html和user.html中,分别对块语句中的内容进行了重写(或者说填充)。这就相当于在base.html中做了一个结构,在子模板中按照这个结构填内容。
CSS
基本上的流程已经差不多了,如果要美化前端,还需要使用css,它的使用方法跟js类似,也是在静态目录中建立文件即可。然后把下面这句加入到base.html的<head></head>
中:
<link rel="stylesheet" type="text/css" href="{{static_url("css/style.css")}}">
当然,要在style.css中写一个样式,比如:
body {
color:red;
}
然后看看前端显示什么样子了,我这里是这样的:
关注字体颜色。
至于其它关于CSS方面的内容,本教程就不重点讲解了。读者可以参考关于CSS的资料。
至此,一个简单的基于tornado的网站就做好了,虽然它很丑,但是它很有前途。因为读者只要按照上述的讨论,可以在里面增加各种自己认为可以增加的内容。
建议读者在上述学习基础上,可以继续完成下面的几个功能:
- 用户注册
- 用户发表文章
- 用户文章列表,并根据文章标题查看文章内容
- 用户重新编辑文章
在后续教程内容中,也会涉及到上述功能。
cookie和安全
cookie是现在网站重要的内容,特别是当有用户登录的时候。所以,要了解cookie。维基百科如是说:
关于cookie的作用,维基百科已经说的非常详细了(读者还能正常访问这么伟大的网站吗?):
和任何别的事物一样,cookie也有缺陷,比如来自伟大的维基百科也列出了三条:
- cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
- 由于在HTTP请求中的cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
- Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。
对于用户来讲,可以通过改变浏览器设置,来禁用cookie,也可以删除历史的cookie。但就目前而言,禁用cookie的可能不多了,因为她总要在网上买点东西吧。
Cookie最让人担心的还是由于它存储了用户的个人信息,并且最终这些信息要发给服务器,那么它就会成为某些人的目标或者工具,比如有cookie盗贼,就是搜集用户cookie,然后利用这些信息进入用户账号,达到个人的某种不可告人之目的;还有被称之为cookie投毒的说法,是利用客户端的cookie传给服务器的机会,修改传回去的值。这些行为常常是通过一种被称为“跨站指令脚本(Cross site scripting)”(或者跨站指令码)的行为方式实现的。伟大的维基百科这样解释了跨站脚本:
cookie是好的,被普遍使用。在tornado中,也提供对cookie的读写函数。
set_cookie()
和get_cookie()
是默认提供的两个方法,但是它是明文不加密传输的。
在index.py文件的IndexHandler类的post()方法中,当用户登录,验证用户名和密码后,将用户名和密码存入cookie,代码如下:
def post(self):
username = self.get_argument(“username”)
password = self.get_argument(“password”)
user_infos = mrd.select_table(table=”users”,column=”*”,condition=”username”,value=username)
if user_infos:
db_pwd = user_infos[0][2]
if db_pwd == password:
self.set_cookie(username,db_pwd) #设置cookie
self.write(username)
else:
self.write(“your password was not right.”)
else:
self.write(“There is no thi user.”)
上面代码中,较以前只增加了一句self.set_cookie(username,db_pwd)
,在回到登录页面,等候之后就成为:
看图中箭头所指,从左开始的第一个是用户名,第二个是存储的该用户密码。将我在登录是的密码就以明文的方式存储在cookie里面了。
明文存储,显然不安全。
tornado提供另外一种安全的方法:set_secure_cookie()和get_secure_cookie(),称其为安全cookie,是因为它以明文加密方式传输。此外,跟set_cookie()的区别还在于, set_secure_cookie()执行后的cookie保存在磁盘中,直到它过期为止。也是因为这个原因,即使关闭浏览器,在失效时间之间,cookie都一直存在。
要是用set_secure_cookie()方法设置cookie,要先在application.py文件的setting中进行如下配置:
setting = dict(
template_path = os.path.join(os.path.dirname(__file__), "templates"),
static_path = os.path.join(os.path.dirname(__file__), "statics"),
cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
)
其中cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
是为此增加的,但是,它并不是这正的加密,仅仅是一个障眼法罢了。
因为tornado会将cookie值编码为Base-64字符串,并增加一个时间戳和一个cookie内容的HMAC签名。所以,cookie_secret的值,常常用下面的方式生成(这是一个随机的字符串):
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes)
'w8yZud+kRHiP9uABEXaQiA=='
如果嫌弃上面的签名短,可以用base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
获取。这里得到的是一个随机字符串,用它作为 cookie_secret值。
然后修改index.py中设置cookie那句话,变成:
self.set_secure_cookie(username,db_pwd)
从新跑一个,看看效果。
啊哈,果然“密”了很多。
如果要获取此cookie,用self.get_secure_cookie(username)
即可。
这是不是就安全了。如果这样就安全了,你太低估黑客们的技术实力了,甚至于用户自己也会修改cookie值。所以,还不安全。所以,又有了httponly和secure属性,用来防范cookie投毒。设置方法是:
self.set_secure_cookie(username, db_pwd, httponly=True, secure=True)
要获取cookie,可以使用self.set_secure_cookie(username)
方法,将这句放在user.py中某个适合的位置,并且可以用print语句打印出结果,就能看到变量username对应的cookie了。这时候已经不是那个“密”过的,是明文显示。
用这样的方法,浏览器通过SSL连接传递cookie,能够在一定程度上防范跨站脚本攻击。
XSRF
XSRF的含义是Cross-site request forgery,即跨站请求伪造,也称之为”one click attack”,通常缩写成CSRF或者XSRF,可以读作”sea surf”。这种对网站的攻击方式跟上面的跨站脚本(XSS)似乎相像,但攻击方式不一样。XSS利用站点内的信任用户,而XSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,XSRF攻击往往不大流行(因此对其 进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
读者要详细了解XSRF,推荐阅读:CSRF | XSRF 跨站请求伪造
对于防范XSRF的方法,上面推荐阅读的文章中有明确的描述。还有一点需要提醒读者,就是在开发应用时需要深谋远虑。任何会产生副作用的HTTP请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用post()方法。这是良好的RESTful做法。
此外,在tornado中,还提供了XSRF保护的方法。
在application.py文件中,使用xsrf_cookies参数开启XSRF保护。
setting = dict(
template_path = os.path.join(os.path.dirname(__file__), "templates"),
static_path = os.path.join(os.path.dirname(__file__), "statics"),
cookie_secret = "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
xsrf_cookies = True,
)
这样设置之后,Tornado将拒绝请求参数中不包含正确的_xsrf
值的post/put/delete请求。tornado会在后面悄悄地处理_xsrf
cookies,所以,在表单中也要包含XSRF令牌以却表请求合法。比如index.html的表单,修改如下:
{% extends "base.html" %}
{% block header %}
<h2>登录页面</h2>
<p>用用户名为:{{user}}登录</p>
{% end %}
{% block body %}
<form method="POST">
{% raw xsrf_form_html() %}
<p><span>UserName:</span><input type="text" id="username"/></p>
<p><span>Password:</span><input type="password" id="password" /></p>
<p><input type="BUTTON" value="登录" id="login" /></p>
</form>
{% end %}
{% raw xsrf_form_html() %}
是新增的,目的就在于实现上面所说的授权给前端以合法请求。
前端向后端发送的请求是通过ajax(),所以,在ajax请求中,需要一个_xsrf参数。
以下是script.js的代码
function getCookie(name){
var x = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return x ? x[1]:undefined;
}
$(document).ready(function(){
$("#login").click(function(){
var user = $("#username").val();
var pwd = $("#password").val();
var pd = {"username":user, "password":pwd, "_xsrf":getCookie("_xsrf")};
$.ajax({
type:"post",
url:"/",
data:pd,
cache:false,
success:function(data){
window.location.href = "/user?user="+data;
},
error:function(){
alert("error!");
},
});
});
});
函数getCookie()的作用是得到cookie值,然后将这个值放到向后端post的数据中var pd = {"username":user, "password":pwd, "_xsrf":getCookie("_xsrf")};
。运行的结果:
这是tornado提供的XSRF防护方法。是不是这样做就高枕无忧了呢?没这么简单。要做好一个网站,需要考虑的事情还很多。特别推荐阅读WebAppSec/Secure Coding Guidelines
常常听到人说做个网站怎么怎么简单,客户用这种说辞来压低价格,老板用这种说辞来缩短工时成本,从上面的简单叙述中,你觉得网站还是随便几个页面就完事了吗?除非那个网站不是给人看的,是在那里摆着的。