揭开Flask app.secret_key的神秘面纱

192 投票
2 回答
117092 浏览
提问于 2025-04-17 22:42

如果没有设置 app.secret_key,Flask 就不允许你设置或访问会话字典。

这就是 Flask 用户指南 对这个问题的全部说明。

我对网页开发非常陌生,对任何安全相关的东西也一无所知。我想了解 Flask 在背后到底在做些什么。

  • 为什么 Flask 要求我们设置这个 secret_key 属性?
  • Flask 是如何使用 secret_key 属性的?

2 个回答

190

下面的内容主要讲的是签名Cookie,这是一种实现会话的方式(在网页应用中使用)。Flask提供了普通的(未签名的)Cookie(通过request.cookiesresponse.set_cookie())和签名Cookie(通过flask.session)。这个回答分为两部分:第一部分描述了如何生成签名Cookie,第二部分以一系列问答的形式呈现,讨论该方案的不同方面。示例中的语法是Python3,但这些概念也适用于之前的版本。

什么是SECRET_KEY(或者如何创建签名Cookie)?

签名Cookie是一种防止Cookie被篡改的措施。在签名Cookie的过程中,SECRET_KEY的使用方式类似于在对密码进行哈希处理之前添加“盐”。以下是这个概念的(广泛)简化描述。示例中的代码仅用于说明,很多步骤被省略,并不是所有的函数实际上都存在。这里的目的是提供对主要思想的总体理解,但实际的实现可能会复杂一些。此外,请记住,Flask在后台已经为你提供了大部分功能。因此,除了通过会话API设置Cookie的值和提供SECRET_KEY外,自己重新实现这个过程不仅不明智,而且没有必要:

简单的Cookie签名方法

在发送响应到浏览器之前:

( 1 ) 首先要建立一个SECRET_KEY。它应该只为应用程序所知,并且在应用程序的生命周期内保持相对不变,包括应用程序重启时。

# choose a salt, a secret string of bytes
>>> SECRET_KEY = 'my super secret key'.encode('utf8')

( 2 ) 创建一个Cookie

>>> cookie = make_cookie(
...     name='_profile', 
...     content='uid=382|membership=regular',
...     ...
...     expires='July 1 2030...'
... )

>>> print(cookie)
name: _profile
content: uid=382|membership=regular...
    ...
    ...
expires: July 1 2030, 1:20:40 AM UTC

( 3 ) 为了创建签名,将SECRET_KEY附加(或前置)到Cookie的字节字符串中,然后从这个组合生成一个哈希值。

# encode and salt the cookie, then hash the result
>>> cookie_bytes = str(cookie).encode('utf8')
>>> signature = sha1(cookie_bytes+SECRET_KEY).hexdigest()
>>> print(signature)
7ae0e9e033b5fa53aa....

( 4 ) 现在将签名附加到原始Cookie的content字段的一端。

# include signature as part of the cookie
>>> cookie.content = cookie.content + '|' + signature
>>> print(cookie)
name: _profile
content: uid=382|membership=regular|7ae0e9...  <--- signature
domain: .example.com
path: /
send for: Encrypted connections only
expires: July 1 2030, 1:20:40 AM UTC

然后将其发送给客户端。

# add cookie to response
>>> response.set_cookie(cookie)
# send to browser --> 

在接收到浏览器的Cookie时:

( 5 ) 当浏览器将这个Cookie返回给服务器时,从Cookie的content字段中去掉签名,以获取原始Cookie。

# Upon receiving the cookie from browser
>>> cookie = request.get_cookie()
# pop the signature out of the cookie
>>> (cookie.content, popped_signature) = cookie.content.rsplit('|', 1)

( 6 ) 使用原始Cookie和应用程序的SECRET_KEY,按照第3步的方法重新计算签名。

# recalculate signature using SECRET_KEY and original cookie
>>> cookie_bytes = str(cookie).encode('utf8')
>>> calculated_signature = sha1(cookie_bytes+SECRET_KEY).hexdigest()

( 7 ) 将计算结果与刚收到的Cookie中剥离出的签名进行比较。如果它们匹配,我们就知道Cookie没有被篡改。但如果即使只是添加了一个空格,签名也不会匹配。

# if both signatures match, your cookie has not been modified
>>> good_cookie = popped_signature==calculated_signature

( 8 ) 如果它们不匹配,你可以采取多种措施,比如记录事件、丢弃Cookie、发放新的Cookie、重定向到登录页面等。

>>> if not good_cookie:
...     security_log(cookie)

基于哈希的消息认证码(HMAC)

