8

让我解释一下问题:

我通过 Pyramid 为我的静态资产提供服务:

config.add_static_view(name='static', path='/var/www/static')

它工作正常。

现在,我有一个在数据库中创建会话的自定义会话工厂。它检查浏览器是否提供会话 cookie。如果是这样,它会从数据库中找到一个会话。如果没有,则在 DB 中创建一个新会话,并将一个 cookie 返回给浏览器。

到目前为止,一切都很好。

现在,在我的home_view(生成我的主页)内部,我不会以任何方式访问请求变量:

@view_config(route_name='home', renderer="package:templates/home.mak")
def home_view(request):
    return {}

因此,当用户访问主页时,不会在服务器上创建会话。我认为这是因为 Pyramid 懒惰地创建会话-只有当您访问时request.session。因此,主页请求的响应标头不包含任何Set-Cookie会话标头。

现在在主页的 mako 模板中,我正在为 JavaScript 和 CSS 文件生成静态 URL...

<link rel="stylesheet" href="${request.static_url(...)}"

<script src="${request.static_url(...)}"></script>

现在,由于我从 Pyramid 提供静态资产,因此对这些资产的所有请求都通过整个 Pyramid 机器。

因此,当我的浏览器发送获取静态资产的请求时,Pyramid 会如何创建会话。也就是说,当浏览器发送对静态资产的请求时,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 {}

我应该如何防止在静态资产请求上创建会话。更好的是,我希望 Pyramid 在收到静态资产请求时甚至不接触会话工厂——这可能吗?

其次,我不明白为什么 Pyramid 在静态请求上创建一个新会话?

更新

这是会话工厂。

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)

更新

我的自定义身份验证:

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())
4

2 回答 2

2

这是一个重现问题的虚拟项目:

  1. 设置一个 virtualenv 环境并在其中安装 Pyramid。

  2. 安装一个启动项目:pcreate -s starter IssueApp

  3. 删除所有不必要的文件,以便您拥有这个简单的树:

.
├── 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
使用文件监视器启动子进程
在 PID 2339 中启动服务器。
服务于 http://0.0.0.0:7777
2013-03-27 03:40:55,539 DEBUG [issueapp][Dummy-2] 路由匹配 url http://192.168.56.102:7777/;route_name:'home',path_info:'/',模式:'/',matchdict:{},谓词:''
2013-03-27 03:40:55,540 DEBUG [issueapp][Dummy-2] debug_authorization of url http://192.168.56.102:7777/(视图名称''针对上下文):允许(未注册权限)
2013-03-27 03:40:55,685 调试 [issueapp][Dummy-3] 路由匹配 url http://192.168.56.102:7777/static/pyramid.png;route_name:'__static/',path_info:'/static/pyramid.png',模式:'static/*subpath',matchdict:{'subpath':('pyramid.png',)},谓词:''
[认证]:http://192.168.56.102:7777/static/pyramid.png
2013-03-27 03:40:55,687 DEBUG [issueapp][Dummy-3] debug_authorization url http://192.168.56.102:7777/static/pyramid.png(视图名称''针对上下文):ACLDenied 权限'__no_permission_required__ ' via ACE '' 在 ACL [('Allow', 'system.Authenticated', 'edit'), ('Deny', 'system.Authenticated', 'login'), ('Allow', 'system.Everyone' , 'login')] 关于主体 ['system.Everyone', 'system.Authenticated', 'user'] 的上下文


请注意,该[auth]: ...消息仅打印一次——针对静态资产请求,而不是针对主页请求。这很奇怪,因为这意味着针对静态资产而不是针对正常请求咨询身份验证策略。(当然,除非涉及许可,在我看来不是)。

于 2013-03-27T07:44:41.737 回答
2

我无法在一个虚拟项目中重现此行为,这使我相信您的某些配置会影响此处未显示的内容。显然,如果调用任何身份验证,将根据您的身份验证策略创建会话。静态资产(默认情况下)已注册,NO_PERMISSION_REQUIRED这意味着它们不会调用 Pyramid 中的任何身份验证 API(我已经验证是这种情况)。

对静态资产的请求确实会调用整个请求管道,这意味着如果您在任何订阅者中有任何代码,或者您的根工厂调用has_permission或其他安全 API,或者直接接触会话本身,那么这将解释您所看到的行为因为您的会话与您的身份验证相关联。

于 2013-03-27T05:57:03.690 回答