首頁 後端開發 Python教學 Python Web服务器Tornado使用小结

Python Web服务器Tornado使用小结

Jun 06, 2016 am 11:30 AM
python tornado 網頁伺服器

首先想说的是它的安全性,这方面确实能让我感受到它的良苦用心。这主要可以分为两点:

一、防范跨站伪造请求(Cross-site request forgery,简称 CSRF 或 XSRF)

CSRF 的意思简单来说就是,攻击者伪造真实用户来发送请求。

举例来说,假设某个银行网站有这样的 URL:
http://bank.example.com/withdraw?amount=1000000&for=Eve
当这个银行网站的用户访问该 URL 时,就会给 Eve 这名用户一百万元。用户当然不会轻易地点击这个 URL,但是攻击者可以在其他网站上嵌入一张伪造的图片,将图片地址设为该 URL:
Python Web服务器Tornado使用小结
那么当用户访问那个恶意网站时,浏览器就会对该 URL 发起一个 GET 请求,于是在用户毫不知情的情况下,一百万就被转走了。

要防范上述攻击很简单,不允许通过 GET 请求来执行更改操作(例如转账)即可。不过其他类型的请求照样也不安全,假如攻击者构造这样一个表单:

代码如下:


   

转发抽奖送 iPad 啊!


   
   
   

不明真相的用户点了下“转发”按钮,结果钱就被转走了…

要杜绝这种情况,就需要在非 GET 请求时添加一个攻击者无法伪造的字段,处理请求时验证这个字段是否修改过。
Tornado 的处理方法很简单,在请求中增加了一个随机生成的 _xsrf 字段,并且 cookie 中也增加这个字段,在接收请求时,比较这 2 个字段的值。
由于非本站的网页是不能获取或修改 cookie 的,这就保证了 _xsrf 无法被第三方网站伪造(HTTP 嗅探例外)。
当然,用户自己是可以随意获取和修改 cookie 的,不过这已经不属于 CSRF 的范畴了:用户自己伪造自己所做的事情,当然由他自己来承担。

要使用该功能的话,需要在生成 tornado.web.Application 对象时,加上 xsrf_cookies=True 参数,这会给用户生成一个名为 _xsrf 的 cookie 字段。
此外还需要你在非 GET 请求的表单里加上 xsrf_form_html(),如果不用 Tornado 的模板的话,在 tornado.web.RequestHandler 内部可以用 self.xsrf_form_html() 来生成。

对于 AJAX 请求来说,基本上是不需要担心跨站的,所以 Tornado 1.1.1 以前的版本并不对带有 X-Requested-With: XMLHTTPRequest 的请求做验证。
后来 Google 的工程师指出,恶意的浏览器插件可以伪造跨域 AJAX 请求,所以也应该进行验证。对此我不置可否,因为浏览器插件的权限可以非常大,伪造 cookie 或是直接提交表单都行。
不过解决办法仍然要说,其实只要从 cookie 中获取 _xsrf 字段,然后在 AJAX 请求时加上这个参数,或者放在 X-Xsrftoken 或 X-Csrftoken 请求头里即可。嫌麻烦的话,可以用 jQuery 的 $.ajaxSetup() 来处理:

代码如下:


$.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]);
            }
        }
}});

此外再顺便谈谈跨站脚本(Cross-site scripting,简称 XSS)。和 CSRF 相反的是,XSS 是利用被攻击网站自身的漏洞,在该网站上注入攻击者想执行的脚本代码,让浏览该网站的用户执行。
不过只要不让用户随意输入 HTML(例如对 进行转义),对 HTML 元素的属性做验证(例如属性里的引号要转义,src 和 事件处理等属性不能随意填写 JavaScript 代码等),并检查 CSS(含 style 属性)中的 expression 即可避免。

二、防止伪造 cookie。

前面提到的 CSRF 和 XSS 都是攻击者在用户不知情的情况下,冒用他的名义来进行操作;而伪造 cookie 则是攻击者自己主动伪造其他用户来进行操作。
举例来说,假设网站的登录验证就是检查 cookie 中的用户名,只要符合的话,就认为该用户已登录。那么攻击者只要在 cookie 中设置 username=admin 之类的值,就可以冒充管理员来操作了。

要防止 cookie 被伪造,首先需要提到设置 cookie 时的两个参数:secure 和 httponly。这两个参数并不在 tornado.web.RequestHandler.set_cookie() 的参数列表里,而是作为关键字参数传递,并在 Cookie.Morsel._reserved 中定义的。
前者是指这个 cookie 只能通过安全连接传递(即 HTTPS),这就使得嗅探者无法截获该 cookie;后者则要求其只能在 HTTP 协议下访问(即无法通过 JavaScript 来获取 document.cookie 中的该字段,并且设置后也不会通过 HTTP 协议向服务器发送),这便使得攻击者无法简单地通过 JavaScript 脚本来伪造 cookie。

