金字塔:会话和静态资产
让我解释一下这个问题:
我通过金字塔服务我的静态资产:
config.add_static_view(name='static', path='/var/www/static')
它工作正常。
现在,我有一个自定义会话工厂,可以在数据库中创建会话。 它检查浏览器是否显示会话cookie。 如果有,它会从数据库中找到一个会话。 如果没有,则在数据库中创建一个新的会话,并将cookie返回给浏览器。
到现在为止还挺好。
现在,在我的home_view
(生成我的主页)内部,我不以任何方式访问请求变量:
@view_config(route_name='home', renderer="package:templates/home.mak")
def home_view(request):
return {}
因此,会发生什么情况是用户访问主页时,会话不会在服务器上创建。 我认为这是因为金字塔懒惰地创建会话 - 只有当你访问request.session
。 因此,主页请求的响应头不包含会话的任何Set-Cookie
头。
现在在主页的mako模板中,我为JavaScript和CSS文件生成静态URL ...
<link rel="stylesheet" href="${request.static_url(...)}"
<script src="${request.static_url(...)}"></script>
现在,由于我服务金字塔的静态资产,所有对这些资产的请求都会通过整个金字塔机器。
所以,发生什么事是当我的浏览器发送请求来获取静态资产时,金字塔如何创建会话。 也就是说,Pyramid正在数据库中创建会话,并在浏览器发送静态资产请求时发回会话cookie。 这是问题#1。
浏览器并行发送所有对静态资产的请求。 我正在使用最新版本的Firefox和Chrome。 由于实际HTML文档的HTTP请求没有返回任何Set-Cookie
标头,因此对静态资产的请求没有任何Cookie标头。 这意味着Pyramid没有看到任何请求的会话cookie,并且它在数据库中创建了一个新的会话,以满足他们为静态资产获得的每一个请求。
如果我在我的主页上获取7个静态资产,并且创建了7个会话条目。 这是因为所有这些请求都与服务器平行并且没有会话cookie,所以Pyramid为每个请求创建一个会话。
如果我故意作为主页请求的一部分访问会话,则不会出现此问题。 它在数据库中创建一个会话并向浏览器发送一个cookie,然后浏览器发送它从服务器请求的每个静态资产(并行)。
@view_config(route_name='home', renderer="package:templates/home.mak")
def home_view(request):
if request.session: pass
return {}
我应该如何防止在静态资产请求上创建会话。 更好的是,我希望金字塔在接收到静态资产请求时甚至不会触碰会话工厂 - 这可能吗?
其次,我不明白为什么金字塔正在静态请求上创建一个新的会话?
UPDATE
这是会话工厂。
def DBSessionFactory(
secret,
cookie_name="sess",
cookie_max_age=None,
cookie_path='/',
cookie_domain=None,
cookie_secure=False,
cookie_httponly=False,
cookie_on_exception=True
):
# this is the collable that will be called on every request
# and will be passed the request
def factory(request):
cookieval = request.cookies.get(cookie_name)
session_id = None
session = None
# try getting a possible session id from the cookie
if cookieval is not None:
try:
session_id = signed_deserialize(cookieval, secret)
except ValueError:
pass
# if we found a session id from the cookie
# we try loading the session
if session_id is not None:
# _load_session will return an object that implements
# the partial dict interface (not complete, just the basics)
session = _load_session(session_id)
# if no session id from cookie or no session found
# for the id in the database, create new
if session_id is None or session is None:
session = _create_session()
def set_cookie(response):
exc = getattr(request, 'exception', None)
if exc is not None and cookie_on_exception == False:
return
cookieval = signed_serialize(session.session_id, secret)
response.set_cookie(
cookie_name,
value=cookieval,
max_age = cookie_max_age,
path = cookie_path,
domain = cookie_domain,
secure = cookie_secure,
httponly = cookie_httponly,
)
def delete_cookie(response):
response.delete_cookie(
cookie_name,
path = cookie_path,
domain = cookie_domain,
)
def callback(request, response):
if session.destroyed:
_purge_session(session)
delete_cookie(response)
return
if session.new:
set_cookie(response)
# this updates any changes to the session
_update_session(session)
# at the end of request
request.add_response_callback(callback)
# return the session from a call to the factory
return session
# return from session factory
return factory
接着,
factory = DBSessionFactory('secret')
config.set_session_factory(factory)
UPDATE
我的自定义验证:
class RootFactory:
__acl__ = [
(Allow, Authenticated, 'edit'),
# only allow non authenticated users to login
(Deny, Authenticated, 'login'),
(Allow, Everyone, 'login'),
]
def __init__(self, request):
self.request = request
class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
def __init__(self, callback=None, debug=False):
self.callback = callback
self.debug = debug
def remember(self, request, principal, **kw):
return []
def forget(self, request):
return []
def unauthenticated_userid(self, request):
if request.session.loggedin:
return request.session.userid
else:
return None
接着,
config.set_root_factory(RootFactory)
config.set_authentication_policy(SessionAuthenticationPolicy())
config.set_authorization_policy(ACLAuthorizationPolicy())
我无法在虚拟项目中重现此行为,这导致我相信您有一些影响此处未显示的配置的配置。 很明显,如果调用任何认证,将根据您的认证策略创建一个会话。 静态资产(默认情况下)以NO_PERMISSION_REQUIRED
进行注册,这意味着它们不会调用金字塔内的任何身份验证API(并且我已经验证了这种情况)。
对静态资产的请求确实会调用整个请求管道,这意味着如果你在任何订户中有任何代码,或者你的根工厂调用了has_permission
或其他安全API,或者直接自己触及会话,那么这将解释你的行为因为您的会话与您的身份验证相关。
这是一个重现问题的虚拟项目:
设置一个virtualenv环境并在其中安装Pyramid。
安装初学者项目: pcreate -s starter IssueApp
删除所有不必要的文件,以便拥有这个简单的树:
树
.
├── CHANGES.txt
├── development.ini
├── issueapp
│ ├── __init__.py
│ └── static
│ └── pyramid.png
├── README.txt
└── setup.py
请注意,我们将在__init__.py
文件中编写整个应用程序 - 所有其他内容都将被删除。
现在安装这个项目: (env) $ python setup.py develop
这会将你的项目安装到虚拟环境中。
development.ini
文件:
[app:main]
use = egg:IssueApp#main
pyramid.reload_all = true
pyramid.reload_templates = true
pyramid.debug_all = true
pyramid.debug_notfound = true
pyramid.debug_routematch = true
pyramid.prevent_http_cache = true
pyramid.default_locale_name = en
[server:main]
use = egg:waitress#main
host = 0.0.0.0
port = 7777
[loggers]
keys = root, issueapp
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_issueapp]
level = INFO
handlers =
qualname = issueapp
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
__init__.py
文件:
from pyramid.config import Configurator
from pyramid.view import view_config
from pyramid.response import Response
from pyramid.authentication import CallbackAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import (
Allow, Deny,
Everyone, Authenticated,
)
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
#config.add_static_view('static', 'static', cache_max_age=3600)
config.add_static_view(name='static', path='issueapp:static')
config.add_route('home', '/')
config.set_root_factory(RootFactory)
config.set_authentication_policy(DummyAuthPolicy())
config.set_authorization_policy(ACLAuthorizationPolicy())
config.scan()
return config.make_wsgi_app()
@view_config(route_name='home')
def home_view(request):
src = request.static_url('issueapp:static/pyramid.png')
return Response('<img src='+ src + '>')
class RootFactory:
__acl__ = [
(Allow, Authenticated, 'edit'),
(Deny, Authenticated, 'login'),
(Allow, Everyone, 'login'),
]
def __init__(self, request):
self.request = request
class DummyAuthPolicy(CallbackAuthenticationPolicy):
def __init__(self, callback=None, debug=False):
self.callback = callback
self.debug = debug
def remember(self, request, principal, **kw):
return []
def forget(self, request):
return []
def unauthenticated_userid(self, request):
# this will print the request url
# so we can know which request is causing auth code to be called
print('[auth]: ' + request.url)
# this means the user is authenticated
return "user"
现在运行该应用程序
pserve development.ini --reload
Starting subprocess with file monitor
Starting server in PID 2303.
serving on http://0.0.0.0:7777
最后,清除浏览器中的所有历史记录(这很重要,或者问题可能不会显示)并访问该页面。 这会打印在控制台上:
[auth]: http://192.168.56.102:7777/static/pyramid.png
这表明认证代码正在被请求静态请求。
现在,当我将日志级别设置为DEBUG
,这是控制台访问页面时的输出:
pserve development.ini --reload Starting subprocess with file monitor Starting server in PID 2339. serving on http://0.0.0.0:7777 2013-03-27 03:40:55,539 DEBUG [issueapp][Dummy-2] route matched for url http://192.168.56.102:7777/; route_name: 'home', path_info: '/', pattern: '/', matchdict: {}, predicates: '' 2013-03-27 03:40:55,540 DEBUG [issueapp][Dummy-2] debug_authorization of url http://192.168.56.102:7777/ (view name '' against context ): Allowed (no permission registered) 2013-03-27 03:40:55,685 DEBUG [issueapp][Dummy-3] route matched for url http://192.168.56.102:7777/static/pyramid.png; route_name: '__static/', path_info: '/static/pyramid.png', pattern: 'static/*subpath', matchdict: {'subpath': ('pyramid.png',)}, predicates: '' [auth]: http://192.168.56.102:7777/static/pyramid.png 2013-03-27 03:40:55,687 DEBUG [issueapp][Dummy-3] debug_authorization of url http://192.168.56.102:7777/static/pyramid.png (view name '' against context ): ACLDenied permission '__no_permission_required__' via ACE '' in ACL [('Allow', 'system.Authenticated', 'edit'), ('Deny', 'system.Authenticated', 'login'), ('Allow', 'system.Everyone', 'login')] on context for principals ['system.Everyone', 'system.Authenticated', 'user']
请注意, [auth]: ...
消息只会打印一次 - 用于静态资产请求,而不是用于主页请求。 这很奇怪,因为这意味着auth策略是针对静态资产查询的,而不是针对正常请求。 (当然,除非涉及到许可,我认为这不是)。