【问题标题】:Why do these tests fail for this custom Flask session interface?为什么这个自定义 Flask 会话接口的这些测试会失败?
【发布时间】:2015-12-05 15:07:09
【问题描述】:

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

我创建了一个GitHub repository 并减少了测试用例。它本身仍然是一个相当大的项目,但自述文件应该可以帮助您快速找到自己的方式。该 repo 包括七个测试,当使用 Flask 的默认基于 cookie 的会话界面时全部成功,而在我的自定义会话界面中全部失败。主要问题似乎是数据有时不会保留在会话对象上,但这很神秘,因为会话对象继承自 Python 的内置 dict,它不应该自发忘记数据。此外,会话界面简洁明了,与Flask's example Redis session snippet相比,似乎没有任何明显的错误。

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

我们将不胜感激。

编辑: Gist 不接受缩减的测试用例,因为它包含目录。我现在将其移至成熟的 GitHub 存储库。完成后我会再次更新这篇文章。

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

【问题讨论】:

  • 简要浏览一下您的测试表明您的帖子中没有包含t,但您的主要应用程序包含。不是 100% 确定,因为有很多东西需要查看。
  • @SeanVieira,略读可能有点太简短了。 :-) 有时测试帖子确实不包含t 字段,但目标是确认它会产生 40 倍的错误。使用 Flask 的默认会话实现时,所有测试都通过。
  • 我正在查看this test,看起来它正在等待成功的响应。我错过了什么吗?
  • 啊,对,很抱歉造成混乱。这实际上可能是那个特定测试中的问题(它是一个管理视图,它的工作方式与其他视图有点不同)!然而,在其他六次失败的测试中,发生了其他事情。
  • 如果您可以将项目缩减为单个 sn-p 将会很有帮助 :-) 无论如何,哪些数据保留在 Session 对象中?我想知道这是否可能是因为你继承自dict__setitem____setattr__ 的一些问题。你知道 Flask 是如何与你的 Session 对象交互的吗?

标签: python session flask customization


【解决方案1】:

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

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

修复测试用例无需太多时间即可正确通过。

每个请求都尝试根据 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 的帖子提供t 值。因此它得到一个空白 没有captcha-expires 键和KeyError 的会话被引发。

您的会话需要一个“令牌”密钥才能保存

在你的会话实现中:

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 将尝试在每次请求时从数据库中加载某些内容。

要解决大多数情况,您需要在创建会话时提供一个“令牌”,并为使用它的任何请求提供一个带有该令牌的“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,
        }, ...

到这里,/reflection/1/reply 的帖子生成了一个新的 令牌并保存它,因此关键密钥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)

/admin/tip/action 的帖子返回重定向。

在这里,您正在检查是否存在 Flash 消息。和闪信息 存储在会话中。

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

由于您的会话 id 被指定为 post 值,因此不会再次发送,会话和 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])

仅此而已

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

【讨论】:

  • 这看起来已经是一个很好的答案了。谢谢你的辛劳工作。我会详细审查你的拉取请求。
  • 我已经接受了你的回答,并会尽可能奖励赏金(此时我仍然需要等待三个小时)。
  • @Julian 太棒了!很高兴我能帮上忙。
  • 伟大的工作——尤其是在拉取请求方面:) 一个小提示@Julian——只要你不要忘记把它给杰里米,就等一下赏金吧。它使问题和答案都得到了更多的曝光(人们检查赏金列表),你们可能都会获得更多的赞成票 :) 除非有某种原因,你们中的任何一个都希望它快速获得奖励!
  • @JRichardSnape 谢谢你的建议。 Jeremy,让我知道你喜欢什么,我认为等待是个好主意,但如果你愿意,我愿意立即这样做。
猜你喜欢
  • 2017-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多