网络爬虫笔记【2】 如何通过 HTTP 认证

登陆网页前遇到的要求输入用户名和密码的程序,通常称为身份认证程序。HTTP 认证可以保护一个作用域(成为一个 realm)内的资源不受非法访问。当一个请求要求取得受保护的资源时,网页服务器回应一个 401 Unauthorized error 错误码。这个回应包含了一个指定验证方法和领域的 WWW-Authenticate 头信息。把这个领域想象成一个存储着用户名和密码的数据库,它将被用来标识受保护资源的有效用户。比如网站使用 http basic auth 时,尝试访问该网站上标识为 “Private Files”的资源,服务器相应可能是:WWW-Authenticate:Basic realm = "Private Files"。

HTTP 规范中定义了两种认证模式:Basic Auth 和 Digest Auth,认证的基本过程是:

Basic Auth

  1. 客户请求访问网页
  2. 服务器返回 401 错误,要求认证。
  3. 客户端重新提交请求并附以认证信息,这部分信息将被编码;
  4. 服务器检查信息,通过则以正常服务页面,否则返回 401 错误

Digest Auth (使用 MD5 散列算法处理密钥)

  1. 客户发送请求
  2. 收到一个 401 消息,包含一个 Challenge 和一个 Nonce
  3. 客户将用户名密码和 401 消息返回的 challenge 一起 MD5 加密后传给服务器
  4. 服务器检查是否合法。

1. 掌握自定义 Opener 的方法

urllib.request.urlopen() 调用了 HTTPHandler 来处理无错误的 HTTP 请求,它的功能是有限的。对于 urllib.request.urlopen() 不支持的功能,我们可以通过自己定义 opener,调用其它 Handler 实现。 urllib 中的 opener 都是 urllib.request.OpenerDirector 类的实例。OpenerDirector 类管理着 Handler 对象集合,这些 Handlers 完成实际的 http 请求工作,每个 Handler 实现了一种特定协议或选项内容。OpenerDirector 作为一个组合型对象,调用各种 Handlers 打开请求的 URL。例如:HTTPHandler 执行 HTTP GET 和 POST 请求,处理无错误返回;HTTPRedirectHandler 自动处理 HTTP 301、302、303 和 307 的重定型错误;HTTPDigestAuthHandler 处理 digest 认证。

自定义的 Opener 对象都由 OpenerDirector 加载不同的 Handler 来生成。

自定义 opener 需要先初始化一个 OpenerDirector,使用 build_opener 方法实现,这是一个使用调用单一函数调用多个 Handlers 生成 opener 实例的方法。
install_opener 可以用于生成一个 opener 对象,形成全局默认的opener,这时再次使用 urlopen 时,不再使用系统原 opener,而是使用自定义的 opener。
不准备替换全局默认的 opener 时,可以使用 opener 实例中的 open 方法,访问 url。

# 自定义Opener
import urllib.request

# demo 1

httphandler = urllib.request.HTTPHandler()
opener = urllib.request.build_opener(httphandler)
request = urllib.request.Request('http://www.baidu.com/')
response = opener.open(request)

print(response.read().decode('utf-8'))
# 自定义 opener
# demo 2
# 在开发中,如果需要了解 HTTPHandler 的调试信息,可以使用下列语句,无需输出语句

httphandler = urllib.request.HTTPHandler(debuglevel=1)
opener = urllib.request.build_opener(httphandler)
request = urllib.request.Request('http://www.baidu.com')
response = opener.open(request)
运行结果:

send: b'GET / HTTP/1.1\r\nAccept-Encoding: identity\r\nHost: www.baidu.com\r\nConnection: close\r\nUser-Agent: Python-urllib/3.5\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Bdpagetype header: Bdqid header: Cache-Control header: Content-Type header: Cxy_all header: Date header: Expires header: P3p header: Server header: Set-Cookie header: Set-Cookie header: Set-Cookie header: Set-Cookie header: Set-Cookie header: Set-Cookie header: Set-Cookie header: Vary header: X-Ua-Compatible header: Connection header: Transfer-Encoding
# 自定义 opener
# demo 3
# 使用 install_opener 方法使自定义的 opener 成为全局默认的 opener

request = urllib.request.Request('http://www.baidu.com')
httphandler = urllib.request.HTTPHandler()
opener = urllib.request.build_opener(httphandler)
urllib.request.install_opener(opener)

a = urllib.request.urlopen(request).read().decode('utf-8')
print(a)

2. 掌握 HTTP Basic authentication 方法

若 Server 采用 Basic Auth 保护资源,那么你访问这些被保护资源时,就会看到一个用户认证表单,要求输入用户名和密码。用户输入后,用户名和密码都会以 Base64 编码形式发送给服务器。认证的基本过程是:

  1. 客户请求访问网页
  2. 服务器端返回 401 错误,要求认证(401 消息的头里面带了 challenge 信息,例如:认证头 WWW-Authenticate: Basic realm = "zhouhh@mydomain.com")
  3. 客户端重新提交请求并附以认证信息,这部分信息将会被编码
  4. 服务器检查信息,通过则给以正常服务页面,否则返回 401 错误。

第一次服务器返回 401 错误时,会返回 headers 字典信息,其中会包含信息:WWW-Authenticate:Basic realm = "cPanel"。我们假定一直用户名和密码,之后利用一定的编码格式将 realm 名,用户名,密码等信息编码;编码之后就可以传递服务器,认证就可通过。

# HTTP basic auth case
# 实例1 :httpbin.org 提供了 Auth demo
# 使用前先登陆该网站,进行设置后可获得相应示例的 request url,复制该 url 到本程序作为初始访问的 url。

