本文翻译自《Basic Authentication》,翻译得不好还请见凉。
引言
这篇教程意在阐明什么是基本认证(base authentication)以及如何用python对其进行处理。你可以从Voidspace Python Recipebook下载本文中的示例代码。
第一个示例,So Let's Do It,展示了如何手动处理基本认证,以此解释其工作原理。
第二个示例,Doing it Properly,则采用一种更合适更自动化的方法来处理。
示例中利用了Python标准库中的urllib2模块,它提供了一个简单的接口用于从互联网请求页面。其中的urlopen方法以openers和handlers的形式提供了更多接口来应对更为复杂的情况,这些玩意儿甚至让中级程序员都感觉困惑。如何使用urllib2?那么就从我这篇教程开始吧!
基本认证
认证是在一个用户访问web页面之前要求用户提供用户名和密码并由服务器进行处理的系统。它允许通过认证的形式将一整套页面保护起来(称为域)。
该方案由HTTP规范制定,因此虽然python支持该方式的认证但并未提供详细的文档。而HTTP文档又是遵循RFCs规范的技术文档,从可读性上可就不那么人性化了。
两种常规的认证方案包括基本认证和数字摘要身份验证。而基本认证无疑是两者中最为常见的,当然也是更为简单的一种。
基本认证过程可以概括如下:
· 用户向服务器发送页面请求
· 服务器向用户返回错误,要求身份认证
· 用户将编码后的认证信息放在请求中并再次向服务器请求
· 服务器验证认证信息并返回所请求的页面或其他的错误
以下是更为详细的说明:
发起请求
客户端是可以想互联网发起请求的任意程序,可以是一个浏览器,也或者是python程序。当用户请求打开一个web页面,它会向对应的服务器发送一个请求。该请求由包含相关请求信息的头部组成,也就是所谓的HTTP请求头。
接收响应
当请求到达服务器后,服务器会相应的作出回应,并且同样会在响应中包含头部,无论请求是否失败。这称之为HTTP响应头。
如果该过程中出现任何问题,服务器都会在响应中包含一个错误代码。你也许对它已经很熟悉了,如404 - 页面不存在;500 - 服务器错误,等等。在urllib2中,这些错误会引发相关异常并包含一个名为code的属性,其值为与HTTP错误代码一致的整数。
401错误和认证域
如果一个页面要求认证,将会向客户端返回401错误,并且在响应头中会包含一个WWW-authenticate头。它会告知服务器采用何种认证方式和对应的认证域。很少说一个网站仅有那么一个页面需要认证,而是包含多个页面的一个部分,即网站中的一个“域”。认证域的名称同样包含在响应头中的这一行。
WWW-Authenticate头大致是这样的:WWW-Authenticate: SCHEME realm="REALM"。
例如,当你尝试访问cPanel(一款著名的主机控制面板应用)时,浏览器将会收到这样的响应头:WWW-Authenticate: Basic realm="cPanel"。
如果客户端已经知道该认证域所需的用户名和密码,那么就可以将其编码后放进请求头并再次发起请求。如果认证成功,请求将会正常返回。反之,如果客户端不知道,则需要向用户询问。这也就意味着在此情况下,客户端对于每个页面都必须请求两次。第一次请求会收到401错误,然后在用户提供所需的认证信息后再次请求。
由于HTTP协议是一种无状态协议,也就意味着使用基本认证的服务器不会记住用户的登录状态,因此对于每一次针对私有域的请求都必须携带正确的请求头。
第一个示例
假设我们要访问一个要求登录并采用基本认证的网页:
theurl = 'http://www.someserver.com/somepath/someprotectedpage.html' req = urllib2.Request(theurl) try: handle = urllib2.urlopen(req) except IOError, e: if hasattr(e, 'code'): if e.code != 401: print 'We got another error' print e.code else: print e.headers print e.headers['www-authenticate']
如果异常具有code属性,那它一定也包含一个headers属性,这是一个字典形式的对象,所有的头部信息都会以键值对保存在其中。不过你仍然可以一次将所有的响应头打印出来。如上面代码的最后一行,当返回401错误时,可以单独查看“www-authenticate”一行的内容。
下面展示了返回结果:
WWW-Authenticate: Basic realm="cPanel" Connection: close Set-Cookie: cprelogin=no; path=/ Server: cpsrvd/9.4.2 Content-type: text/html Basic realm="cPanel"
从www-authenticate头中可以看到服务器采用的认证方式和认证域。只要你在收到401错误时知道该认证域所需的用户名和密码,你就可以将认证凭据编码后放进头部再次请求,这样就可以畅通无阻了。
用户名和密码
假设你需要访问的页面都在同一认证域下并从用户那里得到了正确的用户名和密码,这时你可以从头部中将对应的认证域标识提取出来。这样如果以后在该认证域下的请求遇到401错误,即可使用该认证信息。这样就只剩下一个细节 —— 如何将用户的用户名和密码编码进请求头。其实简单,就是用base64编码。编码后的认证信息看起来不那么明文了,但实际上这着实是非常简单的编码,就是真的所谓“基本”。只要有人嗅探到你的数据包即可从中将你的用户名和密码解码出来。像yahoo、ebay等众多网站都是利用javascript在前端做加密或其他一些手段。这相对来说更难被探测到,并且是从python中借鉴而来的!你也许需要使用客户端代理来研究浏览器到底向服务器发了什么内容。
base64
在《Python Cookbook》中介绍了一个很简单的base64加密技巧展示了如何编码认证凭据并放到请求头中:
import base64 base64string = base64.encodestring('%s:%s' % (username, password))[:-1] req.add_header("Authorization", "Basic %s" % base64string)
So Let's Do It
最后,让我们以一个例子结束这部分内容。我们从例子中展示如何利用正则匹配从响应头中提取认证方式和认证域然后进行认证:
import urllib2 import sys import re import base64 from urlparse import urlparse theurl = 'http://www.someserver.com/somepath/somepage.html' # if you want to run this example you'll need to supply # a protected page with your username and password username = 'johnny' password = 'XXXXXX' # a very bad password req = urllib2.Request(theurl) try: handle = urllib2.urlopen(req) except IOError, e: # here we *want* to fail pass else: # If we don't fail then the page isn't protected print "This page isn't protected by authentication." sys.exit(1) if not hasattr(e, 'code') or e.code != 401: # we got an error - but not a 401 error print "This page isn't protected by authentication." print 'But we failed for another reason.' sys.exit(1) authline = e.headers['www-authenticate'] # this gets the www-authenticate line from the headers # which has the authentication scheme and realm in it authobj = re.compile( r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]''', re.IGNORECASE) # this regular expression is used to extract scheme and realm matchobj = authobj.match(authline) if not matchobj: # if the authline isn't matched by the regular expression # then something is wrong print 'The authentication header is badly formed.' print authline sys.exit(1) scheme = matchobj.group(1) realm = matchobj.group(2) # here we've extracted the scheme # and the realm from the header if scheme.lower() != 'basic': print 'This example only works with BASIC authentication.' sys.exit(1) base64string = base64.encodestring( '%s:%s' % (username, password))[:-1] authheader = "Basic %s" % base64string req.add_header("Authorization", authheader) try: handle = urllib2.urlopen(req) except IOError, e: # here we shouldn't fail if the username/password is right print "It looks like the username or password is wrong." sys.exit(1) thepage = handle.read()
可以看出,程序最后将获取到的页面数据放在变量thepage中。来看看例子中用于匹配认证头的正则表达式:r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]'''。不过这不能匹配认证域中包含空格的情况。可以考虑将\w+改为[^'"]+,修改后的表达式如下:
r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]'''
Doing it Properly
在实际应用中,用Python处理该认证更专业的方法应该是创建一个opener实例并令其使用一个用于处理认证的handler。该handler还需一个密码管理器,然后你就可以大显身手了。
无论你是否知情,urlopen总是使用handler来处理请求,并且默认可以处理所有常规情形。我们要做的就是创建一个能处理基本认证的handler,它称之为urllib2.HTTPBasicAuthHandler。之前已经提到,它还需一个密码管理器 —— urllib2.HTTPPasswordMgr。
不过HTTPPasswordMgr存在一个小问题 —— 你必须知道当前的认证域。好在它的好兄弟HTTPPasswordMgrWithDefaultRealm已经为我们想到了。虽然名字是有那么点长,不过在使用上确实更友好。在使用HTTPPasswordMgrWithDefaultRealm时,如果不知道认证域的名称,可以传入None,这时不管是什么认证域,它会直接用所给的用户名和密码进行尝试。在指定明确的URL的情况下,这样已经足够了。如果你不确信,可以总是使用HTTPPasswordMgr并在第一次访问时从头部提取出认证域。
下面的例子主要有以下几个步骤:
· 建立顶级URL、用户名和密码
· 建立密码管理器
· 向管理器中添加密码
· 创建handler,并用创建好的密码管理器初始化
· 然后用handler创建opener
这里可以有两种方法。我们可以选择直接调用opener的open方法,从而避免使用urllib2.urlopen提供的默认opener。另外也可以将我们自己的opener设为默认,这么做会令所有对urlopen的调用都采用该opener,也就和直接调用的效果一样。这里我们就将自己的opener设为了默认:
import urllib2 theurl = 'http://www.someserver.com/toplevelurl/somepage.htm' username = 'johnny' password = 'XXXXXX' # a great password passman = urllib2.HTTPPasswordMgrWithDefaultRealm() # this creates a password manager passman.add_password(None, theurl, username, password) # because we have put None at the start it will always # use this username/password combination for urls # for which `theurl` is a super-url authhandler = urllib2.HTTPBasicAuthHandler(passman) # create the AuthHandler opener = urllib2.build_opener(authhandler) urllib2.install_opener(opener) # All calls to urllib2.urlopen will now use our handler # Make sure not to include the protocol in with the URL, or # HTTPPasswordMgrWithDefaultRealm will be very confused. # You must (of course) use it when fetching the page though. pagehandle = urllib2.urlopen(theurl) # authentication is now handled automatically for us
can you write a Digest authentication