消息认证码或MAC

我之前提到,上面的例子是对这个概念的过于简化,自己实现签名并不是一个好主意。这是因为Flask中用于签名Cookie的算法叫做HMAC,比上面的简单步骤要复杂一些。总体思路是相同的,但由于讨论范围之外的原因,计算过程稍微复杂一些。如果你仍然有兴趣自己动手,Python有一些模块可以帮助你入门 :) 这里是一个起始代码块:

import hmac
import hashlib

def create_signature(secret_key, msg, digestmod=None):
    if digestmod is None:
        digestmod = hashlib.sha1
    mac = hmac.new(secret_key, msg=msg, digestmod=digestmod)
    return mac.digest()

关于HMAChashlib的文档。


揭秘SECRET_KEY :)

在这个上下文中“签名”是什么?

它是一种确保某些内容没有被任何未授权的人或实体修改的方法。

最简单的签名形式之一是“校验和”,它简单地验证两份数据是否相同。例如,在从源代码安装软件时,首先确认你的源代码副本与作者的完全相同是很重要的。常见的方法是将源代码通过加密哈希函数处理,并将输出与项目主页上发布的校验和进行比较。

假设你要从一个网络镜像下载一个项目的源代码压缩文件。项目网页上发布的SHA1校验和是'eb84e8da7ca23e9f83....'

# so you get the code from the mirror
download https://mirror.example-codedump.com/source_code.tar.gz
# you calculate the hash as instructed
sha1(source_code.tar.gz)
> eb84e8da7c....

如果两个哈希值相同,你就知道你有一个完全相同的副本。

什么是Cookie?

关于Cookie的详细讨论超出了这个问题的范围。这里提供一个概述,因为对Cookie的基本理解有助于更好地理解SECRET_KEY的作用。我强烈建议你自己阅读一些关于HTTP Cookie的资料。

在网页应用中,常见的做法是使用客户端(网页浏览器)作为轻量级缓存。Cookie就是这种做法的一种实现。Cookie通常是服务器通过HTTP响应的头部添加的一些数据。它由浏览器保存,并在发出请求时通过HTTP头部将其发送回服务器。Cookie中包含的数据可以用来模拟所谓的状态性,即服务器与客户端保持持续连接的假象。只是在这种情况下,保持连接“活着”的不是一根线,而是应用程序在处理客户端请求后状态的快照。这些快照在客户端和服务器之间来回传递。在接收到请求时,服务器首先读取Cookie的内容,以重新建立与客户端的对话上下文。然后在该上下文中处理请求,并在返回响应给客户端之前更新Cookie。这样就维持了持续会话的假象。

Cookie是什么样子的?

一个典型的Cookie看起来像这样:

name: _profile
content: uid=382|status=genie
domain: .example.com
path: /
send for: Encrypted connections only
expires: July 1 2030, 1:20:40 AM UTC

在任何现代浏览器中查看Cookie都很简单。例如,在Firefox中,进入偏好设置 > 隐私 > 历史 > 删除单个Cookie

content字段对应用程序来说是最相关的。其他字段主要携带元指令,以指定各种影响范围。

为什么要使用Cookie?

简短的回答是性能。使用Cookie可以减少在各种数据存储(内存缓存、文件、数据库等)中查找信息的需要,从而加快服务器应用程序的速度。请记住,Cookie越大,网络负担就越重,因此你在服务器上节省的数据库查找时间可能会在网络上传输时失去。仔细考虑要包含在Cookie中的内容。

为什么Cookie需要被签名?

Cookie用于保存各种信息,其中一些可能非常敏感。由于Cookie本身并不安全,因此需要采取一些辅助措施,才能在客户端和服务器之间被认为是安全的。签名Cookie特别解决了它们可能被篡改的问题,以试图欺骗服务器应用程序。还有其他措施可以缓解其他类型的漏洞,我鼓励你进一步阅读有关Cookie的资料。

Cookie是如何被篡改的?

Cookie以文本形式存储在客户端,可以轻松编辑。发送到你的服务器应用程序的Cookie可能由于多种原因被修改,其中一些可能并不单纯。想象一下,一个网页应用程序在Cookie中保存用户的权限信息,并根据这些信息授予特权。如果Cookie没有防篡改机制,任何人都可以修改自己的Cookie,将其状态从“role=visitor”提升为“role=admin”,而应用程序对此毫无察觉。

为什么签名Cookie需要SECRET_KEY

