你如何在CherryPy中使用cookie和HTTP基本身份验证?

时间:2012-09-26 05:59:24

标签: python session authentication cookies cherrypy

我有一个需要身份验证的CherryPy Web应用程序。我有一个HTTP基本身份验证工作,其配置如下所示:

app_config = {
    '/' : {
        'tools.sessions.on': True,
        'tools.sessions.name': 'zknsrv',
        'tools.auth_basic.on': True,
        'tools.auth_basic.realm': 'zknsrv',
        'tools.auth_basic.checkpassword': checkpassword,
        }
    }

HTTP身份验证在这一点上很有效。例如,这将为我提供我在AuthTest中定义的成功登录消息:

curl http://realuser:realpass@localhost/AuthTest/

由于会话开启,我可以保存cookie并检查CherryPy设置的那个:

curl --cookie-jar cookie.jar http://realuser:realpass@localhost/AuthTest/

cookie.jar文件最终会如下所示:

# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /       FALSE   1348640978      zknsrv  821aaad0ba34fd51f77b2452c7ae3c182237deb3

但是,如果我在没有用户名和密码的情况下提供此会话ID,我将收到HTTP 401 Not Authorized失败,如下所示:

curl --cookie 'zknsrv=821aaad0ba34fd51f77b2452c7ae3c182237deb3' http://localhost/AuthTest

我错过了什么?

非常感谢您的帮助。

1 个答案:

答案 0 :(得分:9)

所以,简短的回答是你可以做到这一点,但你必须编写自己的CherryPy工具before_handler),你一定不能在CherryPy配置中启用基本身份验证(也就是说,您不应该执行tools.auth.ontools.auth.basic...之类的任何操作) - 您必须自己处理HTTP基本身份验证。原因是内置的基本身份验证功能显然非常原始。如果您通过启用基本身份验证来保护某些内容,就像我上面那样,它会在检查会话之前执行身份验证检查,并且您的cookie将不执行任何操作。

我的解决方案,散文

幸运的是,即使CherryPy无法同时执行这两种操作,您仍然可以使用其内置的会话代码。您仍然需要编写自己的代码来处理基本身份验证部分,但总的来说这并不是很糟糕,使用会话代码是一个很大的胜利,因为编写自定义会话管理器是将安全漏洞引入Web应用程序的好方法。

我最终能够从名为Simple authentication and access restrictions helpers的CherryPy wiki上的页面中获取大量内容。该代码使用CP会话,但它不使用基本身份验证,而是使用具有提交?username=USERNAME&password=PASSWORD的登录表单的特殊页面。我所做的基本上只是将提供的check_auth函数从使用特殊登录页面更改为使用HTTP身份验证头。

通常,您需要一个可以添加为CherryPy工具的功能 - 特别是before_handler。 (在原始代码中,此函数名为check_auth(),但我将其重命名为protect()。)此函数首先尝试查看cookie是否包含(有效)会话ID,如果失败,它会尝试查看标头中是否有HTTP身份验证信息。

然后,您需要一种方法来要求对给定页面进行身份验证;我使用require()以及一些条件执行此操作,这些只是返回True的callables。就我而言,这些条件是zkn_admin()user_is()函数;如果您有更复杂的需求,您可能还需要查看原始代码中的member_of()any_of()all_of()

如果您这样做,您已经有办法登录 - 您只需向使用@require()装饰器保护的任何URL提交有效的会话cookie或HTTPBA凭据。您现在需要的只是一种注销方式。