不过对于恶意的攻击者,这两个参数并不能杜绝 cookie 被伪造。为此就需要对 cookie 做个签名,一旦被修改,服务器端可以判断出来。
Tornado 中提供了 set_secure_cookie() 这个方法来对 cookie 做签名。签名时需要提供一串秘钥(生成 tornado.web.Application 对象时的 cookie_secret 参数),这个秘钥可以通过如下代码来生成:
base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
这个参数可以随机生成,但如果同时有多个 Tornado 进程来服务的话,或者有时会重启的话,还是共用一个常量比较好,并且注意不要泄露。

这个签名用的是 HMAC 算法,hash 算法采用的是 SHA1。简单来说就是把 cookie 名、值和时间戳的 hash 作为签名,再把“值|时间戳|签名”作为新的值。这样服务器端只要拿秘钥再次加密,比较签名是否有变化过即可判断真伪。
值得一提的是读源码时还发现这样一个函数:
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 上的答案才知道:为了避免攻击者通过测试比较时间来判断正确的位数,这个函数让比较的时间比较恒定,也就杜绝了这种情况。(话说这答案看得我各种佩服啊,搞安全的专家果然不是我那么肤浅的…)

三、接着是继承 tornado.web.RequestHandler。

在执行流程上,tornado.web.Application 会根据 URL 寻找一个匹配的 RequestHandler 类,并初始化它。它的 __init__() 方法会调用 initialize() 方法,所以只要覆盖后者即可,并且不需要调用父类的 initialize()。
接着根据不同的 HTTP 方法寻找该 handler 的 get/post() 等方法,并在执行前运行 prepare()。这些方法都不会主动调用父类的,因此有需要时,自行调用吧。
最后会调用 handler 的 finish() 方法,这个方法最好别覆盖。它会调用 on_finish() 方法,它可以被覆盖,用于处理一些善后的事情(例如关闭数据库连接),但不能再向浏览器发送数据了(因为 HTTP 响应已发送,连接也可能已被关闭)。

顺便说下怎么处理错误页面。
简单来说,执行 RequestHandler 的 _execute() 方法(内部依次执行 prepare()、get() 和 finish() 等方法)时,任何未捕捉的错误都会被它的 write_error() 方法捕捉,因此覆盖这个方法即可:

代码如下:

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() 方法,不过不被推荐。
此外,你还可能没到 _execute() 方法就出错了。
例如 initialize() 方法抛出了一个未捕捉的异常,这个异常会被 IOStream 捕捉到,然后直接关闭连接,不能向用户输出任何错误页面。
再比如没有找到一个能处理该请求的 handler,就会用 tornado.web.ErrorHandler 去处理 404 错误。这种情况可以替换这个类来实现自定义错误页面:

代码如下:

class PageNotFoundHandler(RequestHandler):
    def get(self):
        raise tornado.web.HTTPError(404)

tornado.web.ErrorHandler = PageNotFoundHandler


另一种方法就是在 Application 的 handlers 参数的最后,加上一个能捕捉任何 URL 的 handler:

代码如下:

application = tornado.web.Application([
    # ...
    ('.*', PageNotFoundHandler)
])


四、接着说说处理登录。

Tornado 提供了 @tornado.web.authenticated 这个装饰器,在 handler 的 get() 等方法前加上即可。
它会依赖三处代码:
需要定义 handler 的 get_current_user() 方法,例如:

代码如下:

def get_current_user(self):
    return self.get_secure_cookie('user_id', 0)


它的返回值为假时,就会跳转到登录页面了。
创建 application 时设置 login_url 参数:

代码如下:

application = tornado.web.Application(
    [
        # ...
    ],
    login_url = '/login'
)


定义 handler 的 get_login_url() 方法。
如果不能使用默认的 login_url 参数(例如普通用户和管理员需要不同的登录地址),那么可以覆盖 get_login_url() 方法:

代码如下:

class AdminHandler(RequestHandler):
    def get_login_url(self):
        return '/admin/login'


顺带一提,跳转到登录页后时会附带一个 next 参数,指向登录前访问的网址。为达到更好的用户体验,需要在登录后跳转到该网址:

代码如下:

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

五、然后说下获取用户的 IP 地址。

简单来说,在 handler 的方法里用 self.request.remote_ip 就能拿到了。
不过如果使用了反向代理,拿到的就是代理的 IP 了,这时候就需要在创建 HTTPServer 时增加 xheaders 的设置了:

代码如下:

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)

六、最后再提下生产环境下如何提高性能。

Tornado 可以在 HTTPServer 调用 add_sockets() 前创建多个子进程,利用多 CPU 的优势来处理并发请求。

简单来说,代码如下:

代码如下:

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 参数不能为真)。
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

Python與C:學習曲線和易用性 Python與C:學習曲線和易用性 Apr 19, 2025 am 12:20 AM

Python更易學且易用,C 則更強大但複雜。 1.Python語法簡潔,適合初學者,動態類型和自動內存管理使其易用,但可能導致運行時錯誤。 2.C 提供低級控制和高級特性,適合高性能應用,但學習門檻高,需手動管理內存和類型安全。

See all articles