简体   繁体   English

金字塔:会话和静态资产

[英]Pyramid: sessions and static assets

Let me explain the problem: 让我解释一下这个问题:

I am serving my static assets via Pyramid: 我通过金字塔服务我的静态资产:

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

And it works fine. 而且效果很好。

Now, I have a custom session factory that creates sessions in database. 现在,我有一个自定义会话工厂,可以在数据库中创建会话。 It checks if the browser presents a session cookie. 它检查浏览器是否显示会话cookie。 If it does, it finds a session from the DB. 如果是这样,它将从数据库中找到一个会话。 If it does not, then a new session is created in DB and a cookie is returned to the browser. 如果没有,那么将在数据库中创建一个新会话,并将cookie返回到浏览器。

So far so good. 到目前为止,一切都很好。

Now, inside my home_view (that generates my home page), I do not access the request variable in any way: 现在,在我的home_view (生成我的主页)中,我不会以任何方式访问请求变量:

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

Because of this, what happens is when the user visits the home page, the session DOES NOT get created on the server. 因此,发生的情况是当用户访问主页时,不会在服务器上创建会话。 I think this is because Pyramid creates sessions lazily -- only when you access request.session . 我认为这是因为Pyramid 延迟创建了会话-仅当您访问request.session Hence, the response headers for the home page request DO NOT contain any Set-Cookie header for sessions. 因此,主页请求的响应头不包含任何会话的Set-Cookie头。

Now inside my mako template for the home page, I am generating static URLs for JavaScript and CSS files... 现在,在主页的mako模板中,我正在为JavaScript和CSS文件生成静态URL。

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

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

Now, since I am serving the static assets from Pyramid, all the requests for these assets go through the entire Pyramid machinery. 现在,由于我正在为Pyramid提供静态资产,因此对这些资产的所有请求都将贯穿整个Pyramid机械。

So, what happens is when my browser sends requests to fetch static assets, Pyramid some how creates the session. 因此,发生什么情况是当我的浏览器发送请求以获取静态资产时,金字塔如何创建会话。 That is, Pyramid is creating the session in the database and sending session cookie back when browser sends the requests for static assets. 也就是说,Pyramid在数据库中创建会话,并在浏览器发送对静态资产的请求时发回会话cookie。 This is problem #1. 这是问题1。

The browser sends all the requests for static assets in parallel . 浏览器并行发送所有对静态资产的请求。 I am using the recent versions of Firefox and Chrome. 我正在使用最新版本的Firefox和Chrome。 Since the HTTP request for the actual HTML document did not return any Set-Cookie headers, the requests for static assets do NOT have any cookie headers. 由于对实际HTML文档的HTTP请求没有返回任何Set-Cookie标头,因此对静态资产的请求没有任何cookie标头。 What this means is that Pyramid sees no session cookie for any of the requests, and it creates a new session in the database FOR EACH OF THE REQUESTS THAT IT GETS FOR THE STATIC ASSET. 这意味着Pyramid看不到任何请求的会话cookie,并在数据库中为静态资产的每个请求创建一个新会话。

If am fetching 7 static assets on my home page, and 7 session entries get created. 如果要在我的主页上获取7个静态资产,则会创建7个会话条目。 This is because all these requests go in parallel to the server and none has session cookie, so Pyramid creates a session for each. 这是因为所有这些请求都与服务器并行进行,并且没有任何请求具有会话cookie,因此Pyramid为每个请求创建一个会话。

This problem does not arise if I deliberately access the session as part of the home page request. 如果我作为主页请求的一部分故意访问会话,则不会出现此问题。 It creates a session in DB and sends a cookie to the browser which the browser then sends back for each static asset it requests from the server (in parallel). 它在数据库中创建一个会话,然后向浏览器发送一个cookie,然后浏览器针对从服务器请求的每个静态资产(并行)发送回cookie。

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

How should I prevent the creation of sessions on static asset requests. 如何防止在静态资产请求上创建会话。 Better yet, I would like Pyramid to not even touch the session factory when it receives a request for static asset -- is this possible? 更好的是,我希望Pyramid在收到静态资产请求时甚至不接触会话工厂-这可能吗?

