I recently got a chance to play around with Tornado, which is pretty neat (although that certainly isn’t news). One thing that I tried to do pretty quickly and had a hard time with was Basic authentication (you know, the little “so-and-so requires a username and password” pop-up). Paulo Suzart posted a working example over on gist, but it was a bit light on context, and Dhanan Jaynene’s request interceptors are a bit overkill for this purpose (although very useful for more complex behavior!). Let’s start with the “hello world” example from theTornado site, and I’ll show you what I’ve cooked up. I’m only an hour or two into exploring Tornado, like I hinted at above, so if you see any room for improvement, I’d love to hear from you. (I’ve made all the code I’ve written very verbose in the hopes that’ll help people understand and customize it without much trial-and-error. Feel free to tighten things up.)
代码语言:javascript复制import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
application = tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
application.listen(8888) tornado.ioloop.IOLoop.instance().start()
Now, what I really wanted was to be able to add a decorator to any RequestHandler that (without any other modifications) would perform all the back-and-forth with the browser, so that I could write something like this:
代码语言:javascript复制@require_basic_auth
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
If you’re not familiar with Python decorators, in the simplest usage, this will effectively lead to the function require_basic_auth being invoked with MainHandler (the class) as its argument. Whatever that function returns will replace the definition of MainHandler. Nifty, huh? Let’s see how we can use this. We could mess with the get(), put(), etc. methods on RequestHandler, but it turns out that there’s a method that Tornadoinvokes above them called _execute(). If we replace that one, we’ll be able to do all of this in one fell swoop.
代码语言:javascript复制def require_basic_auth(handler_class):
# Should return the new _execute function, one which enforces
# authentication and only calls the inner handler's _execute() if
# it's present.
def wrap_execute(handler_execute):
# I've pulled this out just for clarity, but you could stick
# it in _execute if you wanted. It returns True iff
# credentials were provided. (The end of this function might
# be a good place to see if you like their username and
# password.)
def require_basic_auth(handler, kwargs):
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic '):
# If the browser didn't send us authorization headers,
# send back a response letting it know that we'd like
# a username and password (the "Basic" authentication
# method). Without this, even if you visit put a
# username and password in the URL, the browser won't
# send it. The "realm" option in the header is the
# name that appears in the dialog that pops up in your
# browser.
handler.set_status(401)
handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')
handler._transforms = []
handler.finish()
return False
# The information that the browser sends us is
# base64-encoded, and in the format "username:password".
# Keep in mind that either username or password could
# still be unset, and that you should check to make sure
# they reflect valid credentials!
auth_decoded = base64.decodestring(auth_header[6:])
username, password = auth_decoded.split(':', 2)
kwargs['basicauth_user'], kwargs['basicauth_pass'] = username, password
return True
# Since we're going to attach this to a RequestHandler class,
# the first argument will wind up being a reference to an
# instance of that class.
def _execute(self, transforms, *args, **kwargs):
if not require_basic_auth(self, kwargs):
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
It’s pretty simple, isn’t it? There is one subtlety to keep track of, and it has to do with kwargs (the keyword argument dictionary). If you’ll notice, require_basic_auth uses it without the stars. This lets us pass a reference to the dictionary object, and not send the key/value pairs in the dictionary to require_basic_auth as arguments. Then, when we modify the dictionary, the username and password get passed into handler_execute() as arguments. Naturally, we have to change our RequestHandler to take them:
代码语言:javascript复制@require_basic_auth
class MainHandler(tornado.web.RequestHandler):
def get(self, basicauth_user, basicauth_pass):
self.write('Hi there, {0}! Your password is {1}.'
.format(basicauth_user, basicauth_pass))
def post(self, **kwargs):
basicauth_user = kwargs['basicauth_user']
basicauth_pass = kwargs['basicauth_pass']
self.write('Hi there, {0}! Your password is {1}.'
.format(basicauth_user, basicauth_pass))
Either method works—you can take them as normal arguments, or as keyword arguments again. P.S.: If we tighten up the decorator, we wind up with this:
代码语言:javascript复制def require_basic_auth(handler_class):
def wrap_execute(handler_execute):
def require_basic_auth(handler, kwargs):
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic '):
handler.set_status(401)
handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')
handler._transforms = []
handler.finish()
return False
auth_decoded = base64.decodestring(auth_header[6:])
kwargs['basicauth_user'], kwargs['basicauth_pass'] = auth_decoded.split(':', 2)
return True
def _execute(self, transforms, *args, **kwargs):
if not require_basic_auth(self, kwargs):
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
参考: http://kelleyk.com/post/7362319243/easy-basic-http-authentication-with-tornado