跳转到主要内容

ZC WSGI sessions

项目描述

这是使用 zope.session 作为底层机制的 WSGI 中间件实现持久会话。

使用方法

  1. zc.wsgisessions 添加到您应用程序(例如,myapp)的 setup.py 文件中的 install_requires 列表

  2. 将以下内容添加到 myapp.ini

    [filter:sessions]
    use = egg:zc.wsgisessions

    您可以将以下内容添加到配置中

    secure = true

    http-only = off

    有效词汇包括:truefalseonoffyesno、1 和 0。

    其他选项包括

    domain = .example.com
    
    max-age = 10000
    
    path = /foo

    您还可以指定用于会话存储的数据库名称

    db-name = appdb
  3. 在数据库中间件之后但在应用程序之前将 sessions 添加到管道中。

  4. 将此功能添加到数据库中间件的初始化器列表中

    zc.wsgisessions.sessions.initialize_database(database)

    您还可以传递以下关键字参数:db_namenamespacesecrettimeoutresolution

  5. 将此功能添加到 bobo.configure 列表中(您 WSGI 应用的初始化器)

    zope.component.provideAdapter(zc.wsgisessions.sessions.get_session)
  6. 您可以在您的身份验证代码中使用一些辅助工具

    PKG_KEY = __name__  # e.g., myapp.auth
    
    def get_user(request):
        return zc.wsgisessions.sessions.get(request, PKG_KEY, 'user')
    
    def save_user(request, user):
        zc.wsgisession.sessions.store(request, PKG_KEY, 'user', user)
    
    def forget_user(request):
        return zc.wsgisessions.sessions.remove(request, PKG_KEY, 'user')
  7. 当运行 Selenium 测试时,不能使用 HttpOnly cookie。请将测试应用程序的 global_conf 字典中的选项设置为 'http-only': False

详细文档

会话

会话支持有两个方面:浏览器识别和会话存储。浏览器使用 cookie 进行识别;如果请求中没有设置 cookie,则响应会为未来的请求设置它。

会话数据使用由 zope.session 包定义的持久会话数据容器进行存储。如果不存在,则在启动时将实例添加到数据库中。我们可以通过传递关键字参数到数据库初始化器来控制某些参数。这个测试的一次运行使用默认设置,而第二次运行设置自定义参数。

>>> import re
>>> import zc.wsgisessions.testing
>>> import zc.wsgisessions.sessions
>>> db_name = 'sessions'
>>> if zc.wsgisessions.testing.TEST_DB_INIT:
...     db_name = 'test'
...     db = conn.get_connection(db_name).db()
...     zc.wsgisessions.sessions.initialize_database(
...         db,
...         db_name=db_name,
...         namespace='browserid_c0defeed',
...         secret='0.10612221415937506119',
...         timeout=(15 * 60),  # 15 minutes
...         resolution=60,      #  1 minute
...         )
>>> dbroot = conn.get_connection(db_name).root()
>>> dbroot['sessions']
<zope.session.session.PersistentSessionDataContainer object at 0xc0defeed>
>>> if zc.wsgisessions.testing.TEST_DB_INIT:
...     expected_id = re.compile('browserid_c0defeed')
...     expected_secret = re.compile('0.10612221415937506119')
...     expected_timeout = 15 * 60
...     expected_resolution = 60
... else:
...     expected_id = re.compile('browserid_[0-9a-f]{8}')
...     expected_secret = re.compile('[0-9a-f]{20}')
...     expected_timeout = 24 * 60 * 60
...     expected_resolution = 60 * 60
>>> re.match(expected_id, dbroot['browserid_info'][0]) is not None
True
>>> re.match(expected_secret, dbroot['browserid_info'][1]) is not None
True
>>> dbroot['sessions'].timeout == expected_timeout
True
>>> dbroot['sessions'].resolution == expected_resolution
True

