看原文
最近在做一个网站的后端开发。因为初期只有我一个人做,所以技术选择上很自由。在 web 服务器上我选择了Tornado。虽然曾经也读过它的源码,并做过一些小的 demo,但毕竟这是第一次在工作中使用,难免又发现了一些值得分享的东西。
http://bank.example.com/withdraw?amount=1000000&for=Eve当这个银行网站的用户访问该 URL 时,就会给 Eve 这名用户一百万元。用户当然不会轻易地点击这个 URL,但是攻击者可以在其他网站上嵌入一张伪造的图片,将图片地址设为该 URL:
<img src="http://bank.example.com/withdraw?amount=1000000&for=Eve">
那么当用户访问那个恶意网站时,浏览器就会对该 URL 发起一个 GET 请求,于是在用户毫不知情的情况下,一百万就被转走了。<form action="http://bank.example.com/withdraw" method="post">
<p>转发抽奖送 iPad 啊!</p>
<input type="hidden" name="amount" value="1000000">
<input type="hidden" name="for" value="Eve">
<input type="submit" value="转发">
</form>
不明真相的用户点了下“转发”按钮,结果钱就被转走了…$.ajaxSetup({
beforeSend: function(jqXHR, settings) {
type = settings.type
if (type != 'GET' && type != 'HEAD' && type != 'OPTIONS') {
var pattern = /(.+; *)?_xsrf *= *([^;" ]+)/;
var xsrf = pattern.exec(document.cookie);
if (xsrf) {
jqXHR.setRequestHeader('X-Xsrftoken', xsrf[2]);
}
}
}});
base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
这个参数可以随机生成,但如果同时有多个 Tornado 进程来服务的话,或者有时会重启的话,还是共用一个常量比较好,并且注意不要泄露。def _time_independent_equals(a, b):
if len(a) != len(b):
return False
result = 0
if type(a[0]) is int: # python3 byte strings
for x, y in zip(a, b):
result |= x ^ y
else: # python2
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0
读了半天也没发现和普通的字符串比较有什么优点,直到看了 StackOverflow 上的答案才知道:为了避免攻击者通过测试比较时间来判断正确的位数,这个函数让比较的时间比较恒定,也就杜绝了这种情况。(话说这答案看得我各种佩服啊,搞安全的专家果然不是我那么肤浅的…)class RequestHandler(tornado.web.RequestHandler):
def write_error(self, status_code, **kwargs):
if status_code == 404:
self.render('404.html')
elif status_code == 500:
self.render('500.html')
else:
super(RequestHandler, self).write_error(status_code, **kwargs)
由于历史原因,你也可以覆盖 get_error_html() 方法,不过不被推荐。
class PageNotFoundHandler(RequestHandler):
def get(self):
raise tornado.web.HTTPError(404)
tornado.web.ErrorHandler = PageNotFoundHandler
另一种方法就是在 Application 的 handlers 参数的最后,加上一个能捕捉任何 URL 的 handler:
application = tornado.web.Application([
# ...
('.*', PageNotFoundHandler)
])
def get_current_user(self):
return self.get_secure_cookie('user_id', 0)
它的返回值为假时,就会跳转到登录页面了。application = tornado.web.Application(
[
# ...
],
login_url = '/login'
)
class AdminHandler(RequestHandler):
def get_login_url(self):
return '/admin/login'
class LoginHandler(RequestHandler):
def get(self):
if self.get_current_user():
self.redirect('/')
return
self.render('login.html')
def post(self):
if self.get_current_user():
raise tornado.web.HTTPError(403)
# check username and password
if success:
self.redirect(self.get_argument('next', '/'))
此外,我很多地方都使用了 AJAX 技术,而前端懒得去处理 403 错误,所以我只能改造一下 authenticated() 了:
def authenticated(method):
"""Decorate methods with this to require that the user be logged in."""
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
if not self.current_user:
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest': # jQuery 等库会附带这个头
self.set_header('Content-Type', 'application/json; charset=UTF-8')
self.write(json.dumps({'success': False, 'msg': u'您的会话已过期,请重新登录!'}))
return
if self.request.method in ("GET", "HEAD"):
url = self.get_login_url()
if "?" not in url:
if urlparse.urlsplit(url).scheme:
# if login url is absolute, make next absolute too
next_url = self.request.full_url()
else:
next_url = self.request.uri
url += "?" + urllib.urlencode(dict(next=next_url))
self.redirect(url)
return
raise tornado.web.HTTPError(403)
return method(self, *args, **kwargs)
return wrapper
if __name__ == '__main__':
from tornado.httpserver import HTTPServer
from tornado.netutil import bind_sockets
sockets = bind_sockets(80)
server = HTTPServer(application, xheaders=True)
server.add_sockets(sockets)
tornado.ioloop.IOLoop.instance().start()
此外,我只需要处理 IPv4,但本地测试时会拿到 ::1 这种 IPv6 地址,所以还需要设置一下:
if settings.IPV4_ONLY:
import socket
sockets = bind_sockets(80, family=socket.AF_INET)
else:
sockets = bind_sockets(80)
if __name__ == '__main__':
if settings.IPV4_ONLY:
import socket
sockets = bind_sockets(80, family=socket.AF_INET)
else:
sockets = bind_sockets(80)
if not settings.DEBUG_MODE:
import tornado.process
tornado.process.fork_processes(0) # 0 表示按 CPU 数目创建相应数目的子进程
server = HTTPServer(application, xheaders=True)
server.add_sockets(sockets)
tornado.ioloop.IOLoop.instance().start()
注意这种方式下不能启用 autoreload 功能(application 在创建时,debug 参数不能为真)。