Cookie和Session的理解:

具体Cookie的介绍,可以参考:HTTP Cookie详解

可以先查看之前的一篇文章:Tornado的Cookie过期问题

XSRF跨域请求伪造(Cross-Site-Request-Forgery):

简单的说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

详细细节请参考:XSRF介绍

因为Tornado中XSRF机制的实现是基于Cookie的(XSRF验证信息是保存在Cookie中),所以我们先来分析Tornado中Cookie源码的实现。

Tornado中Cookie源码分析:

set_secure_cookie模块:

用法介绍:

  set_secure_cookie方法是对set_cookie方法的包装。要使用该方法,必须在 Application 中的 settings中指定"cookie_secret"(应该是一个经过HMAC加密的够长且随机的字节序列),如果想读取这个cookie设置,可以通过get_secure_cookie方法(后文会进行介绍)。

参数介绍:

  expires_days:设置cookie在浏览器端的有效期,通过源码可以知道,其默认为30天。注意,其该参数,跟get_secure_cookie方法中的”max_age_days“没有必然的联系,可以使用一个小于”expires_days“的”max_age_days“在服务端来控制安全Cookie的有效期。

  version:该参数主要是为了兼容旧的签名方式,版本号1使用SHA1签名,版本2使用SHA256签名;对于不同的版本,在后文get_secure_cookie方法中,解析Cookie的时候对应着不同的版本解析方法,最新版本的Tornado默认是使用SHA156签名的版本2。

  name,value:这个则是相应的Cookie名称和对应的Cookie的值。

源码:

 def set_secure_cookie(self, name, value, expires_days=30, version=None,**kwargs):
self.set_cookie(name, self.create_signed_value(name, value,version=version),
expires_days=expires_days, **kwargs) def create_signed_value(self, name, value, version=None):
self.require_setting("cookie_secret", "secure cookies")
secret = self.application.settings["cookie_secret"]
key_version = None
if isinstance(secret, dict):
if self.application.settings.get("key_version") is None:
raise Exception("key_version setting must be used for secret_key dicts")
key_version = self.application.settings["key_version"]
return create_signed_value(secret, name, value, version=version,key_version=key_version)

 开始直接调用 self.require_setting("cookie_secret", "secure cookies") 来判断是否设置了签名密钥。

 之后就通过 create_signed_value() 方法对不同的cookie进行不同的签名方式:

 def create_signed_value(secret, name, value, version=None, clock=None,
key_version=None):
if version is None:
version = DEFAULT_SIGNED_VALUE_VERSION
if clock is None:
clock = time.time timestamp = utf8(str(int(clock())))
value = base64.b64encode(utf8(value))
if version == 1:
signature = _create_signature_v1(secret, name, value, timestamp)
value = b"|".join([value, timestamp, signature])
return value
elif version == 2:
def format_field(s):
return utf8("%d:" % len(s)) + utf8(s)
to_sign = b"|".join([
b"",
format_field(str(key_version or 0)),
format_field(timestamp),
format_field(name),
format_field(value),
b'']) if isinstance(secret, dict):
assert key_version is not None, 'Key version must be set when sign key dict is used'
assert version >= 2, 'Version must be at least 2 for key version support'
secret = secret[key_version] signature = _create_signature_v2(secret, to_sign)
return to_sign + signature
else:
raise ValueError("Unsupported version %d" % version)

Cookie 值通过 value = base64.b64encode(utf8(value)) 进行 base64 编码转换,所以 set_secure_cookie 能支持任意的字符,这与 set_cookie 方法不同:

分析:

  看下set_cookie()的源码(仅截取部分):

 def set_cookie(self, name, value, domain=None, expires=None, path="/",
expires_days=None, **kwargs):
name = escape.native_str(name)
value = escape.native_str(value)
if re.search(r"[\x00-\x20]", name + value):
raise ValueError("Invalid cookie %r: %r" % (name, value))

  在escape模块中找到对应的 native_str() 方法:escape.py

 if str is unicode_type:
native_str = to_unicode
else:
native_str = utf8

  对于 unicode_type 的判断,其定义在 util模块中:util.py

 bytes_type = bytes
if PY3:
unicode_type = str
basestring_type = str
else:
# The names unicode and basestring don't exist in py3 so silence flake8.
unicode_type = unicode # noqa
basestring_type = basestring # noqa

结论:

  python2 是转换为 str,python3 时转换为 unicode string,且不允许输入 “\x00-\x20” 之间的字符,其实现代码中由正则表达式来检查。

接着回过头看上面的源码:

  version字段,默认是设置为DEFAULT_SIGNED_VALUE_VERSION(在源码中最开始定义了 DEFAULT_SIGNED_VALUE_VERSION = 2)。如果要指定版本,则需要在 set_secure_cookie() 方法中通过参数传递进来进行设置,我们也可以发现:

    • 对于版本1,version=1:简单的 “value|timestamp|signature” 拼接
    • 对于版本2,version=2:其增加了几个字段,并且返回记录了字符串的长度,尤其是预留的 key_version 字段为后续轮流使用多个 cookie_secret 提供了支持。并且对整个字符串进行了加密处理,版本1仅仅加密了value。

  版本1签名方式:使用的SHA1

 def _create_signature_v1(secret, *parts):
hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
for part in parts:
hash.update(utf8(part))
return utf8(hash.hexdigest())

  版本2签名方式:使用的SHA256

 def _create_signature_v2(secret, s):
hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
hash.update(utf8(s))
return utf8(hash.hexdigest())

get_secure_cookie模块:

  get_secure_cookie 方法签名中的 value 参数指的是通过 set_secure_cookie 加密签名后的 Cookie 值,默认是 None 则会从客户端发送回来的 Cookies 中获取指定名称name的 Cookie 值作为 value。然后再传入 max_age_days, min_version等值进行Cookie的解码验证。

源码:

 def get_secure_cookie(self, name, value=None, max_age_days=31,
min_version=None):
self.require_setting("cookie_secret", "secure cookies")
if value is None:
value = self.get_cookie(name)
return decode_signed_value(self.application.settings["cookie_secret"],
name, value, max_age_days=max_age_days,
min_version=min_version)

解码验证函数:decode_signed_value()

 def decode_signed_value(secret, name, value, max_age_days=31,
clock=None, min_version=None):
if clock is None:
clock = time.time
if min_version is None:
min_version = DEFAULT_SIGNED_VALUE_MIN_VERSION
if min_version > 2:
raise ValueError("Unsupported min_version %d" % min_version)
if not value:
return None value = utf8(value)
version = _get_version(value) if version < min_version:
return None
if version == 1:
return _decode_signed_value_v1(secret, name, value,
max_age_days, clock)
elif version == 2:
return _decode_signed_value_v2(secret, name, value,
max_age_days, clock)
else:
return None

  默认的 min_version 为 DEFAULT_SIGNED_VALUE_MIN_VERSION(在源码中最开始定义了 DEFAULT_SIGNED_VALUE_MIN_VERSION = 1),对于旧版本(版本 1 )加密签名的 cookie 数据中没有版本号这个字段,默认取 1。然后与指定的 min_version 进行比较,仅当大于等于 min_version 才进行下一步验证。版本 1 由函数 _decode_signed_value_v1 验证,版本 2 由 函数 _decode_signed_value_v2 验证,这两个函数主要就是按照对应签名格式解析数据,并对目标签名和时间戳等字段进行比较验证。

版本1解码:

 def _decode_signed_value_v1(secret, name, value, max_age_days, clock):
