代码改变世界

Basic Authentication - Authentication with Python(翻译草稿)

2010-08-25 11:08  ubunoon  阅读(3146)  评论(0编辑  收藏  举报

Basic Authentication

Authentication with Python

 

原文链接:

http://www.voidspace.org.uk/python/articles/authentication.shtml#so-let-s-do-it 

 

Note

There is a French translation of this article - Authentification Basique.

 

 

Contents

· Introduction

Basic Authentication

Making a Request

Getting A Response

Error 401 and realms

First Example

The Username/Password

§ base64

So Let's Do It

· Doing it Properly

· A Word About Cookies

· Footnotes

 

Introduction

This tutorial aims to explain and illustrate what basic authentication is, and how to deal with it from Python. You can download the code from this tutorial from the Voidspace Python Recipebook.

这篇教程试图解释和介绍基本的验证,以及在Python中如何处理验证。可以从Voidspace Python Recipebook.下载本教程。


The first example, So Let's Do It, shows how to do it manually. This illustrates how authentication works.


首先是正确的做,如何手工操作,介绍验证是如何进行的。


The second example, Doing it Properly, shows how to handle it automatically - with a handler.


第二个例子,合适的方式,使用handler如何自动的处理。


These examples make use of the Python module urllib2. This provides a simple interface to fetching pages across the internet, the urlopen function. It provides a more complex interface to specialised situations in the form of openersand handlers. These are often confusing to even intermediate level programmers. For a good introduction to using urllib2, read my urllib2 tutorial.

这些例子充分利用了Python模块urllib2urllib2提供了获取网络页面的简单接口函数urlopen,提供了复杂的在openershandlers形式下指定状态的获取方式。这些很容易在低级programmer中混淆。更好的使用urllib2,请阅读我的urllib2 tutorial.


Basic Authentication

There is a system for requiring a username/password before a client can visit a webpage. This is called authentication and is implemented on the server. It allows a whole set of pages (called a realm) to be protected by authentication.


在一个客户端访问一个页面前,这个系统需要用户名和密码,这个称之为authentication,由server来实现。允许所有页面(称为realm)被这个authentication保护。


This scheme (or schemes) are defined by the HTTP spec, and so whilst python supports authentication it doesn't document it very well. HTTP documentation is in the form of RFCs [1] which are technical documents and so not the most readable  .


这个机制由HTTP手册定义,因此Python支持的authentication没有很好的文档,HTTP文档时RFC形式的技术文档,不容易被阅读。


The two normal [2] authentication schemes are basic and digest authentication. Between these two, basic is overwhelmingly the most common. As you might guess, it is also the simpler of the two.


两个普通的authentication机制是basicdigest authentication,在两个authentication中,basic更通用,你可能会才,为什么也提供两个简单的。


A summary of basic authentication goes like this :

· client makes a request for a webpage

· server responds with an error, requesting authentication

· client retries request - with authentication details encoded in request

· server checks details and sends the page requested, or another error

The following sections covers these steps in more details.

 

一个basic authentication通常具有如下操作:

· 客户端请求一个网页

· server相依出现错误,需要一个authentication

· 客户端重新进行含有authentication细节编码的请求

· server检查细节,发送请求的页面或者出错。


Making a Request

A client is any program that makes requests over the internet. It could be a browser - or it could be a python program. When a client asks for a web page, it is sending a request to a server. The request is made up of headers with information about the request. These are the 'http request headers'.


客户端是任何可以请求internet的程序,可以为浏览器或者一个Python程序,当客户端请求一个网页时,发送一个请求道server,请求包含了请求信息头,这些为http请求头。


Getting A Response

When the request reaches the server it sends a response back. The request may still fail (the page may not be found for example), but the response will still contain headers from the server. These are 'http response headers'.


当请求到达server的时,server回复一个响应,请求可能仍旧出现错误(如页面没有找到),但是响应仍旧包含来自服务器的头,这些微http响应头


If there is a problem then this response will include an error code that describes the problem. You will already be familiar with some of these codes - 404 : Page not found, 500 : Internal Server Error, etc. If this happens; an exception [3] will be raised by urllib2, and it will have a 'code' attribute. The code attribute is an integer that corresponds to the http error code [4].


如果出现了问题,这些响应头将包含一个描述问题的错误码,可能对下面的错误码比较熟悉

404-页面没有找到,500-服务器内部错误,如果发生了错误,urllib2会抛出含有一个code属性的异常,code属性关联http错误码的整数。


Error 401 and realms

If a page requires authentication then the error code is 401. Included in the response headers will be a 'WWW-authenticate' header. This tells us the authentication scheme the server is using for this page and also something called a realm. It is rarely just a single page that is protected by authentication but a section - a 'realm' of a website. The name of the realm is included in this header line.


如果页面需要authentication,并且产生401错误,包含的响应头会是 WWW-authentication头,这表明serverauthentication机制用于页面,并称为realm,很少有单个页面被authentication保护,但是一个section-一个realm为一个websiterealm的名字包含在这个头上。


The 'WWW-Authenticate' header line looks like WWW-Authenticate: SCHEME realm="REALM".


WWW-Authenticate头先包含类似的:

WWW-Authenticate: SCHEME realm="REALM".

For example, if you try to access the popular website admin application cPanel your browser will be sent a header that looks like : WWW-Authenticate: Basic realm="cPanel"

例如,如果你试图访问流行网站admin应用cPanel,浏览器将发送类似的头:WWW-Authenticate: SCHEME realm="cPanel".

 

If the client already knows the username/password for this realm then it can encode them into the request headers and try again. If the username/password combination are correct, then the request will succeed as normal. If the client doesn't know the username/password it should ask the user. This means that if you enter a protected 'realm' the client effectively has to request each page twice. The first time it will get an error code and be told what realm it is attempting to access - the client can then get the right username/password for that realm (on that server) and repeat the request.

 

如果客户端知道realm用户名和password,客户端会重新编码用户名和密码,并在此请求,如果usernamepassword是正确的,请求而将正常成功,如果client的客户不知道username/password,这也就是说如果进入了一个realm保护的serverclient端需要对每个页面请求而两次,第一次将获取一个错误码,告知客户端需要什么样的realm去登录,然后client端为realm(在服务器端)获取正确的usernamepassword,并重新请求。


HTTP is a 'stateless' protocol. This means that a server using basic authentication won't 'remember' you are logged in [5] and will need to be sent the right header for every protected page you attempt to access.


HTTP是一个状态协议,也就是server使用基本的authentication不会记住你的登录,并需要为登录的每个页面发送正确的头。


First Example

Suppose we attempt to fetch a webpage protected by basic authentication. :


假定我们需要通过basic authentication来获取一个网页:

 

 

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']

 

Note

If the exception has a 'code' attribute it also has an attribute called 'headers'. This is a dictionary like object with all the headers in - but you can also print it to display all the headers. See the last line that displays the 'www-authenticate' header line which ought to be present whenever you get a 401 error.


如果异常有code属性,也会有一个headers属性存在,这是一个类似dictionary的对象,包含了所有的头内容,可以通过打印header显示所有的头信息,看最后一行显示'www-authenticate'头行,在获取到401错误时,这个信息就会存在。


A typical output from above example looks like :


上述例子典型的输出如下:

WWW-Authenticate: Basic realm="cPanel"

Connection: close

Set-Cookie: cprelogin=no; path=/

Server: cpsrvd/9.4.2

 

Content-type: text/html

 

Basic realm="cPanel"

 

You can see the authentication scheme and the 'realm' part of the 'www-authenticate' header. Assuming you know the username and password you can then navigate around that website - whenever you get a 401 error with the same realm you can just encode the username/password into your request headers and your request should succeed.


可以看到authentication机制和realm部分的www-authenticate头,假定你清楚usernamepassword,可以冲浪整个网站,当获取到the same realm401错误时,在请求头中编码usernamepassword,请求就可以成功。


The Username/Password

Lets assume you need to access pages which are all in the same realm. Assuming you have got the username and password from the user, you can extract the realm from the header. Then whenever you get a 401 error in the same realm you know the username and password to use. So the only detail left, is knowing how to encode the username/password into the request header  . This is done by encoding it as a base 64 string. It doesn't actually look like clear text - but it is only the most vaguest of 'encryption'. This means basic authentication is just that - basic. Anyone sniffing your traffic who sees an authentication request header will be able to extract your username and password from it. Many websites like yahoo or ebay, use javascript hashing/encryption and other tricks to authenticate a login. This is much harder to detect and mimic from python ! You may need to use a proxy client server and see what information your browser is actually sending to the website [6].


假定进入页面使用同样的realm,假定拥有从user哪儿获取的usernamepassword,你可以从头上提取realm,当在同样的realm中获取到401错误是,需要使用usernamepassword。因此仅剩下细节需要了解,也就是如何在请求头中编码usernamepassword。这个通过base 64 string方式编码,这种方式类似清楚的文本,但仅被vaguest用来加密。这也就用来表明basic authenticationbasic的。任何嗅探网络都可以看到authentication的请求头,并能够从嗅探得到的请求头中获取usernamepassword,许多网站,如yahoo或者ebay,使用JavaScript hashing/加密和其他技术去authenticate登录。从Python上这个更难被检测和模仿。可以使用代理客户端服务器,查看浏览器具体向服务器发送了那些消息。


base64

There is a very simple recipe base64 recipe over on the Activestate Python Cookbook (It's actually in the comments of that page). It shows how to encode a username/password into a request header. It goes like this :

在Activestate Python Cookbook这个非常简单的诀窍base64 recipe(实际上是在评论页面上),显示如何在请求头上编码usernamepassword,类似:

import base64

base64string = base64.encodestring('%s:%s' % (username, password))[:-1]

req.add_header("Authorization", "Basic %s" % base64string)

 

Where req is our request object like in the first example.


此处req为如同第一个例子中请求对象。


So Let's Do It

Let's wrap all this up with an example that shows accessing a page, extracting the realm, then doing the authentication. We'll use a regular expression to pull the scheme and realm out of the response header. :


将上述的内容整合起来做成下面登录页面的例子,提取realm,然后authentication。使用正则表达式来从响应头上取出schemerealm


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()

 

 

When the code has run the contents of the page we've fetched is saved as a string in the variable 'thepage'. The regular expression used to match the authentication header in this example is r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]'''. This doesn't work where there is a space in the realm, which you can fix by replacing \w+ with [^'"]+. This gives us the regular expression:


从代码中获取得到的页面内容作为字符串保存在thepage变量中,在这个例子中正则表达式用来匹配authentication头:r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]'''。在realm中有一个空格将导致运行出错,可以通过取代\w[^'"]来修复,下面给出了正则表达式。

r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]'''


Warning

If you are writing an http client of any sort that has to deal with basic authentication, don't do it this way. The next example that shows using a handler is the right way of doing it.


如果试图编写任何排序的http客户端去处理basic authentication,不用这么使用,下面的例子使用handler正确的处理方式来处理。


Doing it Properly

In actual fact the proper way to do BASIC authentication with Python is to install an opener that uses an authentication handler. The authentication handler needs a passowrd manager - and then you're away  .


用Python确切的方式处理BASIC authentication,使用authentication handler进行opener安装,authentication需要一个密码管理器,之后就不需要密码了。


Every time you use urlopen you are using handlers to deal with your request - whether you know it or not. The default opener has handlers for all the standard situations installed [7]. What we need to do is create an opener that has a handler that can deal with basic authentication. The right handler for our needs is calledurllib2.HTTPBasicAuthHandler. As I mentioned it also needs a password manager - urllib2.HTTPPasswordMgr.


每次使用urlopen,就是直接或间接使用handler处理请求。默认的opener有一个标准状态安装的handler。需要创建一个有handleropener,用以处理basic authentication,此处我们需要的正确的handlerurllib2.HTTPBasicAuthHandler,以及前面提到的密码管理器urllib2.HTTPPasswordMgr.


Unfortunately our friend HTTPPasswordMgr has a slight problem - you must already know the realm you're fetching. Luckily it has a near cousin HTTPPasswordMgrWithDefaultRealm. Despite the keyboard busting name, it's a bit more friendly to use. If you don't know the name of the realm - then pass in None for the realm, and it will try the username and password you give it - whatever the realm. Seeing as you are going to specify a specific URL, it is likely that this will be sufficient. If you aren't convinced then you can always use HTTPPasswordMgr and extract the realm from the authentication header the first time you meet it.


不幸地是,我们的HTTPPasswordMgr有一个轻微的问题,必须知道已经获取的realm,幸好有一个接近的HTTPPasswordMgrWithDefaultRealm可以使用。尽管键盘打碎了名字,需要更友好的使用。如果不知道realm的名字,给realm传递None,无论什么样的realm,系统会使用给定的usernamepassword登录。使用给定地址的URL,可以看到这个很有效。如果不相信,可以使用HTTPPasswordMsg,并如同第一次那样从realm中获取authentication头。


This example goes through the following steps :

· establishes the top level url, username and password

· Create our password manager (with default realm)

· Gives the password to the manager

· Creates the handler with the manager

· Creates an opener with the handler installed

 

下面是例子的步骤

· 建立最顶层的urlusernamepassword

· 创建密码管理器(使用默认的realm

· 给密码管理器设置密码

· 使用密码管理器的handler

· 用这个handler创建opener


At this point we have a choice. We can either use the open method of the opener directly. This leaves urllib2.urlopenusing the default opener. Alternatively we can make our opener the default one. This means all future calles tourlopen will use this opener. As all openers have the default handlers installed as well as the ones you pass it, it shouldn't break urlopen to do this. In the example below we install it, making it the default opener :


此处有一个选择。可以直接使用open或者opener。这个使得urllib2.urlopen使用默认的opener。一般地我们使用默认的opener,以后也就是urlopen都是用这个opener,所有的opener有一个默认的handler通常也需要传递过去,这个不需要urlopen。下面安装的例子中,需要使用默认的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

 

 

Hurrah - not so bad hey  .

A Word About Cookies

Some websites may also use cookies alongside authentication. Luckily there is a library that will allow you to have automatic cookie management without having to think about it. This is ClientCookie. In Python 2.4 it becomes part of the python standard library as cookielib. See my article on cookielib - for an example of how to use it.


一些网站需要cookies伴随authentication,很幸运地是允许用户自由选择cookie管理的库,不需要深入考虑。这个是ClientCookie,在python2.4成为python标准库为cookielib


 

Footnotes

 

[1] http://www.faqs.org/rfcs/rfc2617.html is the RFC that describes basic and digest authentication

[2] There is also a M$ proprietary authentication scheme called NTLM, but it's usually found on intranets - I've never had to deal with it live on the web.

[3] An HTTPError, which is a subclass of IOError

[4] Or at least state management is a separate subject. Using cookies the server may well have details of your session - but you will still need to authenticate each request.

[5] See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a full list of error codes

[6] See this comp.lang.python thread for suggestions of several proxy servers that can do this.

[7] See the urllib2 tutorial for a slightly more detailed discussion of openers and handlers.