如果配置中将 secure 设置为 true 或请求是 https,则在 Set-Cookie 响应中添加 secure。此外,除非配置将 http-only 设置为 false,否则将添加 HttpOnly

>>> global_conf = {}
>>> filter_conf = {'db-name': db_name}
>>> filter = zc.wsgisessions.sessions.BrowserIdFilter(
...     global_conf, **filter_conf)(object())
>>> environ = {
...     'zodb.connection': conn.get_connection('test'),
...     'wsgi.url_scheme': 'https'
... }
>>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1])
>>> cookie_parts = h['Set-Cookie'].split('; ')
>>> 'secure' in cookie_parts
True
>>> 'HttpOnly' in cookie_parts
True

当在过滤器配置(在 .ini 文件中)中更改设置时,默认值将被替换。

>>> filter_conf.update({'http-only': 'false', 'secure': 'true',
...                     'domain': '.example.com', 'max-age': '5000',
...                     'path': '/foo'})
>>> filter = zc.wsgisessions.sessions.BrowserIdFilter(
...     global_conf, **filter_conf)(object())
>>> environ['wsgi.url_scheme'] = 'http'
>>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1])
>>> cookie_parts = h['Set-Cookie'].split('; ')
>>> 'secure' in cookie_parts
True
>>> 'HttpOnly' in cookie_parts
False
>>> 'Domain=.example.com' in cookie_parts
True
>>> 'Max-Age=5000' in cookie_parts
True
>>> 'Path=/foo' in cookie_parts
True

注意,上面的 URL 方案不是 https,但由于过滤器配置中请求了它,因此已设置安全。

对于 Selenium 测试,我们需要重置 HttpOnly,因为我们正在使用开发中的 http URL 方案,因此 secure 的默认值(关闭)是可以接受的。注意,这次我们在全局配置中设置 http-only 以覆盖 .ini 文件中的设置值。

>>> global_conf = {'http-only': 'off'}
>>> filter_conf = {'db-name': db_name, 'http-only': 'on'}
>>> filter = zc.wsgisessions.sessions.BrowserIdFilter(
...     global_conf, **filter_conf)(object())
>>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1])
>>> cookie_parts = h['Set-Cookie'].split('; ')
>>> 'secure' in cookie_parts
False
>>> 'HttpOnly' in cookie_parts
False

会话存储的数据库名称在 initialize_database 中默认设置为 sessions 或提供的 db_name(对于这些测试的第二次运行为 test)。如果我们尝试从其配置(在 .ini 文件中)将错误的数据库名称传递给过滤器,我们将得到一个错误。

>>> if zc.wsgisessions.testing.TEST_DB_INIT:
...     filter_conf['db-name'] = 'sessions'
...     filter = zc.wsgisessions.sessions.BrowserIdFilter(
...         global_conf, **filter_conf)(object())
... else:
...     filter_conf['db-name'] = 'test'
...     filter = zc.wsgisessions.sessions.BrowserIdFilter(
...         global_conf, **filter_conf)(object())
>>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1])
Traceback (most recent call last):
  ...
KeyError: 'browserid_info'

浏览器识别

支持 cookie 所需的信息也存储在数据库中

>>> dbroot['browserid_info']
('browserid_...', '...')
>>> cookie_name = dbroot['browserid_info'][0]
>>> import webtest
>>> app = webtest.TestApp(app)
>>> response = app.get('https://127.0.0.1/')
>>> cookie_value = app.cookies[cookie_name]
>>> len(cookie_value)
54

如果我们更改数据库中的密钥,我们可以导致会话标识符被重置

>>> import random
>>> import transaction
>>> secret = '%.20f' % random.random()
>>> dbroot['browserid_info'] = cookie_name, secret
>>> transaction.commit()
>>> response = app.get('https://127.0.0.1/')
>>> cookie_value == app.cookies[cookie_name]
False
>>> cookie_value = app.cookies[cookie_name]
>>> app.cookies[cookie_name] = 'bad'
>>> response = app.get('https://127.0.0.1/')
>>> cookie_value == app.cookies[cookie_name]
False