(原始代码包含AuthController类,其中包含login()logout(),您可以通过放置HTTP文档树中的整个AuthController对象在您的CherryPy根类中auth = AuthController(),并使用例如http://example.com/auth/loginhttp://example.com/auth/logout的URL来访问它。我的代码不使用authcontroller对象,只是一些函数。)

关于我的代码的一些注释

  • 警告:因为我为HTTP身份验证头编写了自己的解析器,它只解析我所说的内容,这意味着只需要HTTP Basic Auth - 例如,Digest Auth或其他任何内容。对于我的应用程序,这很好;对你来说,它可能不是。
  • 它假设我的代码中的其他位置定义了一些函数:user_verify()user_is_admin()
  • 我还使用debugprint()函数,该函数仅在设置DEBUG变量时打印输出,为了清楚起见,我已将这些调用留下。
  • 你可以称之为cherrypy.tools.WHATEVER(见最后一行);我根据应用名称调用了zkauth。但请注意不要将其称为auth,或称为任何其他内置工具的名称。
  • 然后,您必须在CherryPy配置中启用cherrypy.tools.WHATEVER
  • 正如您在所有TODO:消息中看到的那样,此代码仍然处于不稳定状态,并且没有针对边缘情况进行100%测试 - 抱歉!尽管如此,我仍然会给你足够的想法,但我希望如此。

我的解决方案,代码

import base64
import re
import cherrypy 

SESSION_KEY = '_zkn_username'

def protect(*args, **kwargs):
    debugprint("Inside protect()...")

    authenticated = False
    conditions = cherrypy.request.config.get('auth.require', None)
    debugprint("conditions: {}".format(conditions))
    if conditions is not None:
        # A condition is just a callable that returns true or false
        try:
            # TODO: I'm not sure if this is actually checking for a valid session?
            # or if just any data here would work? 
            this_session = cherrypy.session[SESSION_KEY]

            # check if there is an active session
            # sessions are turned on so we just have to know if there is
            # something inside of cherrypy.session[SESSION_KEY]:
            cherrypy.session.regenerate()
            # I can't actually tell if I need to do this myself or what
            email = cherrypy.request.login = cherrypy.session[SESSION_KEY]
            authenticated = True
            debugprint("Authenticated with session: {}, for user: {}".format(
                    this_session, email))

        except KeyError:
            # If the session isn't set, it either wasn't present or wasn't valid. 
            # Now check if the request includes HTTPBA?
            # FFR The auth header looks like: "AUTHORIZATION: Basic <base64shit>"
            # TODO: cherrypy has got to handle this for me, right? 

            authheader = cherrypy.request.headers.get('AUTHORIZATION')
            debugprint("Authheader: {}".format(authheader))
            if authheader:
                #b64data = re.sub("Basic ", "", cherrypy.request.headers.get('AUTHORIZATION'))
                # TODO: what happens if you get an auth header that doesn't use basic auth?
                b64data = re.sub("Basic ", "", authheader)

                decodeddata = base64.b64decode(b64data.encode("ASCII"))
                # TODO: test how this handles ':' characters in username/passphrase.
                email,passphrase = decodeddata.decode().split(":", 1)

                if user_verify(email, passphrase):

                    cherrypy.session.regenerate()

                    # This line of code is discussed in doc/sessions-and-auth.markdown
                    cherrypy.session[SESSION_KEY] = cherrypy.request.login = email
                    authenticated = True
                else:
                    debugprint ("Attempted to log in with HTTBA username {} but failed.".format(
                            email))
            else:
                debugprint ("Auth header was not present.")

        except:
            debugprint ("Client has no valid session and did not provide HTTPBA credentials.")
            debugprint ("TODO: ensure that if I have a failure inside the 'except KeyError'"
                        + " section above, it doesn't get to this section... I'd want to"
                        + " show a different error message if that happened.")

        if authenticated:
            for condition in conditions:
                if not condition():
                    debugprint ("Authentication succeeded but authorization failed.")
                    raise cherrypy.HTTPError("403 Forbidden")
        else:
            raise cherrypy.HTTPError("401 Unauthorized")

cherrypy.tools.zkauth = cherrypy.Tool('before_handler', protect)

def require(*conditions):
    """A decorator that appends conditions to the auth.require config
    variable."""
    def decorate(f):
        if not hasattr(f, '_cp_config'):
            f._cp_config = dict()
        if 'auth.require' not in f._cp_config:
            f._cp_config['auth.require'] = []
        f._cp_config['auth.require'].extend(conditions)
        return f
    return decorate

#### CONDITIONS
#
# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current user as cherrypy.request.login

# TODO: test this function with cookies, I want to make sure that cherrypy.request.login is 
#       set properly so that this function can use it. 
def zkn_admin():
    return lambda: user_is_admin(cherrypy.request.login)

def user_is(reqd_email):
    return lambda: reqd_email == cherrypy.request.login

#### END CONDITIONS

def logout():
    email = cherrypy.session.get(SESSION_KEY, None)
    cherrypy.session[SESSION_KEY] = cherrypy.request.login = None
    return "Logout successful"

现在,您只需在CherryPy配置中启用内置会话和您自己的cherrypy.tools.WHATEVER。同样,请注意不要启用cherrypy.tools.auth。我的配置最终看起来像这样:

config_root = {
    '/' : {
        'tools.zkauth.on': True, 
        'tools.sessions.on': True,
        'tools.sessions.name': 'zknsrv',
        }
    }