Tornado - 应用安全

普通 Cookie

  • 设置

    • 原型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      self.set_cookie(
      self,
      name: str,
      value: Union[str, bytes],
      domain: str = None,
      expires: Union[float, Tuple, datetime.datetime] = None,
      path: str = "/",
      expires_days: int = None,
      **kwargs: Any
      )
    • 参数

      • name:cookie 名
      • value:cookie 值
      • domain:提交 cookie 时匹配的域名
      • path:提交 cookie 时匹配的路径
      • expires:cookie 的有效期,可以是时间戳整数、时间元组、datetime 类型;为 UTC 时间。
      • expires_days:cookie 的有效期天数,优先级低于 expires
    • 示例

      1
      2
      3
      4
      5
      6
      class PCookieHandler(RequestHandler):
      def get(self, *args, **kwargs):
      # 设置
      self.set_cookie("sunck", "good")
      # self.set_header("Set-Cookie", "kaige=nice; Path=/")
      self.write("ok")
  • 原理

    设置 cookie 实际上是通过设置 header 的 Set-Cookie 来实现的

    1
    self.set_header("Set-Cookie", "kaige=nice; Path=/")
  • 获取

    • 原型

      1
      self.get_cookie(self, name: str, default: str = None)
    • 参数

      • name:要获取的 cookie 的名称
      • default:如果名为 name 的 cookie 不存在,则返回 default 的值
    • 示例

      1
      2
      3
      4
      5
      6
      class GetPCookieHandler(RequestHandler):
      def get(self, *args, **kwargs):
      # 获取cookie
      cookie = self.get_cookie("sunck", "未登录")
      print("cookie =", cookie)
      self.write("ok")
  • 清除

    • self.clear_cookie():删除名为 name,并同时匹配 domain 和 path 的 cookie

      1
      self.clear_cookie(self, name: str, path: str = "/", domain: str = None)
    • self.clear_all_cookies():删除同时匹配 path 和 domain 的所有 cookie

      1
      self.clear_all_cookies(self, path: str = "/", domain: str = None)
    • ⚠️注意:执行清除 cookie 操作后,并不是立即删除浏览器端的 cookie,而是给 cookie 值设置空,并改变其有限期限为失效。真正删除 cookie 是在关闭浏览器时浏览器自己去清理的。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      class ClearPCookieHandler(RequestHandler):
      def get(self, *args, **kwargs):
      # 清除一个cookie
      # self.clear_cookie("sunck")
      # 清除所有cookie
      self.clear_all_cookies()
      self.write("ok")

安全 Cookie

  • 概述

    • Cookie 是存在客户端浏览器的数据,很容易被篡改
    • Tornado 提供了一种对 Cookie 进行简易加密方式来防止 Cookie 被恶意篡改
  • 设置

    • 需要为应用配置一个用来给 Cookie 进行混淆加密的秘钥

      1
      2
      3
      4
      5
      6
      settings = {
      'debug': True,
      'static_path': os.path.join(BASE_DIRS, 'static'),
      'template_path': os.path.join(BASE_DIRS, 'templates'),
      'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0=',
      }

      'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0='

    • 生成一个秘钥

      生成秘钥

      1
      2
      3
      4
      5
      import base64
      import uuid

      secret = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
      print(secret)
      1
      b'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0='
    • 设置安全 Cookie

      • 原型

        1
        2
        3
        4
        5
        6
        7
        8
        self.set_secure_cookie(
        self,
        name: str,
        value: Union[str, bytes],
        expires_days: int = 30,
        version: int = None,
        **kwargs: Any
        )
      • 作用:设置一个带有签名和时间戳的 cookie,防止 cookie 被伪造

    • 示例

      1
      2
      3
      4
      class SCookieHandler(RequestHandler):
      def get(self, *args, **kwargs):
      self.set_secure_cookie("zhangmanyu", "nice")
      self.write("ok")
    • 查看浏览器端的 cookie 值

      1
      "2|1:0|10:1596855751|10:zhangmanyu|8:bmljZQ==|3d8c5a68a3ca2b7aacd073e317bf5c664ab53e6a67b8dda6f7b9b3a3c307a073"	

      说明

      • |1:0|:1 表示 : 后边有多少位;0 表示正真的数值;| 表示分隔符
      • 安全 cookie 的版本,默认使用版本 2
      • 默认为 0
      • 时间戳
      • cookie 名
      • base64 编码的 cookie 值
      • 签名值,不带长度说明
  • 获取

    • 原型

      1
      2
      3
      4
      5
      6
      7
      self.get_secure_cookie(
      self,
      name: str,
      value: str = None,
      max_age_days: int = 31,
      min_version: int = None,
      )
      • 如果 cookie 存在且验证通过,返回 cookie 值,反则返回 None
      • max_age_days 不同于 expires_days,expires_days 设置浏览器中 cookie 的有效时间;而 max_age_days 是过滤安全 cookie 的时间戳
    • 示例

      1
      2
      3
      4
      5
      class GetSCookieHandler(RequestHandler):
      def get(self, *args, **kwargs):
      scookie = self.get_secure_cookie("zhangmanyu")
      print("scookie =", scookie)
      self.write("ok")
  • ⚠️注意

    • 也不是完全安全的,一定程度上增加了破解 cookie 的难度
    • 以后 cookie 不要存储一些敏感性的数据

XSRF

跨站请求伪造

  • cookie 计数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class CookieNumHandler(RequestHandler):
    def get(self, *args, **kwargs):
    count = self.get_cookie("count", None)
    if not count:
    count = 1
    else:
    count = int(count)
    count += 1
    self.set_cookie("count", str(count))
    self.render('cookienum.html', count=count)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>cookie计数</title>
    </head>
    <body>
    <h1>第{{ count }}访问</h1>
    </body>
    </html>
  • 跨站请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <title>搞事情</title>
    </head>
    <body>
    <img src="http://127.0.0.1:8848/cookienum">
    <h1>去看看吧,我可能把你搞坏了!</h1>
    </body>
    </html>
  • 说明

    • 当访问 “搞事情” 网站时,在我们不知情、为授权的情况下,“cookie 计数器” 网站 cookie 被使用,以至于 “cookie 计数器” 网址认为是它自己调用 Handler 的逻辑
    • 上一个程序使用的是 GET 方式模拟的攻击,为了防止这种攻击,一般对于相对安全的操作一般是不放在 GET 请求中,常常使用的是 POST 请求

XSRF 保护

  • 原理:同源策略,即:

    • 协议相同
    • 域名相同
    • 端口号相同
  • 图示

    同源分析

开启 XSRF 保护

1
2
3
4
5
6
7
settings = {
'debug': True,
'xsrf_cookies': True,
'static_path': os.path.join(BASE_DIRS, 'static'),
'template_path': os.path.join(BASE_DIRS, 'templates'),
'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0=',
}

在配置文件 config.py 中添加'xsrf_cookies': True

