所以,简短的回答是你可以这样做,但你必须编写自己的 CherryPy 工具(a before_handler
),并且你不能在 CherryPy 配置中启用基本身份验证(也就是说,你不应该做任何类似的事情tools.auth.on
)tools.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 根类中来使用AuthController
HTTP 文档树中的整个对象,并使用例如http://example.com/的 URL 访问它auth/login和http://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',
}
}