parts = utf8(value).split(b"|")
if len(parts) != 3:
return None
signature = _create_signature_v1(secret, name, parts[0], parts[1])
if not _time_independent_equals(parts[2], signature):
gen_log.warning("Invalid cookie signature %r", value)
return None
timestamp = int(parts[1])
if timestamp < clock() - max_age_days * 86400:
gen_log.warning("Expired cookie %r", value)
return None
if timestamp > clock() + 31 * 86400:
gen_log.warning("Cookie timestamp in future; possible tampering %r",
value)
return None
if parts[1].startswith(b""):
gen_log.warning("Tampered cookie %r", value)
return None
try:
return base64.b64decode(parts[0])
except Exception:
return None

版本2解码:

 def _decode_signed_value_v2(secret, name, value, max_age_days, clock):
try:
key_version, timestamp, name_field, value_field, passed_sig = _decode_fields_v2(value)
except ValueError:
return None
signed_string = value[:-len(passed_sig)] if isinstance(secret, dict):
try:
secret = secret[key_version]
except KeyError:
return None expected_sig = _create_signature_v2(secret, signed_string)
if not _time_independent_equals(passed_sig, expected_sig):
return None
if name_field != utf8(name):
return None
timestamp = int(timestamp)
if timestamp < clock() - max_age_days * 86400:
# The signature has expired.
return None
try:
return base64.b64decode(value_field)
except Exception:
return None

注:需要说一下的是由于版本 1 的设计缺陷,没有对 timestamp 进行签名,为了尽可能防止攻击者篡改时间戳来进行攻击, _decode_signed_value_v1 函数对 timestamp 执行了额外的检查(timestamp > clock() + 31 * 86400),但这个检查并不能完全杜绝此类攻击。这应该也是重新设计版本 2 的一个原因。

Tornado中XSRF源码分析:

  通过上面的Cookie分析后,知道不同版本的Cookie含有的相应组成字段,如果我们想要使用XSRF机制的话,我们需要在Application的Settings中设置参数:“xsrf_cookie_version”。我们会在Cookie中,设置一个“_xsrf”字段,然后所有的POST请求中包含一个“_xsrf”字段,如果其与服务器上的“_xsrf”值无法匹配,那么服务器会认为其有一个潜在的跨域伪造风险而拒绝表单的提交。从而防止跨域请求伪造。

  在tornado.web.RequestHandler 中与生成跨站请求伪造 token 直接相关的是 xsrf_token 属性和 xsrf_form_html 方法。

xsrf_token() 模块:

 def xsrf_token(self):
if not hasattr(self, "_xsrf_token"):
version, token, timestamp = self._get_raw_xsrf_token()
output_version = self.settings.get("xsrf_cookie_version", 2)
cookie_kwargs = self.settings.get("xsrf_cookie_kwargs", {})
if output_version == 1:
self._xsrf_token = binascii.b2a_hex(token)
elif output_version == 2:
mask = os.urandom(4)
self._xsrf_token = b"|".join([
b"",
binascii.b2a_hex(mask),
binascii.b2a_hex(_websocket_mask(mask, token)),
utf8(str(int(timestamp)))])
else:
raise ValueError("unknown xsrf cookie version %d",
output_version)
if version is None:
expires_days = 30 if self.current_user else None
self.set_cookie("_xsrf", self._xsrf_token,
expires_days=expires_days,
**cookie_kwargs)
return self._xsrf_token

首先,通过 _get_raw_xsrf_token() 方法,从cookie中解析出相应的字段:

 def _get_raw_xsrf_token(self):
if not hasattr(self, '_raw_xsrf_token'):
cookie = self.get_cookie("_xsrf")
if cookie:
version, token, timestamp = self._decode_xsrf_token(cookie)
else:
version, token, timestamp = None, None, None
if token is None:
version = None
token = os.urandom(16)
timestamp = time.time()
self._raw_xsrf_token = (version, token, timestamp)
return self._raw_xsrf_token

找到名为 “_xsrf” 的cookie,然后通过 _decode_xsrf_token() 方法解码出 (version,token,timestamp)以元祖的形式返回,同时其会对版本1进行兼容(版本1没有timestamp和version字段),:

 def _decode_xsrf_token(self, cookie):
try:
m = _signed_value_version_re.match(utf8(cookie))
if m:
version = int(m.group(1))
if version == 2:
_, mask, masked_token, timestamp = cookie.split("|")
mask = binascii.a2b_hex(utf8(mask))
token = _websocket_mask(
mask, binascii.a2b_hex(utf8(masked_token)))
timestamp = int(timestamp)
return version, token, timestamp
else:
raise Exception("Unknown xsrf cookie version")
else:
version = 1
try:
token = binascii.a2b_hex(utf8(cookie))
except (binascii.Error, TypeError):
token = utf8(cookie)
timestamp = int(time.time())
return (version, token, timestamp)
except Exception:
gen_log.debug("Uncaught exception in _decode_xsrf_token",
exc_info=True)
return None, None, None

xsrf_token检测check_xsrf_cookie模块:

对 xsrf_token 的检查在 _execute 方法(仅仅显示部分代码)中委托 check_xsrf_cookie 方法进行,代码如下所示:

 def _execute(self, transforms, *args, **kwargs):
......
if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
self.application.settings.get("xsrf_cookies"):
self.check_xsrf_cookie()
......
 def check_xsrf_cookie(self):
token = (self.get_argument("_xsrf", None) or
self.request.headers.get("X-Xsrftoken") or
self.request.headers.get("X-Csrftoken"))
if not token:
raise HTTPError(403, "'_xsrf' argument missing from POST")
_, token, _ = self._decode_xsrf_token(token)
_, expected_token, _ = self._get_raw_xsrf_token()
if not token:
raise HTTPError(403, "'_xsrf' argument has invalid format")
if not _time_independent_equals(utf8(token), utf8(expected_token)):
raise HTTPError(403, "XSRF cookie does not match POST argument")

check_xsrf_cookie 方法代码显示与 cookie 中的 token 进行比较的 token 来源于请求参数 _xsrf 或者 HTTP 头域(X-Xsrftoken 或者 X-Csrftoken)。目前仅比较 token 值,对其中的 timestamp 和 version 字段不做比较验证。

最后对xsrf_form_html方法进行介绍:

xsrf_form_html 就是返回一个隐藏的 HTML < input/> 元素,用于包含在页面的 Form 元素中以便在 POST 请求时将 token 发送给服务端验证。

它定义了“_xsrf”输入值,其会检查所有POST要求防止跨站点请求伪造。如果在Application中的settings中已经设置好了“xsrf_cookies=True”,那么必须在所有HTML表单中的包含该HTML函数。

在template中,这个方法可以被调用通过 “{%module xsrf_form_html()%}”

 def xsrf_form_html(self):
return '<input type="hidden" name="_xsrf" value="' + \
escape.xhtml_escape(self.xsrf_token) + '"/>'

  

  

Tornado源码分析 --- Cookie和XSRF机制的更多相关文章

  1. Tornado源码分析 --- 静态文件处理模块

    每个web框架都会有对静态文件的处理支持,下面对于Tornado的静态文件的处理模块的源码进行分析,以加强自己对静态文件处理的理解. 先从Tornado的主要模块 web.py 入手,可以看到在App ...

  2. Solr4.8.0源码分析(19)之缓存机制(二)

    Solr4.8.0源码分析(19)之缓存机制(二) 前文<Solr4.8.0源码分析(18)之缓存机制(一)>介绍了Solr缓存的生命周期,重点介绍了Solr缓存的warn过程.本节将更深 ...

  3. Solr4.8.0源码分析(18)之缓存机制(一)

    Solr4.8.0源码分析(18)之缓存机制(一) 前文在介绍commit的时候具体介绍了getSearcher()的实现,并提到了Solr的预热warn.那么本文开始将详细来学习下Solr的缓存机制 ...

  4. tornado源码分析-iostream

    tornado源码分析-iostream 1.iostream.py作用 用来异步读写文件,socket通信 2.使用示例 import tornado.ioloop import tornado.i ...

  5. java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制

    通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...

  6. Java ArrayList源码分析(含扩容机制等重点问题分析)

    写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...

  7. Tornado源码分析 --- Redirect重定向

    “重定向”简单介绍: “重定向”指的是HTTP重定向,是HTTP协议的一种机制.当client向server发送一个请求,要求获取一个资源时,在server接收到这个请求后发现请求的这个资源实际存放在 ...

  8. Struts2 源码分析——拦截器的机制

    本章简言 上一章讲到关于action代理类的工作.即是如何去找对应的action配置信息,并执行action类的实例.而这一章笔者将讲到在执行action需要用到的拦截器.为什么要讲拦截器呢?可以这样 ...

  9. Tornado源码分析系列之一: 化异步为'同步'的Future和gen.coroutine

    转自:http://blog.nathon.wang/2015/06/24/tornado-source-insight-01-gen/ 用Tornado也有一段时间,Tornado的文档还是比较匮乏 ...

随机推荐

  1. vue自定义过滤器的创建和使用

    1.简单介绍   过滤器的作用:实现数据的筛选.过滤.格式化.   过滤器的本质是一个有参数,有返回值的方法.   过滤器可以用在两个地方:双花括号插值和v-bind表达式(后者从2.1.0+开始支持 ...

  2. Centos设置SSH限制登录用户及IP

    1,系统版本查看 2,编辑ssh配置文件 vim /etc/ssh/sshd_config 在尾部加一行 允许sysman用户从ip1.1.1.*登录 3,重启sshd即可 /etc/init.d/s ...

  3. Golang学习-第二篇 搭建一个简单的Go Web服务器

    序言 由于本人一直从事Web服务器端的程序开发,所以在学习Golang也想从Web这里开始学起,如果对Golang还不太清楚怎么搭建环境的朋友们可以参考我的上一篇文章 Golang的简单介绍及Wind ...

  4. c# devExpress控件 comboBoxEdit,gridControl1,labelcontrol

    一.comboBoxEdit:下拉框 属性 添加项:Properties->items 二.gridControl gridControl与Gridview的区别:前者是容器,后者为视图 2)g ...

  5. HDU1530 Maximum Clique dp

    正解:dp 解题报告: 这儿是传送门 又是个神仙题趴QAQ 这题就直接说解法辣?主要是思想比较难,真要说有什么不懂的知识点嘛也没有,所以也就没什么好另外先提一下的知识点QAQ 首先取反,就变成了求最大 ...

  6. linux彻底删除nginx

    卸载 删除 nginx 1.删除nginx,–purge包括配置文件 sudo apt-get --purge remove nginx 1 2.自动移除全部不使用的软件包 sudo apt-get ...

  7. 第1章 1.7计算机网络概述--理解OSI参考模型分层思想

    OSI七层模型,知识参考理论. 分层标准的好处: 1.不同的硬件生产商生产的硬件产品,连通后就可以用了,有助于互联网发展. 2.分层,分成不同的模块,某一层的变化,不会影响其他层.如:IPv4改为IP ...

  8. 双舵轮AGV里程计、运动控制核心算法

    舵轮AGV可以通过调整两个舵轮的角度及速度,可以使小车在不转动车头的情况下实现变道,转向等动作,甚至可以实现沿任意点为半径的转弯运动,有很强的灵活性. 因此在AGV行业,这种驱动方式应用很广,但是目前 ...

  9. 29张截图-全新安装CentOS7.5-超详细!

    目录 全新安装CentOS7.5 配置虚拟机 调整网卡名称 配置时区,分区,关闭安全工具 配置网络参数 配置root账户密码 参考链接 全新安装CentOS7.5 可以到这里下载镜像https://m ...

  10. 【转】Deep Learning(深度学习)学习笔记整理系列之(五)

    9.2.Sparse Coding稀疏编码 如果我们把输出必须和输入相等的限制放松,同时利用线性代数中基的概念,即O = a1*Φ1 + a2*Φ2+….+ an*Φn, Φi是基,ai是系数,我们可 ...