应用

  • 模板中应用

    • {% module xsrf_form_html() %}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>XSRF</title>
      </head>
      <body>
      <form action="/postfile" method="post">
      {% module xsrf_form_html() %}
      姓名:<input type="text" name="username"/>
      <hr/>
      密码:<input type="password" name="passwd"/>
      <hr/>
      <input type="submit" value="登陆"/>
      </form>
      </body>
      </html>
    • 作用
      • 为浏览器设置了_xsrf 的安全 cookie,这个 cookie 在关闭浏览器后会失效
      • 为模板表单添加了一个隐藏的域,名为_xsrf,值为_xsrf 这个 cookie 的值
        1
        <input type="hidden" name="_xsrf" value="2|b4826962|86075754385b9af629ccee663023729a|1596862208">
  • 非模板中应用

    • 手动设置 _xsrf 的 cookie
      1
      2
      3
      4
      5
      class SetXSRFCookie(RequestHandler):
      def get(self, *args, **kwargs):
      # 设置_xsrf的cookie
      self.xsrf_token
      self.finish("Ok")
    • 第一种:基本上不会用
      手动创建的 input,并设置 name 属性值为_xsrf,value 属性值为名为_xsrf 的 cookie 的值
      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
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>XSRF</title>
      </head>
      <body>
      <form action="/postfile" method="post">
      <input id="hi" type="hidden" name="_xsrf" value="">
      姓名:<input type="text" name="username"/>
      <hr/>
      密码:<input type="password" name="passwd"/>
      <hr/>
      <input type="submit" value="登陆"/>
      </form>
      <script>
      function getCookie(name) {
      var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
      return cook ? cook[1] : undefined
      }

      console.log(getCookie('_xsrf'))
      document.getElementById("hi").value = getCookie("_xsrf")
      </script>
      </body>
      </html>
    • 第二种:$.post 发起 Ajax 请求
      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
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>XSRF</title>
      <script type="text/javascript" charset="UTF-8" src="{{ static_url('js/jquery.min.js') }}"></script>
      </head>
      <body>
      姓名:<input type="text" name="username"/>
      <hr/>
      密码:<input type="password" name="passwd"/>
      <hr/>
      <button onclick="login()">登陆</button>

      <script>
      function getCookie(name) {
      var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
      return cook ? cook[1] : undefined
      }

      function login() {
      // _xsrf=wertyu&username=sunck&passwd=23456
      $.post("/postfile", "_xsrf=" + getCookie('_xsrf') + "&username=" + "sunck" + "&passwd=" + "123456789", function (data) {
      alert("OK")
      })
      }
      </script>
      </body>
      </html>
    • 第三种:$.ajax 发起 Ajax 请求
      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
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>XSRF</title>
      <script type="text/javascript" charset="UTF-8" src="{{ static_url('js/jquery.min.js') }}"></script>
      </head>
      <body>
      姓名:<input type="text" name="username"/>
      <hr/>
      密码:<input type="password" name="passwd"/>
      <hr/>
      <button onclick="login()">登陆</button>

      <script>
      function getCookie(name) {
      var cook = document.cookie.match("\\b" + name + "=([^;]*)\\b")
      return cook ? cook[1] : undefined
      }

      function login() {
      data = {
      "username": "sunck",
      "passwd": "123456"
      }
      var datastr = JSON.stringify(data)
      $.ajax({
      url: "/postfile",
      method: "POST",
      data: datastr,
      success: function (data) {
      alert("OK")
      },
      headers: {
      "X-XSRFToken": getCookie("_xsrf")
      }
      })
      }
      </script>
      </body>
      </html>
    • 问题
      需要手动添加 _xsrf 的 cookie,需要在进入主页时就自动设置上 _xsrf 的 cookie
      1
      2
      (r'/(.*)$', index.StaticFileHandler,
      {"path": os.path.join(config.BASE_DIRS, "static/html"), "default_filename": "index.html"})
      1
      2
      3
      4
      class StaticFileHandler(tornado.web.StaticFileHandler):
      def __init__(self, *args, **kwargs):
      super(StaticFileHandler, self).__init__(*args, **kwargs)
      self.xsrf_token
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>主页</title>
      </head>
      <body>
      <h1>这是主页</h1>
      </body>
      </html>

      static -> html -> index.html

用户验证

  • 指在收到用户请求后进行预先判断用户的认证状态 (是否登录),若验证通过则正常处理,否则进如到登录界面
  • tornado.web.authenticated 装饰器,Tornado 将确保这个方法的主体只有合法的用户才能调用
  • get_current_user()
    • 验证用户的逻辑应该写在该方法中,如果该方法返回的为 True 说明验证成功,否则验证失败
    • 验证失败,请求会将访客重定向到配置中的 login_url 所指定的路由
      1
      2
      3
      4
      5
      6
      7
      8
      settings = {
      'debug': False,
      'xsrf_cookies': True,
      'login_url': '/login',
      'static_path': os.path.join(BASE_DIRS, 'static'),
      'template_path': os.path.join(BASE_DIRS, 'templates'),
      'cookie_secret': 'wtsaTrAfTBuZTx5f9yBhX8ZVZ479HknqnSMKKAmau+0=',
      }

      'login_url': '/login'

  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>登录</title>
    </head>
    <body>
    <form action="{{ url }}" method="post">
    {% module xsrf_form_html() %}
    姓名:<input type="text" name="username"/>
    <hr/>
    密码:<input type="password" name="passwd"/>
    <hr/>
    <input type="submit" value="登陆"/>
    </form>
    </body>
    </html>

    templates -> login.html

    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
    class LoginHandler(RequestHandler):
    def get(self, *args, **kwargs):
    next = self.get_argument("next", "/")
    url = "login?next=" + next
    self.render("login.html", url=url)

    def post(self, *args, **kwargs):
    name = self.get_argument("username")
    passwd = self.get_argument("passwd")
    if name == "1" and passwd == "1":
    next = self.get_argument("next", "/")
    self.redirect(next + "?flag=logined")
    else:
    next = self.get_argument("next", "/")
    self.redirect("/login?next=" + next)


    class HomeHandler(RequestHandler):
    def get_current_user(self):
    # /home
    flag = self.get_argument("flag", None)
    return flag

    @tornado.web.authenticated
    def get(self, *args, **kwargs):
    self.render("home.html")


    class CartHandler(RequestHandler):
    def get_current_user(self):
    # /home
    flag = self.get_argument("flag", None)
    return flag

    @tornado.web.authenticated
    def get(self, *args, **kwargs):
    self.render("cart.html")

    index.py