import urllib.request
import urllib.error
import urllib.parse

# 我设定的用户名为 wxhh ,密码为 123456
url = 'http://www.httpbin.org/basic-auth/wxhh/123456'
headers = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
    }

username = 'wxhh'
password = '123456'

# 若我们将 username 写错,如username = 'wxhh1'
# 则运行结果为:HTTP Error 401: UNAUTHORIZED

request = urllib.request.Request(url,headers=headers)

# Create a password manager
passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

# Because we have put None at the start
# it will always use this username / password combination for urls
# for which the url is a super-url
passmgr.add_password(None,url,username,password)

# create the AuthHandler
authhandler = urllib.request.HTTPBasicAuthHandler(passmgr)

opener = urllib.request.build_opener(authhandler)

try:
    with opener.open(request) as response:
        print(response.status)
        print(response.read().decode('utf-8'))
except urllib.error.URLError as e:
    print(e)
except urllib.error.HTTPError as e:
    print(e)
    print(e.headers)
except:
    print('Unknown error')
运行结果:

200
{
  "authenticated": true, 
  "user": "wxhh"
}

3. 掌握 HTTP Digest Auth 方法

Basic Auth 是一种很不安全的认证方式,如果有人截留了合法用户成功认证的过程,那么就可以解码 Base64 编码的 username 和 password,实现重放攻击。
这种认证方式较 Basic Auth 安全一些,因为它不会讲密码明文发送给服务器。Digest Auth 使用 MD5 散列算法处理密钥,防止了密钥被窃取后解密(当然这安全也并不稳妥)。具体情况可以参考 tfc2617 文档。
使用 Digest Auth 认证的基本过程:

  1. 客户发送请求后
  2. 收到一个 401(未授权)消息,包含一个 Challenge 和一个唯一的字符串:nonce(其值每次请求都不一样)
  3. 客户将用户名密码和 401 消息返回的 Challenge 一起 MD5 加密之后传给服务器(这样即使有窃听,也无法进行重放攻击)
  4. 服务器检查是否合法

通常在第 2 步会收到下列响应头信息(部分):

www-authenticate: Digest realm="me@kennethreitz.com",
nonce="bd66cd6ebd97952de63c027ebc175f5f", qop="auth",
opaque="33ab5186c75988ba5712c61874eaa041", algorithm=MD5, stale=FALSE

参数解释:

  • www-authenticate : http 中用于提供认证信息的一个标头
  • realm :表示保护域,其值可以是一个简单的字符串( rfc2617 上要求是一个 Email 类型的字符串)
  • qop :是认证的(校验)方式,这个信息比较重要,对后面 md5 加密过程有影响
  • nonce :表示一次性会话密钥,其值是一个字符串,每次登录服务器都会产生一个新的随机字符串作为 nonce 值;如果不严格,可以随机生成一个 GUID(即唯一、不重复的);如果严格,则需要包含时间信息、客户端 IP 信息和其他信息,因为认证过程的时间很短,所以如果服务器收到认证信息后发现这个时间和服务器的时间相去甚远,那说明不正常,直接拒绝,以防止攻击。附有客户端 IP 信息有利于服务器了解哪些 IP 持续试探,可以将其置入 blacklist。这些严格的做法主要是为了防止攻击。在 rfc2617 上有较为详细的描述。
  • opaque :由服务器指定的一个字符串,通常是 Base64 或 hexadecimal 编码数据,这个字符串由服务器发送给客户端后,客户端会原样传回,它只是透传而已。事实上,上面的那些域,客户端还是会原样返回的,但是返回时除了以上的那些域之外,还会增加新的内容进来。
  • algorithm :表示加密算法,通常为 MD5 散列算法,并不是严格的加密。
  • stale :一个标志位,用来指示前一个请求是否被拒绝了。
# 尝试通过 http digest auth
# 首先在 httpbin.org 网站上设置 auth-digest-auth,并获得测试用的 url

import urllib.request
import urllib.error

def auth():
    url = 'http://www.httpbin.org/digest-auth/auth/wxhh1/1234567'
    username = 'wxhh1'
    password = '123456'   # 密码故意写错,查看服务器返回的 401 错误信息
    realm = 'me@kennethreitz.com'
    
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
    }
    request = urllib.request.Request(url,headers=headers)
    
    try:
        passmgr = urllib.request.HTTPPasswordMgr()
        passmgr.add_password(realm=realm,uri=url, user=username, passwd=password)
        authHandler = urllib.request.HTTPDigestAuthHandler(passmgr)
        
        opener = urllib.request.build_opener(authHandler)
        
        with opener.open(request) as response:
            print(response.status)
            print(response.read().decode('utf-8'))

    except urllib.error.HTTPError as e:
        print(e)
        print(e.headers)
    except urllib.error.URLError as e:
        print(e)
    except:
        print("Unknown Error!")
        
if __name__ == "__main__":
    auth()
运行结果:

HTTP Error 401: digest auth failed
Connection: close
Server: gunicorn/19.9.0
Date: Thu, 18 Oct 2018 08:56:33 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Www-Authenticate: Digest realm="me@kennethreitz.com", nonce="3a6bc7bd32670e7028a49af70c91fb0f", qop="auth", opaque="ee4b5e7cbd4d2de7462ca887218b0302", algorithm=MD5, stale=FALSE
Set-Cookie: stale_after=never; Path=/
Set-Cookie: last_nonce=1bb42ac7d4aa2fbb8941e3b0e22cb439; Path=/
Set-Cookie: fake=fake_value; Path=/
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页