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

Mic*_*ter 4 python authentication cookies session 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,
        }
    }
Run Code Online (Sandbox Code Playgroud)

HTTP auth在这一点上很有用.例如,这将为我提供我在内部定义的成功登录消息AuthTest:

curl http://realuser:realpass@localhost/AuthTest/
Run Code Online (Sandbox Code Playgroud)

自会话开始以来,我可以保存cookie并检查CherryPy设置的cookie:

curl --cookie-jar cookie.jar http://realuser:realpass@localhost/AuthTest/
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

curl --cookie 'zknsrv=821aaad0ba34fd51f77b2452c7ae3c182237deb3' http://localhost/AuthTest
Run Code Online (Sandbox Code Playgroud)

我错过了什么?

非常感谢您的帮助.

Mic*_*ter 9

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

我的解决方案,散文

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

我最终能够从CherryPy wiki上的一个页面中获取很多东西,称为简单身份验证和访问限制助手.该代码使用CP会话,但它不使用Basic Auth,而是使用具有提交的登录表单的特殊页面?username=USERNAME&password=PASSWORD.我所做的基本上只是将提供的check_auth功能从使用特殊登录页面改为使用HTTP身份验证头.

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

然后,您需要一种方法来要求对给定页面进行身份验证; 我这样做require(),加上一些条件,只是返回的callables True.就我而言,这些条件是zkn_admin()user_is()功能; 如果你有更复杂的需求,你可能也想看看member_of(),any_of()all_of()从原来的代码.

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

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

关于我的代码的一些注释

  • 警告:因为我为HTTP身份验证头编写了自己的解析器,它只解析我所说的内容,这意味着只需要HTTP Basic Auth - 例如,Digest Auth或其他任何内容.对于我的应用程序,这很好; 对你来说,它可能不是.
  • 它假设我的代码中的其他地方定义了一些函数:user_verify()user_is_admin()
  • 我还使用了一个debugprint()只在设置DEBUG变量时打印输出的函数,为了清楚起见,我将这些调用留在了里面.
  • 你可以打电话给它cherrypy.tools.WHATEVER(见最后一行); 我zkauth根据应用程序的名称调用它.但请注意不要调用它auth,或任何其他内置工具的名称.
  • 然后cherrypy.tools.WHATEVER,您必须在CherryPy配置中启用.
  • 正如您在所有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"
Run Code Online (Sandbox Code Playgroud)

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

config_root = {
    '/' : {
        'tools.zkauth.on': True, 
        'tools.sessions.on': True,
        'tools.sessions.name': 'zknsrv',
        }
    }
Run Code Online (Sandbox Code Playgroud)