编写加密的应用
很多时候,加密应用是一件非常繁琐的事情(也很让开发人员头疼).tornado 的web服务端设计之初已经考虑到了这些事情,内置了很多加密模块,让我们可以轻松地对容易出现问题的地方进行处理.一个可靠的 cookies 可以避免用户的状态被浏览器的恶意代码进行修改.此外浏览器的 cookies 还可以配合 http 请求的变量来避免一些伪造的跨站恶意攻击.在这一章节,我们将会了解到 tornado 的这些功能是如何防止攻击的,同时我们还会看到一个用户认证的例子和相关的功能.
危险的cookie
许多网页会使用浏览器的cookies来存储用户的身份标识与sessions信息.通过浏览器的sessions状态来确定用户的状态是非常简单且常用的手段,具有非常广泛的应用.不幸的是,浏览器的cookies非常容易被跨站攻击.这里会有一小段内容展示tornado是如何防止恶意脚本篡改你应用存储的cookies的.
伪造cookies
有许多途径可以截取到浏览器中的cookies信息,网页中的javascript和flash都有读写这个域中的cookies信息的权限.浏览器插件同样也可以通过程序去获取这些cookies信息.这些通过网页完成的脚本攻击,可以直接篡改用户浏览器中的cookies数据.
加密cookies
tornado使用加密的签名验证cookies的值,以保证这些cookies的数值不能够被服务器以外的第三方修改.恶意的攻击脚本并不知道加密的key,所以它们将无法修改应用中的任何cookies数据.
使用加密的cookies
tornado的 set_secure_cookies() 和 get_secure_cookes() 功能用于发送和接受浏览器cookies数据,防止这些数据在浏览器中被篡改.要想使用这些功能,你必须在应用的构造函数中声明 cookies_secret 变量.让我们来看一个简单的例子.
例子6-1中的应用将会返回一个浏览器中累计重载网页的次数.如果cookies没有设置过(或cookies被修改过),这个应用将会设置一个新的cookies值 1. 否则应用将会获取cookies的值并进行自增操作(对原数值加一). 例子6-1 cookie_counter.py
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
|
import
tornado
.
httpserver
import
tornado
.
ioloop
import
tornado
.
web
import
tornado
.
options
from
tornado
.
options
import
define
,
options
define
(
"port"
,
default
=
8000
,
help
=
"run on the given port"
,
type
=
int
)
class
MainHandler
(
tornado
.
web
.
RequestHandler
)
:
def
get
(
self
)
:
cookie
=
self
.
get_secure_cookie
(
"count"
)
count
=
int
(
cookie
)
+
1
if
cookie
else
1
countString
=
"1 time"
if
count
==
1
else
"%d times"
%
count
self
.
set_secure_cookie
(
"count"
,
str
(
count
)
)
self
.
write
(
''
'<h1>
You’ve viewed this page %s times.
</h1>'
%
countString
''
)
if
__name__
==
"__main__"
:
tornado
.
options
.
parse_command_line
(
)
settings
=
{
"cookie_secret"
:
"bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
}
application
=
tornado
.
web
.
Application
(
[
(
r
'/'
,
MainHandler
)
]
,
*
*
settings
)
http_server
=
tornado
.
httpserver
.
HTTPServer
(
application
)
http_server
.
listen
(
options
.
port
)
tornado
.
ioloop
.
IOLoop
.
instance
(
)
.
start
(
)
|
如果你去检查浏览器中的cookies,你将会注意到经过计算之后存储的数值是MQ==|1310335926|8ef174ecc489ea963c5cdc26ab6d41b49502f2e2. tornado 使用 base-64 编码存储cookies目录中的数据,包括每一次追加 timestamp 和 HMAC 签名.如果你cookies中的 timestamp 太旧(或者超前了).或者签名没有符合期望值.get_secure_cookie() 函数会假设这个cookies已经被修改并返回一个 None 的空值.当做cookie 没有设置过.
这个 cookie_secret 值将会做为一个唯一的随机字符串传送给应用的构造函数.在 python shell 中执行下面的代码,将会生成一个字符串给你.
1
2
3
|
>>>
import
base64
,
uuid
>>>
base64
.
b64encode
(
uuid
.
uuid4
(
)
.
bytes
+
uuid
.
uuid4
(
)
.
bytes
)
'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='
|
tornado 可以保证cookies不被窥探.然而,攻击者仍然可以通过浏览器脚本或插件去截取 cookies 信息.或者直接窃听未加密的网络数据,记住 cookie 变量必须使用加密的签字.即使如此,恶意的程序仍然可以获取到存储的 cookie 数据,并且发送这些数据到其它服务器上或者修改的 cookie 伪造请求发送到应用服务器上.因此,必须避免存储敏感的用户数据到浏览器的 cookie 中. 我们还应该注意到,用户可以修改自己的 cookies.这可能会带来一次提权攻击.例如,我们存储一些用户已经购买且可以查看的文章数量到 cookie 中.我们需要防止用户试图通过修改这个数值获取免费内容的行为. httponly 和 secure 的 cookie 属性可以帮助我们防止这种类型的攻击.
cookies 的 httponly 和 SSL
tornado 的 cookie 功能是基于 python 的 cookie 模块构建的.所以我们可以使用一些加密功能提供的高级特性, 可以只加密 http cookie 中的部分信息.并且它通过运行的脚本去告诉浏览器可以暴露哪些 cookie 数据给,通过什么方式连接服务器.例如,我们可以只通过 SSL 连接去传送 cookie的数据,减小网络请求被截取的概率.我们还可以通过 javascript 要求浏览器隐藏 cookie 的数据. 设置 secure 属性告诉浏览器只能通过 SSL 连接去传送加密的 cookie.(这可能有些令人困惑, tornado 加密的 cookies 与其它的不太一样,准确的说,这是一个 signed cookies).从 python2.6 的版本开始, cookie 对象还支持 httponly 属性.包括告诉浏览器如何让 cookie 更不容易被 javascript 调用.这可以防止跨站攻击脚本去获取 cookie 的值. 要使用这些功能,你必须传送一个 keyword 参数给 set_cookie 和 set_secure_cookie 方法.例如,一个加密, http-only 的 cookie (没有使用 tornado 的签署功能)可以通过 self.set_cookie(‘foo’, ‘bar’, httponly=Ture, secure=True) 进行发送. 现在我们来探讨如何保护 cookie 中存储的持久化数据的策略.我们可以看到通过另一个公共的攻击变量,”脆弱的请求”在96页将会看到如何防止恶意网页伪造的请求攻击你的应用.
脆弱的请求
对于任何 web 应用一个主要的安全漏洞是跨站请求的攻击.通常缩写为 CSRF 或 XSRF, 它的发音是 “sea surf”. 这个是通过浏览器的一个安全漏洞.将恶意代码注入到受害者的网站,伪造非法的请求危害登录网站的用户利益.让我们来看一个例子.
剖析一个伪造的跨站请求
我们假设有一个正常的 burt 书店的用户 alice.当她使用她的帐号登录到在线商店后.网站将会标识她的浏览器 cookie.现在假设她是一个不小心的用户, melvin 想要增加他店中书籍的销售量,在 alice 经常访问的一个网络社区, melvin 已经发表了一篇带有 HTML 图片标记的文章,这个 HTML 图片标记的代码中包含一个在线书店中的购买链接,例如: <img src="http://store.burts-books.com/purchase?title=Melvins+Web+Sploitz" /> alice 的浏览器在试图获取这个图片资源的时候,将会把合法的 cookie 添加到请求中.并不知道这个kitten的图片已经被替换掉了, 这条 URL 将会对在线商店发起一次购买请求.
防止伪造的请求
有许多预防这类攻击的方法.首先在你开发的时候,必须要考虑的是只能接受来自同一个域的请求.这会对所有 HTTP 请求带来副作用,类似点击按钮去提交购买订单,编辑账户设置,修改密码,或者删除的操作,都应该使用 HTTP 的 POST 方法.这是一个很好的 RESTful 实践,同时它还有有个额外的优点,避免一些类似于刚才看到的恶意图片带来的 XSRF 攻击,当然这是远远不够的,一些恶意的网站仍然可以通过比如 HTML 表单或 XMLHTTPRequest 的 API 伪造 POST 请求去攻击你的应用.我们需要通过其它策略去保护 POST 请求. 一个防止伪造 POST 请求的策略是,我们在每一个请求中包含一个变量 token,这个变量将会与发起请求的cookie相匹配,我们的应用将会为每一个页面提供不同的 token , 服务通过 cookie 头和网页中隐藏的 HTML 元素给用户的浏览器提供 token. 如果两个都匹配,我们的应用就会认为这个请求是合法的.
使用tornado的 XSRF 防护
你还可以在应用的构造函数中包含 xsrf_cookies 变量去启用 XSRF 保护.
1
2
3
4
5
6
7
8
|
settings
=
{
"cookie_secret"
:
"bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
,
"xsrf_cookies"
:
True
}
application
=
tornado
.
web
.
Application
(
[
(
r
'/'
,
MainHandler
)
,
(
r
'/purchase'
,
PurchaseHandler
)
,
]
,
*
*
settings
)
|
设置了这个标记,tornado将会拒绝并删除没有包含正确 _xsrf 值的变量的 POST PUT请求.tornado 还会处理这个情况下的 _xsrf cookies.当然你的网页表单必须包含 XSRF token 的合法才会被当成授权请求处理.要实现它,只需要在你的 template 中简单地调用一个函数 xsrf_form_html就可以了:
1
|
|
XSRF tokens 和 AJAX 请求
AJAX 请求同样也要声明 _xsrf 变量,而且要在返回的页面中显式声明 _xsrf 的值.这个脚本可以查询客户端浏览器中的 cookie 值. 通过两个功能可以清楚得看到, AJAX POST 的请求中添加了 token 的值.第一个函数通过用户名获取 cookie ,这样第二个函数就可以很方便地添加 _xsrf 变量到存储数据的对象中,并传送给 postJSON 函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function
getCookie
(
name
)
{
var
c
=
document
.
cookie
.
match
(
"\\b"
+
name
+
"=([^;]*)\\b"
)
;
return
c
?
c
[
1
]
:
undefined
;
}
jQuery
.
postJSON
=
function
(
url
,
data
,
callback
)
{
data
.
_xsrf
=
getCookie
(
"_xsrf"
)
;
jQuery
.
ajax
(
{
url
:
url
,
data
:
jQuery
.
param
(
data
)
,
dataType
:
"json"
,
type
:
"POST"
,
success
:
callback
}
)
;
}
|
这里有许多必须要预先考虑的事情, tornado 的 secure cookie 支持 XSRF 防护可以减少应用开发者的负担.它内建的加密功能非常有用.尽管如此,你仍然要特别留心应用的安全性,互联网上有非常多在线的 web 应用加密资料可以参考,其中实际应用最广泛的是 mozilla 的 secure coding guidelines
用户验证
现在我们来了解一下 XSRF 攻击的原理,然后学习如何设置和恢复安全的 cookies,我们以一个简单的用户验证系统做为示例,在这一段中,我们将会构建一个应用,询问访问者的用户名并将其存储到加密的 cookie 中以及如何恢复 cookie 数据.接下来的请求将会记录回访者的信息并显式一个特定的用户页面给回访者.你需要学习一些 login_url 的变量和 tornado.web.authenticated 修饰符的知识,这样可以去掉应用中一些常用的 headaches .
例子:欢迎回来
在这个例子中,我们将通过加密的 cookie 中存储的数据直接识别用户信息.当某人通过特定的浏览器第一次访问我们的网站时(或者她的 cookie 已经过期),应用将会展现一个带有登录表单的页面,这个表单将会以 POST 请求的方式向 LoginHandler 地址提交信息.表单中的 POST 方法会调用 set_secure_cookie() 去存储请求中提交的用户名等参数. 这个有用户验证功能的 tornado 应用示例代码可以查看例6-2.我们将会在这一段落深入讨论其中的细节. LoginHandler 类会返回登录表单并设置 cookie 直到 LogoutHandler 类将其删除掉. 例子6-2:cookies.py
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
42
43
|
import
tornado
.
httpserver
import
tornado
.
ioloop
import
tornado
.
web
import
tornado
.
options
import
os.path
from
tornado
.
options
import
define
,
options
define
(
"port"
,
default
=
8000
,
help
=
"run on the given port"
,
type
=
int
)
class
BaseHandler
(
tornado
.
web
.
RequestHandler
)
:
def
get_current_user
(
self
)
:
return
self
.
get_secure_cookie
(
"username"
)
class
LoginHandler
(
BaseHandler
)
:
def
get
(
self
)
:
self
.
render
(
'login.html'
)
def
post
(
self
)
:
self
.
set_secure_cookie
(
"username"
,
self
.
get_argument
(
"username"
)
)
self
.
redirect
(
"/"
)
class
WelcomeHandler
(
BaseHandler
)
:
@
tornado
.
web
.
authenticated
def
get
(
self
)
:
self
.
render
(
'index.html'
,
user
=
self
.
current_user
)
class
LogoutHandler
(
BaseHandler
)
:
def
get
(
self
)
:
if
(
self
.
get_argument
(
"logout"
,
None
)
)
:
self
.
clear_cookie
(
"username"
)
self
.
redirect
(
"/"
)
if
__name__
==
"__main__"
:
tornado
.
options
.
parse_command_line
(
)
settings
=
{
"template_path"
:
os.path
.
join
(
os.path
.
dirname
(
__file__
)
,
"templates"
)
,
"cookie_secret"
:
"bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
,
"xsrf_cookies"
:
True
,
"login_url"
:
"/login"
}
application
=
tornado
.
web
.
Application
(
[
(
r
'/'
,
WelcomeHandler
)
,
(
r
'/login'
,
LoginHandler
)
,
(
r
'/logout'
,
LogoutHandler
)
]
,
*
*
settings
)
http_server
=
tornado
.
httpserver
.
HTTPServer
(
application
)
http_server
.
listen
(
options
.
port
)
tornado
.
ioloop
.
IOLoop
.
instance
(
)
.
start
(
)
|
而例子6-3和例子6-4对应的文件应该放到应用所属目录的 templates 目录下. 例子6-3:login.html
1
|
|
例子6-4:index.html
1
2
3
4
|
<
h1
>
Welcome
back
,
{
{
user
}
}
<
/
h1
>
|
authenticated 修饰符
要想使用 tornado 的验证功能,我们必须要标记一个专用的 handlers 处理我们的用户登录请求.使用 @tornado.web.authenticated 修饰符可以让我们更方便地实现这个需求.只要在编写处理方法的时候附加上这个修饰符,tornado 将会在校验用户信息正确之后才会调用这个方法.让我们来看看例子中的 welcomehandler.它只有确认用户登录之后才会返回 index.html 的页面.
1
2
3
4
|
class
WelcomeHandler
(
BaseHandler
)
:
@
tornado
.
web
.
authenticated
def
get
(
self
)
:
self
.
render
(
'index.html'
,
user
=
self
.
current_user
)
|
在 get 方法被调用之前, authenticated 修饰符会确保 current_user 属性存在对应的值.(后面会对这个属性做简单的讨论).如果 current_user 的值是 “falsy” (None, False, 0, “”)任何 get 或 head 请求将会被重定向到 login_url 应用设定的 URL. 请注意, 没有通过用户校验的 POST 请求将会返回 http 403 状态的响应信息. 如果用户校验通过, tornado 将会调用期望的 handler 方法.这个 authenticated 修饰符信赖 current_user 属性和 login_url 设置的全部功能,现在我们来看看下一步做什么.
current_user 属性
这个请求处理的类有 current_user 属性(它也可以用在其它 template 的上)可以用于存储正常请求中的用户身份验证信息.在默认情况下,这个值是None,如果要让 authenticated 修饰符成功识别用户信息,你必须重载正常用户请求处理的 get_current_user() 方法中的默认值. 在这个例子中,启用这个接口之后,我们可以从访问者的 cookie 中快速地恢复他的用户信息.很显然,大家都希望使用健壮性更强的技术,为了达到这个目的,我们必须使用下面几个方法:
1
2
3
|
class
BaseHandler
(
tornado
.
web
.
RequestHandler
)
:
def
get_current_user
(
self
)
:
return
self
.
get_secure_cookie
(
"username"
)
|
在这里讨论的例子没有涉及存储和检索用户密码或其它证书,但是进行极小的改动之后本章所描述的技术可以扩展成查询数据库凭据.
login_url 设置
请留意这个应用中构造方法的内容.记住我们传给应用的新设置: Login_url 在应用中的地址为 login 形式.如果 get_current_user 方法返回了一个 falsy 值, authenticated 修饰符将会把请求处理重定向浏览器到登录页面的地址上.
1
2
3
4
5
6
7
8
9
10
11
|
settings
=
{
"template_path"
:
os.path
.
join
(
os.path
.
dirname
(
__file__
)
,
"templates"
)
,
"cookie_secret"
:
"bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E="
,
"xsrf_cookies"
:
True
,
"login_url"
:
"/login"
}
application
=
tornado
.
web
.
Application
(
[
(
r
'/'
,
WelcomeHandler
)
,
(
r
'/login'
,
LoginHandler
)
,
(
r
'/logout'
,
LogoutHandler
)
]
,
*
*
settings
)
|
当 tornado 构建这条重定向 URL 时,还可以附加上一个 next 的请求字符串变量,这个变量会包含在重定向的到登录页面的 URL 中,你可以使用这样的语句 self.redirect(self.get_argument('next','/'))让用户完成登录之后返回登录前的页面.
总结
目前我们只看到使用两种技术来帮助我们保护 tornado应用.如何使用 @tornado.web.authenticated 修饰符去实现用户的身份验证.在第七章中,我们将会着眼于如何扩展我们已经讨论过的技术,将其应用到外部的 web 服务中.通过外部的web server例如 facebook 或 twitter 完成用户身份验证.
博主渣基础,对于web应用还不够了解,未必能将作者的意图都翻译过来,如果其中有不够完善的地方,请帮忙纠正. 原创翻译:发布于http://blog.xihuan.de/tech/web/tornado/tornadowritingsecure_applications.html
上一篇: 翻译:introduce to tornado-Asynchronous Web Services
下一篇: 翻译: introduce to tornado - Authenticating with External Services