所以,简短的答案是你可以这样做,但你必须编写自己的 CherryPy 工具 (a before_handler
), 你呢must not在 CherryPy 配置中启用基本身份验证(也就是说,您不应该执行类似的操作tools.auth.on
or tools.auth.basic...
等) - 您必须自己处理 HTTP 基本身份验证。原因是内置的基本身份验证内容显然非常原始。如果您像我上面那样通过启用基本身份验证来保护某些内容,它将执行身份验证检查在检查会话之前,并且您的 cookie 将不会执行任何操作。
我的解决方案,散文
幸运的是,即使 CherryPy 没有办法内置这两种方法,您仍然可以使用其内置会话代码。您仍然需要编写自己的代码来处理基本身份验证部分,但总的来说,这还不错,并且使用会话代码是一个巨大的胜利,因为编写自定义会话管理器是将安全错误引入到您的 web 应用程序中的好方法。
我最终能够从 CherryPy wiki 上的一个名为 的页面获取很多内容简单的身份验证和访问限制助手 http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions。该代码使用 CP 会话,但它不是使用基本身份验证,而是使用带有登录表单的特殊页面,该页面可以提交?username=USERNAME&password=PASSWORD
。我所做的基本上无非就是改变提供的check_auth
从使用特殊登录页面到使用 HTTP 身份验证标头的功能。
一般来说,您需要一个可以添加为 CherryPy 工具的函数 - 特别是before_handler
。 (在原始代码中,这个函数被称为check_auth()
,但我将其重命名为protect()
.) 该函数首先尝试查看 cookie 是否包含(有效)会话 ID,如果失败,它会尝试查看标头中是否存在 HTTP 身份验证信息。
然后,您需要一种方法来要求对给定页面进行身份验证;我这样做与require()
,加上一些状况,这只是返回的可调用对象True
。就我而言,这些条件是zkn_admin()
, and user_is()
功能;如果您有更复杂的需求,您可能还想看看member_of()
, any_of()
, and all_of()
从原始代码。
如果您这样做,您已经有了一种登录方法 - 您只需将有效的会话 cookie 或 HTTPBA 凭据提交到您使用@require()
装饰师。您现在需要的只是一种注销方式。
(原始代码有一个AuthController
类包含login()
and logout()
,你可以使用整个AuthController
只需将对象放入 HTTP 文档树中即可auth = AuthController()
在您的 CherryPy 根类中,并通过 URL 例如访问它http://example.com/auth/login http://example.com/auth/login and http://example.com/auth/logout http://example.com/auth/logout。我的代码不使用 authcontroller 对象,仅使用一些函数。)
关于我的代码的一些注释
- 警告:因为我为 HTTP 身份验证标头编写了自己的解析器,所以它只解析我告诉它的内容,这意味着只是 HTTP 基本身份验证 - 而不是摘要身份验证或其他任何内容。对于我的应用程序来说,这很好;对你来说,可能不是。
- 它假设我的代码中其他地方定义了一些函数:
user_verify()
and 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',
}
}