OAuth Step by Step

Mon 04 January 2010
  • 手艺 tags:
  • mashup
  • oauth
  • python
  • web published: true comments: true

最近琢磨OAuth认证方式。OAuth的优点主要在于

  • 用户不需要直接提供用户名密码给第三方应用,就可以让第三方应用访问受限资源;
  • 资源提供方对第三方应用有更细粒度的控制。
在整个OAuth协议里,生成signature的base string是最容易出错的部分。它由HTTP方法名、URL编码的请求路径和请求的参数表组成。
请求的参数表是除去oauth_signature以外的所有参数,按参数名排序,并进行url转义
def to_signature_key(method, url, data):
    keys = list(data.keys())
    keys.sort()
    encoded = urllib.quote("&".join([key+"="+data[key] for key in keys]))
    return "&".join([method, urllib.quote(url, safe="~"), encoded])
有了这个通用的生成signature base string的方法,以后就可以根据OAuth协议规范按步骤进行。

首先获取Request Token。这一步通常使用资源提供方注册的API Key和API Key Secret

def request_token_params(consumer_key, consumer_secret, path, method='GET'):
    data={}
    data['oauth_consumer_key']=consumer_key
    data['oauth_signature_method']='HMAC-SHA1'
    data['oauth_timestamp']=str(int(time.time()))
    data['oauth_nonce']=''.join([str(random.randint(0,9)) for i in range(10)])
    print data

msg = to_signature_key(method, path, data)
print msg

signed = base64.b64encode(hmac.new(consumer_secret+"&", msg, hashlib.sha1).digest())
print signed
data['oauth_signature']=signed
return data

def result2dict(result_string):
d = {}
params = res.split('&')
for p in params:
d[p.split('=')[0]] = p.split('=')[1]
return d

conn = httplib.HTTPConnection("www.douban.com", 80)

params = request_token_params(consumer_key, consumer_secret, request_token_path)
conn.request('GET', request_token_path+"?"+urllib.urlencode(params))
res = conn.getresponse().read()
print res
request_token = result2dict(res) 这一步可以获得未经认证的Request Token和Request Token Secret。需要注意的细节是在计算hmac签名的时候,即使只有一个Token Secret,仍然需要加上"&"

第二步要求用户授权该Request Token,打开浏览器,将用户定向到相应的授权页面,参数为上一步获得的Request Token

第三步,用授权过的Request Token换取Access Token。这一步类似第一步,只是用于签名的token包括API Key Secret和Request Token

def access_token_params(consumer_key, consumer_secret, oauth_token, oauth_secret, path, method='GET'):
    data={}
    data['oauth_consumer_key']=consumer_key
    data['oauth_signature_method']='HMAC-SHA1'
    data['oauth_timestamp']=str(int(time.time()))
    data['oauth_nonce']=''.join([str(random.randint(0,9)) for i in range(10)])
    data['oauth_token'] = oauth_token

msg = to_signature_key(method, path, data)
print msg

signed = base64.b64encode(hmac.new(consumer_secret+"&"+oauth_secret, msg, hashlib.sha1).digest())
print signed
data['oauth_signature']=signed
return data

params = access_token_params(consumer_key, consumer_secret, request_token['oauth_token'],
request_token['oauth_token_secret'], access_token_path)
conn.request('GET', access_token_path+"?"+urllib.urlencode(params))
res = conn.getresponse().read()
print res
access_token = result2dict(res) 这一步将至少返回Access Token和Access Token Secret,是最终用于访问受限资源的Token。以豆瓣的实现为例,OAuth的相关参数应放在HTTP头里随请求进行发送。

def oauth_header(consumer_key, consumer_secret, oauth_token, oauth_secret, path, realm):
    data = access_token_params(consumer_key, consumer_secret, oauth_token, oauth_secret, path, method="POST")
    header_string = ','.join([key+'="'+data[key]+'"' for key in data.keys()])
    return 'OAuth realm="'+realm+'",'+header_string

posturl = 'http://api.douban.com/miniblog/saying'

content = """<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns:ns0="http://www.w3.org/2005/Atom" xmlns:db="http://www.douban.com/xmlns/">
<content>li lei ju le han mei mei</content>
</entry>
"""

header = {}
header['Authorization'] = oauth_header(consumer_key, consumer_secret,
access_token['oauth_token'], access_token['oauth_token_secret'],
posturl, "http://api.douban.com")
header['Content-Type'] = 'application/atom+xml'
print header

conn.request('POST', posturl, content, header)
res = conn.getresponse().read()

print res

conn.close() 在这一步中,用于生成signature base string的url是要访问的受限资源地址,而签名的参数表依然是oauth相关的参数。
生成的Authorization头如下

Authorization: OAuth realm="http://api.douban.com",
    oauth_nonce="8735717688",
    oauth_timestamp="1262613619",
    oauth_consumer_key="0bc081a01168b263234184e0343a1729",
    oauth_signature_method="HMAC-SHA1",
    oauth_token="5fb836c37543ad691f28a44a5fcb083b",
    oauth_signature="jk6p5qaXVPrGQctSzpO5jjYHfDk="
用这个头就可以在一定的时间内访问所有授权范围内的受限资源。

代码是ugly了一些,不过应该相对易于理解吧。