Secondly, I don't understand WHY Pyramid is creating a new session on static requests? 其次,我不明白为什么金字塔要根据静态请求创建新会话?

UPDATE 更新

Here is the session factory. 这是会话工厂。

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

And then, 然后,

factory = DBSessionFactory('secret')
config.set_session_factory(factory)

UPDATE 更新

My custom authentication: 我的自定义身份验证:

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

And then, 然后,

config.set_root_factory(RootFactory)

config.set_authentication_policy(SessionAuthenticationPolicy())
config.set_authorization_policy(ACLAuthorizationPolicy())

I'm unable to reproduce this behavior in a dummy project which leads me to believe that you have some configuration affecting things that isn't shown here. 我无法在虚拟项目中重现此行为,这使我相信您的某些配置会影响此处未显示的内容。 Clearly if any authentication is invoked a session will be created, as per your authentication policy. 显然,如果根据身份验证策略调用了任何身份验证,则会创建一个会话。 Static assets are (by default) registered with NO_PERMISSION_REQUIRED which means that they will not invoke any of the authentication APIs within Pyramid (and I've verified that this is the case). 静态资产(默认情况下)已向NO_PERMISSION_REQUIRED注册,这意味着它们将不会调用Pyramid中的任何身份验证API(并且我已经证明是这种情况)。

Requests for static assets do invoke the entire request pipeline, meaning that if you have any code in any subscribers, or your root factory that invoke has_permission or other security APIs, or touch the session directly themselves, then this would explain the behavior you're seeing since your sessions are coupled to your authentication. 对静态资产的请求确实会调用整个请求管道,这意味着,如果您在任何订户中有任何代码,或者您的根工厂调用了has_permission或其他安全API,或者自己直接触摸了会话,那么这将解释您的行为看到,因为您的会话已耦合到您的身份验证。

Here is a dummy project to reproduce the problem: 这是一个重现该问题的虚拟项目:

  1. setup a virtualenv environment and install Pyramid in it. 设置一个virtualenv环境并在其中安装Pyramid。

  2. Install a starter project: pcreate -s starter IssueApp 安装入门项目: pcreate -s starter IssueApp

  3. Delete all the unnecessary files so that you have this simple tree: 删除所有不必要的文件,以便拥有以下简单树:

Tree

.
├── CHANGES.txt
├── development.ini
├── issueapp
│   ├── __init__.py
│   └── static
│       └── pyramid.png
├── README.txt
└── setup.py

Note that we wil write the entire app in the __init__.py file -- so everything else is removed. 请注意,我们会将整个应用程序写入__init__.py文件中-因此将删除所有其他内容。

Now install the project: (env) $ python setup.py develop This will install your project into virtual environment. 现在安装项目: (env) $ python setup.py develop这将把您的项目安装到虚拟环境中。

The development.ini file: 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

The __init__.py file: __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"

Now run the app 现在运行应用

pserve  development.ini  --reload
Starting subprocess with file monitor
Starting server in PID 2303.
serving on http://0.0.0.0:7777

Finally, clear all history from your browser (this is important or the issue might not reveal itself) and access the page. 最后,从浏览器中清除所有历史记录(这很重要,否则问题可能不会解决)并访问该页面。 this gets printed on the console: 这被打印在控制台上:

[auth]: http://192.168.56.102:7777/static/pyramid.png   

Which shows that auth code is getting called for static requests. 这表明身份验证代码正被静态请求调用。

Now, when I set the log level to DEBUG , this is the output of console on accessing the page: 现在,当我将日志级别设置为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']

Note that the [auth]: ... message is getting printed ONLY ONCE -- for the static asset request, and NOT for the home page request. 请注意, [auth]: ...消息仅被打印一次-用于静态资产请求,而不用于主页请求。 This is strange because it means that the auth policy is consulted for static assets but not for normal requests. 这很奇怪,因为这意味着针对静态资产(而非正常请求)咨询了auth策略。 (Unless of course there is a permission involved, which in my view isn't). (当然,除非涉及许可,但我认为没有)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM