来自 编程应用 2019-09-04 19:47 的文章
当前位置: 六合联盟网 > 编程应用 > 正文

Web服务器Tornado使用小结

率先想说的是它的安全性,那上边的确能让自个儿感触到它的良苦用心。这至关心重视要可以分为两点:

1.tornado是多少个异步的http框架

一、防守跨站伪造央浼(Cross-site request forgery,简称 CSEscortF 或 XS陆风X8F)

2.常用模块

CSCRUISERF 的意味一言以蔽之正是,攻击者伪造真实客户来发送伏乞。

importtornado.httpserver

importtornado.ioloop

importtornado.options

importtornado.web

那多个都是 Tornado 的模块,在本例中都以必须的。它们四个在形似的网站开辟中,都要选取,基本职能分别是:

tornado.httpserver:那么些模块正是用来消除 web 服务器的 http 协议难点,它提供了广大质量方法,完毕顾客端和劳动器端的互通。Tornado 的非阻塞、单线程的风味在那一个模块中反映。

tornado.ioloop:那些也充足主要,能够落到实处非阻塞 socket 循环,无法互通三遍就终止。

tornado.options:那是命令行解析模块,也常用到。

tornado.web:那是必须的模块,它提供了贰个简易的 Web 框架与异步作用,进而使其扩充到大气开垦的连年,使其改为优异的

比释尊讲,借使有些银行网址有那般的 URAV4L:

当以此银行网址的客户访谈该 UEscortL 时,就能够给 Eve这名顾客第一百货公司万元。客户当然不会自由地方击那几个U奥迪Q7L,然而攻击者能够在别的网址上停放一张伪造的图片,将图纸地址设为该 U中华VL:
<img src=";
那就是说当顾客访问非凡恶意网址时,浏览器就能够对该 U昂科雷L 发起二个 GET 央求,于是在客商毫不知情的场合下,一百万就被转走了。

3.优化路线,管理post,get乞请

要幸免上述攻击非常的粗略,差异意通过 GET 乞请来实行改换操作(举个例子转账)就能够。然而其余门类的呼吁照样也不安全,假使攻击者构造这样三个表单:

import textwrap

import os

import tornado.httpserver

import tornado.ioloop

import tornado.options

import tornado.web

from tornado.options import define, options

define("port", default=8000, help="run on the given port", type=int)

class ReverseHandler(tornado.web.RequestHandler):

     def get(self, input):

         self.write(input[::-1])

class WrapHandler(tornado.web.RequestHandler):

    def post(self):

        text = self.get_argument('text')

        width = self.get_argument('width', 40)

        self.write(textwrap.fill(text, int(width)))

if __name__ == "__main__":

    #解析命令行参数

    tornado.options.parse_command_line()

    #创办应用程序

    app = tornado.web.Application(

        handlers=[

            #(r"/reverse/(w+)", ReverseHandler),

            (r"/", ReverseHandler),

            (r"/wrap", WrapHandler)

        ],

        template_path=os.path.join(os.path.dirname(__file__),'templates'),

        debug=True

    )

    #设置http服务器

    http_server = tornado.httpserver.HTTPServer(app)

    #设置监听端口

    http_server.listen(options.port)

    tornado.ioloop.IOLoop.instance().start()

复制代码 代码如下:

4.关联 模板

<form action="" method="post">
    <p>转载抽取奖品送 三星平板 啊!</p>
    <input type="hidden" name="amount" value="1000000">
    <input type="hidden" name="for" value="Eve">
    <input type="submit" value="转发">
</form>

import textwrap

import os

import tornado.web

from tornado.options import define, options

define("port", default=5678, help="run on the given port", type=int)

#自定义方法

def defiend(str):

    return '<<%s>>'% str

class BookHandler(tornado.web.RequestHandler):

    def get(self):

        title = self.get_argument('title',)

        self.render(

            'book.html',

            title=title,

            header="books",

            books=[

                'baiduren',

                'huozhe',

                'xiaowangzi'

            ],

            tag=defiend

        )

if __name__ == "__main__":

    tornado.options.parse_command_line()

    app = tornado.web.Application(

        handlers=[

            (r"/", BookHandler),

        ],

        #安装文件路线

        template_path=os.path.join(os.path.dirname(__file__),'templates'),

        debug=True

    )

    http_server = tornado.httpserver.HTTPServer(app)

    http_server.listen(options.port)

    tornado.ioloop.IOLoop.instance().start()

不明真相的客商点了下“转发”按键,结果钱就被转走了…

5.模板承袭,首要在html页面

要杜绝这种状态,就需求在非 GET 需要时加多多个攻击者不恐怕伪造的字段,管理央浼时表明那些字段是不是修改过。
Tornado 的拍卖办法很简短,在呼吁中追加了贰个私自变化的 _xsrf 字段,何况cookie 中也大增那几个字段,在收到央浼时,相比较那 2 个字段的值。
鉴于非本站的网页是不能够得到或改变 cookie 的,那就保证了 _xsrf 无法被第三方网址假冒(HTTP 嗅探例外)。
自然,客商本身是能够自由获取和修改 cookie 的,可是那曾经不属于 CS奥迪Q5F 的范畴了:客户本人伪造自身所做的事体,当然由他本人来担负。

        {%block header%}

            this is header

        {%end%}

        {%block content%}

            this is content

        {%end%}

        {%block footer%}

            this is footer

        {%end%}

要使用该意义的话,供给在调换 tornado.web.Application 对象时,加上 xsrf_cookies=True 参数,那会给顾客生成贰个名称为 _xsrf 的 cookie 字段。
别的还索要您在非 GET 须要的表单里拉长 xsrf_form_html(),若是不用 Tornado 的模版的话,在 tornado.web.RequestHandler 内部能够用 self.xsrf_form_html() 来生成。

6.tornado设置cookie

对此 AJAX 乞请来讲,基本上是无需顾忌跨站的,所以 Tornado 1.1.1 从前的本子并不对含蓄 X-Requested-With: XMLHTTPRequest 的伸手做注明。
新兴 Google 的程序猿提出,恶意的浏览器插件能够伪造跨域 AJAX 央浼,所以也应该进行验证。对此小编不置可不可以,因为浏览器插件的权能能够充裕大,伪造 cookie 或是直接付出表单都行。
但是化解办法仍旧要说,其实借使从 cookie 中收获 _xsrf 字段,然后在 AJAX 诉求时增进那些参数,可能放在 X-Xsrftoken 或 X-Csrftoken 供给头里就可以。嫌麻烦的话,能够用 jQuery 的 $.ajaxSetup() 来拍卖:

#-*- coding:utf-8 -*-

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):

        print("get current user")

        return self.get_secure_cookie("username")

class LoginHandler(BaseHandler):

    def get(self):

        self.render('cookie_login.html')

    def post(self):

        #安装cookie(标识已经报到)

        self.set_secure_cookie("username", self.get_argument("username"))

        self.redirect("/")

class WelcomeHandler(BaseHandler):

    @tornado.web.authenticated

    def get(self):

        self.render('cookie_index.html', user=self.current_user)

#清除cookie

class LogoutHandler(BaseHandler):

    def get(self):

        if (self.get_argument("username", 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防跨站攻击

        "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()

复制代码 代码如下:

$.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)。和 CS奥迪Q3F 相反的是,XSS 是选用被攻击网站自个儿的尾巴,在该网站上注入攻击者想进行的本子代码,让浏览该网址的客商实施。
可是假如不让客商专断输入 HTML(比如对 < 和 > 实行转义),对 HTML 成分的品质做申明(举个例子属性里的引号要转义,src 和 事件管理等属性不可能自由填入 JavaScript 代码等),并检讨 CSS(含 style 属性)中的 expression 就可以防止。

二、防止伪造 cookie。

前方提到的 CS汉兰达F 和 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 会依据 U中华VL 寻觅多个相配的 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 参数的终极,加上贰个能捕捉任何 U昂CoraL 的 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 参数无法为真)。

一、防卫跨站伪造诉求(Cross-site request forgery,...

本文由六合联盟网发布于编程应用,转载请注明出处:Web服务器Tornado使用小结

关键词: