为什么这些自定义 Flask 会话接口的测试失败了?

Why do these tests fail for this custom Flask session interface?

我正在 Flask 中编写混合单页 web/PhoneGap 应用程序。由于PhoneGap应用中的cookies基本不可用,我实现了一个完全避免cookies的自定义session interface。它将会话数据存储在应用程序数据库中,并在 HTTP 请求和响应主体中显式传递会话 ID。

我创建了一个GitHub repository with a reduced testcase. It's still a sizeable project in its own right, but the Readme should help you to quickly find your way. The repo includes seven tests that all succeed when using Flask's default cookie-based session interface and all fail with my custom session interface. The main problem appears to be that data are sometimes not retained on the session object, but this is mysterious because the session object inherits from Python's builtin dict, which shouldn't spontaneously forget data. In addition, the session interface is straightforward and doesn't seem to make any obvious mistakes compared to Flask's example Redis session snippet

更令人沮丧的是,自定义会话界面似乎在实际应用程序中工作正常。只有单元测试失败。但是,由于这个原因,假设会话接口在所有情况下都能正常工作是不安全的。

非常感谢您的帮助。

编辑: Gist 不接受简化的测试用例,因为它包含目录。我现在将它移动到一个成熟的 GitHub 存储库。完成后我会再次更新此 post。

新编辑: 将简化的测试用例移至适当的 GitHub 存储库。自述文件仍然提到 "this Gist",抱歉。

您的问题主要归结为在您的测试请求中提供会话令牌。如果您不提供令牌,则会话为空。

我假设您的实际应用程序正在正确发送会话令牌,因此似乎可以正常工作。

修复测试用例以正确通过并不需要太多。

每个请求都尝试根据 post 参数加载会话

在您的会话实现中:

def open_session(self, app, request):
    s = Session()
    if 't' in request.form:
        ....

    return s

这意味着每个不是 POST(或 PUT)且未发送 t 的请求都将 有一个空白会话。

而基于 cookie 的实现将始终具有可用的会话令牌 并且能够加载以前请求的会话。

这是您的测试样本之一:

def test_authorize_captcha_expired(self):
    with self.client as c:
        with c.session_transaction() as s:
            s['captcha-answer'] = u'one two three'.split()
            s['captcha-expires'] = datetime.today() - timedelta(minutes=1)
        self.assertEqual(c.post('/test', data={
            'ca': 'one two three',
        }).status_code, 400)

您还没有为 /test 提供 post 的 t 值。因此它得到一个空白 没有 captcha-expires 密钥和 KeyError 的会话被引发。

您的会话需要 'token' 密钥才能保存

在您的会话实现中:

def save_session(self, app, session, response):
    if session.modified and 'token' in session:
        ...
        # save session to database
        ...

因此当你有:

with c.session_transaction() as s:
    s['captcha-answer'] = u'one two three'.split()
    s['captcha-expires'] = datetime.today() - timedelta(minutes=1)

实际上没有会话写入数据库。对于任何后续请求 采用。请注意,它确实 确实 需要写入数据库,因为 open_session 将尝试在每次请求时从数据库加载一些内容。

要解决大多数情况,您需要在创建会话时提供 'token',并为使用该令牌的任何请求提供 't'。

因此我上面使用的样本测试最终会像这样:

def test_authorize_captcha_expired(self):
    with self.client as c:
        token = generate_key(SystemRandom())
        with c.session_transaction() as s:
            s['token'] = token
            s['captcha-answer'] = u'one two three'.split()
            s['captcha-expires'] = datetime.today() - timedelta(minutes=1)
        self.assertEqual(c.post('/test', data={
            'ca': 'one two three',
            't': token
        }).status_code, 400)

当您回复 json

时,您更改了令牌

...但是您在发出后续请求时并未使用新令牌

def test_reply_to_reflection_passthrough(self):
    with self.client as c:
        token = 'abcdef'

        ...

        response2 = c.post('/reflection/1/reply', data={
            'p': 'test4',
            'r': 'testmessage',
            't': token,
        }, ...

到这里,post到/reflection/1/reply已经生成了一个新的 token 并保存,因此关键密钥 last-reply 不在 abcdef 识别的会话。如果这是基于 cookie 的会话,那么 last-reply 将可用于下一个请求。

所以要修复此测试...使用新令牌

def test_reply_to_reflection_passthrough(self):
    with self.client as c:

        ...

        response2 = c.post('/reflection/1/reply', data={

        ...

        token = session['token']
        with c.session_transaction(method="POST", data={'t':token}) as s:
            s['token'] = token
            s['last-request'] = datetime.now() - timedelta(milliseconds=1001)

        response3 = c.post('/reflection/1/reply', data={

        ...

重定向将丢失会话令牌

测试中test_bump:

def test_bump(self):
    response = self.client.post(
        '/admin/tip/action/',
        data = {'action': 'Bump', 'rowid': '1',},
        follow_redirects=True )
    self.assertIn(' tips have been bumped.', response.data)

将post重定向到/admin/tip/actionreturns。

您在此处检查是否存在即显消息。和闪讯 存储在会话中。

对于基于 cookie 的会话,会话 ID 会随后续的重定向请求再次发送。

由于您的会话 ID 被指定为 post 值,因此它不会再次发送,会话和即显消息将丢失。

解决此问题的方法不是遵循重定向,而是通过 flasks flash 方法检查会话中的数据集。

def test_bump(self):
    with self.client as c:
        token = generate_key(SystemRandom())
        with c.session_transaction() as s:
            s['token'] = token
        c.post('/admin/tip/action/',
               data={'action': 'Bump', 'rowid': '1', 't': token})

        with c.session_transaction(method="POST", data={'t': token}) as s:
            self.assertIn(' tips have been bumped.', s['_flashes'][0][1])

仅此而已

我已经发送了一个拉取请求,其中包含我上面描述的更改,您会发现默认的 flask 会话和您的会话实现的测试现在都通过了。