会话存储

一旦从请求中加载了 cookie,或安排在响应中发送,就会在请求上存储一个 ISession 对象。让我们直接创建一个,以便我们可以看到它是如何工作的

>>> sdc = dbroot['sessions']
>>> session = zc.wsgisessions.sessions.Session(cookie_value, sdc)
>>> pkgdata = session['myapp.auth']
>>> pkgdata['mydata'] = 42
>>> sdc[cookie_value]['myapp.auth']['mydata']
42
>>> list(session)
Traceback (most recent call last):
...
NotImplementedError

辅助工具

>>> import webob
>>> import zc.dbconnection
>>> import zope.session.interfaces
>>> zc.dbconnection.set_local(conn)
>>> environ = {'zc.wsgisessions.session': session}
>>> request = webob.Request(environ=environ)
get(request, pkg_id, key=None)

从会话中检索值;如果没有指定键,则检索 SessionPkgData 容器。

>>> pkgdata = zc.wsgisessions.sessions.get(request, 'myapp.auth')
>>> zope.session.interfaces.ISessionPkgData.providedBy(pkgdata)
True
>>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'blah') is None
True
>>> pkgdata['blah'] = '!!!'
>>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'blah')
'!!!'
>>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'mydata')
42

当指定包标识符和键名称时,如果该会话数据对象不存在,则不会创建它。

>>> zc.wsgisessions.sessions.get(request, "dontcreateme", "blah") is None
True
>>> adapter = zope.session.interfaces.ISession(request)
>>> adapter.get("dontcreateme") is None
True
store(request, pkg_id, key, value)

将键/值对存储在会话中。

>>> obj = object()
>>> zc.wsgisessions.sessions.store(
...     request, 'myapp.auth', 'someobject', obj)
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.auth', 'someobject') is obj
True
>>> obj = object()
>>> zc.wsgisessions.sessions.store(
...     request, 'myapp.data', 'someobject', obj)
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.auth', 'someobject') is obj
False
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.data', 'someobject') is obj
True
remove(request, pkg_id, key)

通过键从会话中删除值。如果没有指定 pkg_id,则使用 zc.wsgisessions.sessions.KEY 的默认 pkg_id。

>>> _obj = zc.wsgisessions.sessions.remove(
...     request, 'myapp.auth', 'someobject')
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.auth', 'someobject') is None
True
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.data', 'someobject') is obj
True
>>> zc.wsgisessions.sessions.remove(
...     request, 'myapp.data', 'someobject') is obj
True
>>> zc.wsgisessions.sessions.get(
...     request, 'myapp.data', 'someobject') is None
True

如果不存在,则不会创建底层的会话数据映射。

>>> zc.wsgisessions.sessions.remove(
...     request, "dontcreateme", "somekey") is None
True
>>> adapter.get("dontcreateme") is None
True

更改

0.6.1 (2013-10-08)

  • 在发布中包含 CHANGES.txt。

0.6.0 (2013-10-08)

  • 添加 domainmax-agepath 配置选项。

0.5.1 (2013-06-12)

开源发布。

0.5 (2013-03-12)

  • 使用密码学安全的随机数源(os.urandom)生成浏览器 ID。

  • 修复了导致不必要地创建 SessionData 对象的 get/remove 辅助工具中的错误。

0.4 (2012-01-03)

  • 接受会话存储的数据库名称参数。

0.3 (2011-11-11)

  • 将辅助函数的参数放置在更合理的顺序中。

  • 要求 pkg_id 以防止不良使用模式。

0.2 (2011-11-10)

  • 使 http-only 和 secure 可配置。

  • 测试配置选项。

  • 测试数据库初始化和选项。

0.1 (2011-11-10)

首次发布

项目详情


支持