用前面的方法,已經(jīng)能夠很順利地編寫模板了。讀者如果留心一下,會覺得每個模板都有相同的部分內(nèi)容。在 Python 中,有一種被稱之為“繼承”的機(jī)制(請閱讀本教程第貳季第肆章中的類 (4)中有關(guān)“繼承”講述]),它的作用之一就是能夠讓代碼重用。
在 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 >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 為父模板,依次改寫已經(jīng)有的 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 %}
看以上代碼,已經(jīng)沒有以前重復(fù)的部分了。{% extends "base.html" %}
意味著以 base.html 為父模板。在 base.html 中規(guī)定了形式如同{% block header %}{% end %}
這樣的塊語句。在 index.html 和 user.html 中,分別對塊語句中的內(nèi)容進(jìn)行了重寫(或者說填充)。這就相當(dāng)于在 base.html 中做了一個結(jié)構(gòu),在子模板中按照這個結(jié)構(gòu)填內(nèi)容。
基本上的流程已經(jīng)差不多了,如果要美化前端,還需要使用 css,它的使用方法跟 js 類似,也是在靜態(tài)目錄中建立文件即可。然后把下面這句加入到 base.html 的 <head></head>
中:
<link rel="stylesheet" type="text/css" href="{{static_url("css/style.css")}}">
當(dāng)然,要在 style.css 中寫一個樣式,比如:
body {
color:red;
}
然后看看前端顯示什么樣子了,我這里是這樣的:
http://wiki.jikexueyuan.com/project/start-learning-python/images/30701.png" alt="" />
關(guān)注字體顏色。
至于其它關(guān)于 CSS 方面的內(nèi)容,本教程就不重點(diǎn)講解了。讀者可以參考關(guān)于 CSS 的資料。
至此,一個簡單的基于 tornado 的網(wǎng)站就做好了,雖然它很丑,但是它很有前途。因?yàn)樽x者只要按照上述的討論,可以在里面增加各種自己認(rèn)為可以增加的內(nèi)容。
建議讀者在上述學(xué)習(xí)基礎(chǔ)上,可以繼續(xù)完成下面的幾個功能:
在后續(xù)教程內(nèi)容中,也會涉及到上述功能。
cookie 是現(xiàn)在網(wǎng)站重要的內(nèi)容,特別是當(dāng)有用戶登錄的時候。所以,要了解 cookie。維基百科如是說:
Cookie(復(fù)數(shù)形態(tài) Cookies),中文名稱為小型文字檔案或小甜餅,指某些網(wǎng)站為了辨別用戶身份而儲存在用戶本地終端(Client Side)上的數(shù)據(jù)(通常經(jīng)過加密)。定義於 RFC2109。是網(wǎng)景公司的前雇員 Lou Montulli 在 1993 年 3 月的發(fā)明。
關(guān)于 cookie 的作用,維基百科已經(jīng)說的非常詳細(xì)了(讀者還能正常訪問這么偉大的網(wǎng)站嗎?):
因?yàn)?HTTP 協(xié)議是無狀態(tài)的,即服務(wù)器不知道用戶上一次做了什么,這嚴(yán)重阻礙了交互式 Web 應(yīng)用程序的實(shí)現(xiàn)。在典型的網(wǎng)上購物場景中,用戶瀏覽了幾個頁面,買了一盒餅干和兩瓶飲料。最后結(jié)帳時,由于 HTTP 的無狀態(tài)性,不通過額外的手段,服務(wù)器并不知道用戶到底買了什么。 所以 Cookie 就是用來繞開 HTTP 的無狀態(tài)性的“額外手段”之一。服務(wù)器可以設(shè)置或讀取 Cookies 中包含信息,借此維護(hù)用戶跟服務(wù)器會話中的狀態(tài)。
在剛才的購物場景中,當(dāng)用戶選購了第一項商品,服務(wù)器在向用戶發(fā)送網(wǎng)頁的同時,還發(fā)送了一段 Cookie,記錄著那項商品的信息。當(dāng)用戶訪問另一個頁面,瀏覽器會把 Cookie 發(fā)送給服務(wù)器,于是服務(wù)器知道他之前選購了什么。用戶繼續(xù)選購飲料,服務(wù)器就在原來那段 Cookie 里追加新的商品信息。結(jié)帳時,服務(wù)器讀取發(fā)送來的 Cookie 就行了。
Cookie 另一個典型的應(yīng)用是當(dāng)?shù)卿浺粋€網(wǎng)站時,網(wǎng)站往往會請求用戶輸入用戶名和密碼,并且用戶可以勾選“下次自動登錄”。如果勾選了,那么下次訪問同一網(wǎng)站時,用戶會發(fā)現(xiàn)沒輸入用戶名和密碼就已經(jīng)登錄了。這正是因?yàn)榍耙淮蔚卿洉r,服務(wù)器發(fā)送了包含登錄憑據(jù)(用戶名加密碼的某種加密形式)的 Cookie 到用戶的硬盤上。第二次登錄時,(如果該 Cookie 尚未到期)瀏覽器會發(fā)送該 Cookie,服務(wù)器驗(yàn)證憑據(jù),于是不必輸入用戶名和密碼就讓用戶登錄了。
和任何別的事物一樣,cookie 也有缺陷,比如來自偉大的維基百科也列出了三條:
對于用戶來講,可以通過改變?yōu)g覽器設(shè)置,來禁用 cookie,也可以刪除歷史的 cookie。但就目前而言,禁用 cookie 的可能不多了,因?yàn)樗傄诰W(wǎng)上買點(diǎn)東西吧。
Cookie 最讓人擔(dān)心的還是由于它存儲了用戶的個人信息,并且最終這些信息要發(fā)給服務(wù)器,那么它就會成為某些人的目標(biāo)或者工具,比如有 cookie 盜賊,就是搜集用戶 cookie,然后利用這些信息進(jìn)入用戶賬號,達(dá)到個人的某種不可告人之目的;還有被稱之為 cookie 投毒的說法,是利用客戶端的 cookie 傳給服務(wù)器的機(jī)會,修改傳回去的值。這些行為常常是通過一種被稱為“跨站指令腳本(Cross site scripting)”(或者跨站指令碼)的行為方式實(shí)現(xiàn)的。偉大的維基百科這樣解釋了跨站腳本:
跨網(wǎng)站腳本(Cross-site scripting,通常簡稱為 XSS 或跨站腳本或跨站腳本攻擊)是一種網(wǎng)站應(yīng)用程序的安全漏洞攻擊,是代碼注入的一種。它允許惡意用戶將代碼注入到網(wǎng)頁上,其他用戶在觀看網(wǎng)頁時就會受到影響。這類攻擊通常包含了 HTML 以及用戶端腳本語言。
XSS 攻擊通常指的是通過利用網(wǎng)頁開發(fā)時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網(wǎng)頁,使用戶加載并執(zhí)行攻擊者惡意制造的網(wǎng)頁程序。這些惡意網(wǎng)頁程序通常是 JavaScript,但實(shí)際上也可以包括 Java, VBScript, ActiveX, Flash 或者甚至是普通的 HTML。攻擊成功后,攻擊者可能得到更高的權(quán)限(如執(zhí)行一些操作)、私密網(wǎng)頁內(nèi)容、會話和 cookie 等各種內(nèi)容。
cookie 是好的,被普遍使用。在 tornado 中,也提供對 cookie 的讀寫函數(shù)。
set_cookie()
和 get_cookie()
是默認(rèn)提供的兩個方法,但是它是明文不加密傳輸?shù)摹?/p>
在 index.py 文件的 IndexHandler 類的 post() 方法中,當(dāng)用戶登錄,驗(yàn)證用戶名和密碼后,將用戶名和密碼存入 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) #設(shè)置 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)
,在回到登錄頁面,等候之后就成為:
http://wiki.jikexueyuan.com/project/start-learning-python/images/30702.png" alt="" />
看圖中箭頭所指,從左開始的第一個是用戶名,第二個是存儲的該用戶密碼。將我在登錄是的密碼就以明文的方式存儲在 cookie 里面了。
明文存儲,顯然不安全。
tornado 提供另外一種安全的方法:set_secure_cookie() 和 get_secure_cookie(),稱其為安全 cookie,是因?yàn)樗悦魑募用芊绞絺鬏敗4送?,?set_cookie() 的區(qū)別還在于, set_secure_cookie() 執(zhí)行后的 cookie 保存在磁盤中,直到它過期為止。也是因?yàn)檫@個原因,即使關(guān)閉瀏覽器,在失效時間之間,cookie 都一直存在。
要是用 set_secure_cookie() 方法設(shè)置 cookie,要先在 application.py 文件的 setting 中進(jìn)行如下配置:
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="
是為此增加的,但是,它并不是這正的加密,僅僅是一個障眼法罷了。
因?yàn)?tornado 會將 cookie 值編碼為 Base-64 字符串,并增加一個時間戳和一個 cookie 內(nèi)容的 HMAC 簽名。所以,cookie_secret 的值,常常用下面的方式生成(這是一個隨機(jī)的字符串):
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes)
'w8yZud+kRHiP9uABEXaQiA=='
如果嫌棄上面的簽名短,可以用 base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
獲取。這里得到的是一個隨機(jī)字符串,用它作為 cookie_secret 值。
然后修改 index.py 中設(shè)置 cookie 那句話,變成:
self.set_secure_cookie(username,db_pwd)
從新跑一個,看看效果。
http://wiki.jikexueyuan.com/project/start-learning-python/images/30703.png" alt="" />
啊哈,果然“密”了很多。
如果要獲取此 cookie,用 self.get_secure_cookie(username)
即可。
這是不是就安全了。如果這樣就安全了,你太低估黑客們的技術(shù)實(shí)力了,甚至于用戶自己也會修改 cookie 值。所以,還不安全。所以,又有了 httponly 和 secure 屬性,用來防范 cookie 投毒。設(shè)置方法是:
self.set_secure_cookie(username, db_pwd, httponly=True, secure=True)
要獲取 cookie,可以使用 self.set_secure_cookie(username)
方法,將這句放在 user.py 中某個適合的位置,并且可以用 print 語句打印出結(jié)果,就能看到變量 username 對應(yīng)的 cookie 了。這時候已經(jīng)不是那個“密”過的,是明文顯示。
用這樣的方法,瀏覽器通過 SSL 連接傳遞 cookie,能夠在一定程度上防范跨站腳本攻擊。
XSRF 的含義是 Cross-site request forgery,即跨站請求偽造,也稱之為"one click attack",通??s寫成 CSRF 或者 XSRF,可以讀作"sea surf"。這種對網(wǎng)站的攻擊方式跟上面的跨站腳本(XSS)似乎相像,但攻擊方式不一樣。XSS 利用站點(diǎn)內(nèi)的信任用戶,而 XSRF 則通過偽裝來自受信任用戶的請求來利用受信任的網(wǎng)站。與 XSS 攻擊相比,XSRF 攻擊往往不大流行(因此對其 進(jìn)行防范的資源也相當(dāng)稀少)和難以防范,所以被認(rèn)為比 XSS 更具危險性。
讀者要詳細(xì)了解 XSRF,推薦閱讀:CSRF | XSRF 跨站請求偽造
對于防范 XSRF 的方法,上面推薦閱讀的文章中有明確的描述。還有一點(diǎn)需要提醒讀者,就是在開發(fā)應(yīng)用時需要深謀遠(yuǎn)慮。任何會產(chǎn)生副作用的 HTTP 請求,比如點(diǎn)擊購買按鈕、編輯賬戶設(shè)置、改變密碼或刪除文檔,都應(yīng)該使用 post() 方法。這是良好的 RESTful 做法。
又一個新名詞:REST。這是一種 web 服務(wù)實(shí)現(xiàn)方案。偉大的維基百科中這樣描述:
表徵性狀態(tài)傳輸(英文:Representational State Transfer,簡稱 REST)是 Roy Fielding 博士在 2000 年他的博士論文中提出來的一種軟件架構(gòu)風(fēng)格。目前在三種主流的 Web 服務(wù)實(shí)現(xiàn)方案中,因?yàn)?REST 模式與復(fù)雜的 SOAP 和 XML-RPC 相比更加簡潔,越來越多的 web 服務(wù)開始采用 REST 風(fēng)格設(shè)計和實(shí)現(xiàn)。例如,Amazon.com 提供接近 REST 風(fēng)格的 Web 服務(wù)進(jìn)行圖書查找;雅虎提供的 Web 服務(wù)也是 REST 風(fēng)格的。
更詳細(xì)的內(nèi)容,讀者可網(wǎng)上搜索來了解。
此外,在 tornado 中,還提供了 XSRF 保護(hù)的方法。
在 application.py 文件中,使用 xsrf_cookies 參數(shù)開啟 XSRF 保護(hù)。
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,
)
這樣設(shè)置之后,Tornado 將拒絕請求參數(shù)中不包含正確的_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() %}
是新增的,目的就在于實(shí)現(xiàn)上面所說的授權(quán)給前端以合法請求。
前端向后端發(fā)送的請求是通過 ajax(),所以,在 ajax 請求中,需要一個 _xsrf 參數(shù)。
以下是 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!");
},
});
});
});
函數(shù) getCookie() 的作用是得到 cookie 值,然后將這個值放到向后端 post 的數(shù)據(jù)中 var pd = {"username":user, "password":pwd, "_xsrf":getCookie("_xsrf")};
。運(yùn)行的結(jié)果:
http://wiki.jikexueyuan.com/project/start-learning-python/images/30704.png" alt="" />
這是 tornado 提供的 XSRF 防護(hù)方法。是不是這樣做就高枕無憂了呢? 沒這么簡單。要做好一個網(wǎng)站,需要考慮的事情還很多 。特別推薦閱讀WebAppSec/Secure Coding Guidelines
常常聽到人說做個網(wǎng)站怎么怎么簡單,客戶用這種說辭來壓低價格,老板用這種說辭來縮短工時成本,從上面的簡單敘述中,你覺得網(wǎng)站還是隨便幾個頁面就完事了嗎?除非那個網(wǎng)站不是給人看的,是在那里擺著的。
總目錄 | 上節(jié):用 tornado 做網(wǎng)站 (4) | 下節(jié):用 tornado 做網(wǎng)站 (6)
如果你認(rèn)為有必要打賞我,請通過支付寶:qiwsir@126.com,不勝感激。