验证Cookie与之前描述的验证源代码略有不同。在源代码的情况下,原作者是信任的,并且拥有参考指纹(校验和),这个指纹是公开的。你不信任的是源代码,但你信任的是公开的签名。因此,要验证你的源代码副本,你只需确保计算出的哈希与公开的哈希匹配。

然而,在Cookie的情况下,应用程序并不跟踪签名,而是跟踪其SECRET_KEYSECRET_KEY就是参考指纹。Cookie携带一个声称是合法的签名。这里的合法性意味着签名是由Cookie的拥有者,即应用程序发出的,而在这种情况下,你不信任的是这个声明,你需要检查签名的有效性。为此,你需要在签名中包含一个只有你知道的元素,那就是SECRET_KEY。有人可能会更改Cookie,但由于他们没有秘密成分来正确计算有效的签名,因此无法伪造它。正如之前所述,这种指纹识别方式,即在校验和之上还提供一个秘密密钥,被称为消息认证码。

会话是什么?

经典实现中的会话是仅在content字段中携带一个ID的Cookie,即session_id。会话的目的与签名Cookie完全相同,即防止Cookie被篡改。然而,经典会话有不同的方法。在接收到会话Cookie时,服务器使用ID在其本地存储中查找会话数据,这可能是数据库、文件,或者有时是内存中的缓存。会话Cookie通常设置为在浏览器关闭时过期。由于需要进行本地存储查找,这种会话实现通常会带来性能损失。签名Cookie正成为一种更受欢迎的替代方案,这就是Flask会话的实现方式。换句话说,Flask会话就是签名Cookie,要在Flask中使用签名Cookie,只需使用其Session API。

为什么不对Cookie进行加密?

有时Cookie的内容可以在签名之前进行加密。如果它们被认为过于敏感而无法在浏览器中可见(加密隐藏内容),则会这样做。然而,仅仅签名Cookie则解决了不同的需求,即希望在浏览器中保持一定的可见性和可用性,同时防止它们被篡改。

如果我更改SECRET_KEY会发生什么?

通过更改SECRET_KEY,你将使之前用旧密钥签名的所有Cookie失效。当应用程序收到一个用旧SECRET_KEY签名的Cookie时,它会尝试用新的SECRET_KEY计算签名,而两个签名将不匹配,这个Cookie及其所有数据将被拒绝,就好像浏览器第一次连接到服务器一样。用户将被登出,他们的旧Cookie将被遗忘,里面存储的任何内容也会丢失。请注意,这与过期Cookie的处理方式不同。过期Cookie如果签名检查通过,可以延长其有效期。而无效签名则意味着一个简单的无效Cookie。

因此,除非你想使所有签名Cookie失效,否则尽量保持SECRET_KEY在较长时间内不变。

什么是好的SECRET_KEY

一个秘密密钥应该难以猜测。关于会话的文档中有一个很好的随机密钥生成方法:

>>> import os
>>> os.urandom(24)
'\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'

你可以复制这个密钥并将其粘贴到你的配置文件中作为SECRET_KEY的值。

除了使用随机生成的密钥外,你还可以使用复杂的单词、数字和符号的组合,或许以只有你知道的句子形式排列,编码为字节形式。

不要直接用一个每次调用都会生成不同密钥的函数来设置SECRET_KEY。例如,不要这样做:

# this is not good
SECRET_KEY = random_key_generator()

每次你的应用程序重启时,它都会获得一个新密钥,从而使之前的密钥失效。

相反,打开一个交互式的Python命令行,调用函数生成密钥,然后将其复制并粘贴到配置中。

127

任何需要加密的东西(为了防止攻击者篡改)都需要设置一个秘密密钥。对于Flask本身来说,这个“任何东西”就是Session对象,但其他扩展也可以使用同样的秘密密钥。

secret_key就是为SECRET_KEY这个配置项设置的值,或者你也可以直接设置它。

快速入门中的会话部分提供了关于你应该设置什么样的服务器端秘密的好建议。

加密依赖于秘密;如果你没有为加密设置一个服务器端的秘密,任何人都能破解你的加密,就像你的电脑密码一样。秘密和要签名的数据一起用来创建一个签名字符串,这个字符串是通过加密哈希算法生成的,难以重现;只有当你拥有完全相同的秘密原始数据时,才能重新创建这个值,这样Flask就能检测到是否有任何未经授权的更改。因为秘密从来不会和Flask发送给客户端的数据一起发送,所以客户端无法篡改会话数据并希望生成一个新的有效签名。

Flask使用itsdangerous来完成所有复杂的工作;会话使用itsdangerous.URLSafeTimedSerializer,并配有定制的JSON序列化器。

撰写回答