4

我有一个需要身份验证的 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 auth 在这一点上工作得很好。例如,这将为我提供我在内部定义的成功登录消息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

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

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

我错过了什么?

非常感谢您的帮助。

4

1 回答 1

9

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

我的解决方案,散文

幸运的是,即使 CherryPy 没有内置的方法,您仍然可以使用其内置的会话代码。您仍然必须编写自己的代码来处理基本身份验证部分,但总的来说这还不错,并且使用会话代码是一个很大的胜利,因为编写自定义会话管理器是将安全错误引入您的 web 应用程序的好方法.

我最终能够从 CherryPy wiki 上的一个名为Simple authentication and access limits helpers的页面中获取很多东西。该代码使用 CP 会话,但它不是使用基本身份验证,而是使用带有登录表单的特殊页面,该页面提交?username=USERNAME&password=PASSWORD. 我所做的基本上只是将提供的check_auth功能从使用特殊登录页面更改为使用 HTTP auth 标头。

通常,您需要一个可以作为 CherryPy 工具添加的函数 - 特别是before_handler. (在原始代码中,这个函数被调用check_auth(),但我将它重命名为protect()。)这个函数首先尝试查看 cookie 是否包含(有效的)会话 ID,如果失败,它会尝试查看是否有 HTTP auth 信息在标题中。

然后,您需要一种方法来要求对给定页面进行身份验证;我这样做了require(),加上一些条件,它们只是返回的可调用对象True。就我而言,这些条件是zkn_admin(), 和user_is()函数;如果您有更复杂的需求,您可能还想查看原始代码中的member_of()any_of()all_of()

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

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

关于我的代码的一些注释

  • 警告:因为我为 HTTP auth 标头编写了自己的解析器,所以它只解析我告诉它的内容,这意味着只是 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"

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

config_root = {
    '/' : {
        'tools.zkauth.on': True, 
        'tools.sessions.on': True,
        'tools.sessions.name': 'zknsrv',
        }
    }
于 2012-12-19T19:19:59.277 回答