本文讲的是 Tornado 框架中的 “secure cookie” 实现。
效果
user =
"2|1:0|10:1545880175|4:user|4:MTA=|98ba9d8916cdb090dc573d3008185f2b0a5b41a7b420249f56398cf2aab2d8dd";
竖线隔开,长度:值
的格式,拆分一下的话就是 5 段:
2
版本,这个比较特殊,没有长度1:0
次版本,保留字段,默认 010:1545880175
时间戳,服务器当地时间(默认:time.time
),精确到秒4:user
字段名4:MTA=
字段值,Base64 编码,解析过来就是 1098ba9d8916cdb090dc573d3008185f2b0a5b41a7b420249f56398cf2aab2d8dd
Cookie 封装
class RequestHandler(object):
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")
return create_signed_value(self.application.settings["cookie_secret"],
name, value, version=version)
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)
加密方法
思路就是加个签名,防止伪造、篡改。
加密方法有两个版本:
- v1
值|时间戳|签名
将原来的值 Base64 之后带上时间戳和签名
签名算法:sha1
签名内容:字段名 字段值 时间戳
(没有空格隔开) - v2
版本|次版本|时间戳|字段名|值|签名
,同时,每一段都采用长度:内容
的格式 (就是开头举得例子)
签名算法:sha256
签名内容:版本|次版本|时间戳|字段名|值|
第二版 是 2014 年 v3.2.1 引入 (dd7bc4),设计更加周全,忽略第一版,就研究一下第二版的实现:
import time, base64, hmac, hashlib
def utf8(value):
if isinstance(value, bytes):
return value
return value.encode("utf-8")
def create_signed_value(secret, name, value):
timestamp = str(int(time.time())).encode('ascii')
value = base64.b64encode(utf8(value))
def format_field(s):
return utf8("%d:" % len(s)) + utf8(s)
to_sign = b"|".join([
b"2|1:0",
format_field(timestamp),
format_field(name),
format_field(value),
b''])
signature = create_signature(secret, to_sign)
return to_sign + signature
def decode_signed_value(secret, name, value, max_age_days=31):
def _consume_field(s):
length, _, rest = s.partition(b':')
n = int(length)
field_value = rest[:n]
# In python 3, indexing bytes returns small integers; we must
# use a slice to get a byte string as in python 2.
if rest[n:n + 1] != b'|':
raise ValueError("malformed v2 signed value field")
rest = rest[n + 1:]
return field_value, rest
# 切割
rest = value[2:] # remove version number
try:
key_version, rest = _consume_field(rest)
timestamp, rest = _consume_field(rest)
name_field, rest = _consume_field(rest)
value_field, rest = _consume_field(rest)
except ValueError:
return None
# 签名验证
# 我觉得这个操作应该放在字段名校验和有效期校验之后
passed_sig = rest
signed_string = value[:-len(passed_sig)]
expected_sig = create_signature(secret, signed_string)
if not hmac.compare_digest(passed_sig, expected_sig):
return None
# 字段名验证:第一版没有这个
if name_field != utf8(name):
return None
# 有效期校验
timestamp = int(timestamp)
if timestamp < time.time() - max_age_days * 86400:
return None
try:
return base64.b64decode(value_field)
except Exception:
return None
def create_signature(secret, s):
hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
hash.update(utf8(s))
return utf8(hash.hexdigest())
print(create_signature('hello', 'world'))
# b'f1ac9702eb5faf23ca291a4dc46deddeee2a78ccdaf0a412bed7714cfffb1cc4'
encoded = create_signed_value('hello', 'user', '10')
print(encoded)
# b'2|1:0|10:1545880245|4:user|4:MTA=|680337e612a4360e74343b00f979586fbc7a376a9b2e0c13f44389a5d5085efa'
print(decode_signed_value('hello', 'user', encoded))
# b'10'