Twitter API开发与OAuth介绍
2010 6 18 12:24 PM 10090次查看
分类:Google App Engine 标签:Google App Engine, Python
不用说,Twitter API肯定是必备的,我只要让我的应用去访问Twitter API,然后返回结果给我的手机就行了。但由于GAE被墙,单纯地在GAE上建个WEB应用对我的手机也没有任何益处,于是想到了XMPP。由于GAE可以收发XMPP,而我的手机可以用Google Talk,于是可以进行2次转发,来更新我的Twitter了。
思路想好后,我就准备动手了,可惜对Twitter API是一头雾水,所以只能先看开发文档了。
初步看了下后,发现Twitter API还算是比较简单的,除了Streaming API,其他都是REST full的,懂HTTP的上手应该不难。
而其中最重要和最困难的部分无疑是授权(authorization)和认证(authentication)。
在Authenticating Requests这篇文章中提到了Twitter在2010年6月30日以后将不支持直接传递用户名和密码的basic auth(但是由于南非世界杯导致的downtime和overload,Twitter决定将这一举措延期到8月),因此推荐WEB应用使用OAuth;但桌面和移动应用也可以使用xAuth。
xAuth无疑要比OAuth方便,用户可以直接在第三方应用输入用户名和密码,以交换OAuth access token,也就意味着用户无需自己翻墙到Twitter去允许应用的访问了。不过使用xAuth需要Twitter的申请,而且我感觉这对用户来说并不安全,所以断然放弃了。
在Overview of Authorization Options这篇文章里还提到了OAuth Echo和Out-of-band/PIN Code Authentication,不过对我而言没什么意义,所以无视了。
于是接下来就该钻研OAuth了。实际上1.0a已经摒弃了,RFC 5849才是标准实现,但是从REST API Changelog来看,Twitter似乎还在用1.0a。
回到这篇文章,它的第三点Definitions部分是首先需要弄懂的,由于是很简单的英文,我就不翻译了。
而认证的过程主要是由这几个步骤组成:
- Consumer向Service Provider请求Request Token
- Service Provider验证后返回未授权的Request Token
- Consumer将User跳转到Service Provider,让其同意授权
- Service Provider接到User的授权许可后,将User重定向到Consumer
- Consumer在用户跳转回来后拿到已授权的Request Token,再次向Service Provider请求Access Token
- Service Provider验证已授权的Request Token,向Consumer提供Access Token
- Consumer使用Access Token,访问User的资源
接下来就来介绍这几个部分的实现了。原文只是粗略地说了下怎么做,细节和算法方面就没有涉及了,所以这里我用GAE/Python代码来辅助说明。
另外,实际上这篇文章介绍的实现方法很难,我看别人的实现,URL和数据格式都有些不一样,只可惜没有那方面的文档,所以我还是按照该文来做吧。
首先要做的自然是注册一个应用了。要填的项以后全都能更改,所以不需要担心填错。
Application Name就是应用名,当你使用你的应用来发推时,会在下面显示“xx 时间 ago via 应用名”,也算是展示你的个性吧。
Application Website是应用的主页,其他人如果对你的应用感兴趣的话,点那个应用名,就会跳到你的应用主页了。
Application Type是应用类型,这里由于是用GAE,所以设为Browser。
Callback URL是回调地址,在认证时需要用到,不过你也可以在认证时手动设置其他的回调地址参数。
Default Access type是访问权限类型,由于发推需要写权限,所以设为Read & Write。
其他就没什么好提的了,填错了也没关系。
注册完成后就可以看到你的应用设置了,包括API key、Registered Callback URL、Consumer key和Consumer secret等,还有些通用的token获取地址。
除了API key外,这些常量都将在后面用到,所以先写上:
CONSUMER_KEY = ...
CONSUMER_SECRET = ...
HOME_URL = 'http://keakon.appspot.com/twitter/'
#CALLBACK_URL = 'http://localhost:8080/twitter/callback' # 这个是用于本地测试的,改成下面的或者设为''就使用默认回调地址
CALLBACK_URL = 'http://keakon.appspot.com/twitter/callback'
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
AUTHORIZE_URL = 'https://api.twitter.com/oauth/authorize'
接着把要加载的库也列出一下:
import logging
from urllib import quote, urlencode
import hmac
from hashlib import sha1
from random import getrandbits
from time import time
from google.appengine.api import xmpp, urlfetch, users, memcache
from google.appengine.ext import webapp, db
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp.xmpp_handlers import CommandHandler
然后就是设定访问链接了:
application = webapp.WSGIApplication([
('/_ah/xmpp/message/chat/', XMPPHandler),
('/twitter/', TwitterStatus),
('/twitter/oauth', TwitterOauth),
('/twitter/callback', TwitterCallback)
])
其中XMPPHandler用于接收和解析XMPP消息,调用相应的Twitter API;TwitterStatus是这个应用的主页,用于显示用户账号的关联情况;TwitterOauth是用于获取Request Token,并将用户重定向到Service Provider的;TwitterCallback是接收Service Provider的回调,完成整个验证过程的。为方便起见,我还将后3个设置了login: required,免得处理麻烦的登录。
先来完成首页。这个页面最简单,只要向数据库查询用户的Google账号是否已与Twitter账号关联即可。而模型也很简单,key_name保存Google账号的邮箱,再加上Access Token和Access Token Secret这2个属性即可:
class Twitter(db.Model):
token = db.StringProperty(indexed=False)
secret = db.StringProperty(indexed=False)
class TwitterStatus(webapp.RequestHandler):
def get(self):
email = users.get_current_user().email()
twitter = Twitter.get_by_key_name(email)
# 然后判断twitter是否能取到,输出状态信息即可。当然也别忘了加上TwitterOauth的链接。
下面就该做验证了,讲述之前先要说下签署请求。它的目的就是保证请求是由Consumer发出的(因为包含oauth_consumer_key),并且请求不是重复的(因为oauth_nonce只能用一次)。签署的方法就是把所有参数进行排序,用&连接起来,再进行URL转义,然后和HTTP方法、URL转义过的base URL用&连接起来,组成base string,再用oauth_consumer_key和oauth_token_secret进行签名。而URL转义时与Python的默认实现不同,/是需要转义的,~却不需要。
目前Twitter只支持HMAC-SHA1这一种签名方法,算法实现将在下面讲述。
从Handler的数目能知道,验证共需要2步。
第一步非常复杂,只是为了获取request token。
它的base URL是http://api.twitter.com/oauth/request_token,支持GET和POST,建议使用HTTPS和POST。签名时别忘了统一HTTP方法和base URL,有次我检查了半天没找到原因,最后发现是https写成了http…
参数如下:
oauth_callback
- 可选的回调地址
oauth_consumer_key
oauth_nonce
- 随机生成的字符串,要保证每次调用都是独一无二的,最容易想到的就是用UUID了,不过开销较大,实际上64位的随机数基本就能保证唯一性了,当然从0开始递增也行。
oauth_signature_method
- HMAC-SHA1
oauth_timestamp
- UNIX时间戳,从格林威治时间1970年01月01日00时00分00到现在为止的秒数,最简单的方法就是int(time.time())
oauth_version
- 1.0
先写个辅助函数实现URL转义:
def qt(s):
return quote(s, '~')
接着就开始签名了:def get_oauth_params(params, base_url, token_secret='', callback_url=CALLBACK_URL, method='POST'):
default_params = {
'oauth_consumer_key': CONSUMER_KEY,
'oauth_signature_method': 'HMAC-SHA1',
'oauth_timestamp': str(int(time())),
'oauth_nonce': hex(getrandbits(64))[2:], # 生成64位的随机数
'oauth_version': '1.0'
} # 这些是都需要用到的通用参数,而params是特定参数
params.update(default_params)
if callback_url: # 没有callback_url时就不加,会使用默认回调地址
params['oauth_callback'] = qt(callback_url)
keys = sorted(list(params.keys()))
encoded = qt('&'.join(['%s=%s' % (key, params[key]) for key in keys]))
# 连接成'key1=value1&key2=value2'的转义后的形式,且key经过了排序
base_string = '%s&%s&%s' % (method, qt(base_url), encoded) # 拼接base string
key = CONSUMER_SECRET + '&' + token_secret # 注意token_secret可能为'',这也不要紧,'&'必须要有
params['oauth_signature'] = qt(hmac.new(key, base_string, sha1).digest().encode('base64')[:-1]) # 看上去挺复杂的签名,看不懂就照抄吧
return params
有了这个签名函数后就好办了,向Service Provider获取未授权的request token,再让用户带着request token,转到Service Provider即可。不过还需要2个辅助函数:
def qs2dict(s): # 这个函数是把'key1=value1&key2=value2'转换成字典对象
dic = {}
for param in s.split('&'):
(key, value) = param.split('=')
dic[key] = value
return dic
def dict2qs(dic): # 这个函数是把字典对象转换成'key1="value1", key2="value2"'的形式
return ', '.join(['%s="%s"' % (key, value) for key, value in dic.iteritems()])
这就是TwitterOauth的实现了:class TwitterOauth(webapp.RequestHandler):
def get(self):
params = get_oauth_params({}, REQUEST_TOKEN_URL)
res = urlfetch.fetch(url=REQUEST_TOKEN_URL, headers={'Authorization': 'OAuth ' + dict2qs(params)}, method='POST')
# OAuth参数需要放在Authorization头字段里
if res.status_code != 200:
logging.error('Fetch request token error.\n%s\n%s\n%s' % (res.status_code, res.headers, params))
self.response.out.write('获取request token失败,可能是Google App Engine或Twitter出现了问题,请稍后再试。')
return
dic = qs2dict(res.content) # 拿到oauth_token和oauth_token_secret
if dic['oauth_callback_confirmed'] != 'true': # 成功的话会有oauth_callback_confirmed=true,没有的话说明失败了
logging.error('Request token param error.\n%s\n%s\n%s' % (res.status_code, res.headers, params))
self.response.out.write('获取request token失败,可能是Google App Engine或Twitter出现了问题,请稍后再试。')
return
memcache.set(dic['oauth_token'], dic['oauth_token_secret'], 600, namespace='TwitterRequestToken')
# 临时存储一下oauth_token和oauth_token_secret,用户回来时还会用到;如果想要更保险的话,可以使用数据库来取代memcache
self.redirect('%s?oauth_token=%s' % (AUTHORIZE_URL, dic['oauth_token'])) # 将用户跳转到Service Provider
跳转到Service Provider后,用户就需要点击allow来允许Consumer访问他的资源了。注意这一步只需要向用户暴露oauth_token,别把其他参数(例如oauth_consumer_key)暴露了,不然不怀好心的人可以伪装你的应用。
此外用户必须能在浏览器里访问Twitter,不管是用代理还是改hosts,这也是OAuth的一个麻烦之处。
用户点击allow后,Service Provider确认请求,将用户重定向回callback URL,并返回oauth_token和oauth_verifier。
因此第二步就是拿到oauth_verifier,以此交换access token。它的base URL是https://api.twitter.com/oauth/access_token,需要这些参数:
oauth_consumer_key
oauth_nonce
- 再次随机生成,不要和之前一样
oauth_signature_method
- HMAC-SHA1
oauth_token
- 刚返回的oauth_token(实际上和重定向时是一样的)
oauth_timestamp
- 当前时间戳,不能早于前一次的时间戳
oauth_verifier
- 刚返回的oauth_verifier
oauth_version
- 1.0
有了之前的经验,TwitterCallback的实现也不难了:
class TwitterCallback(webapp.RequestHandler):
def get(self):
request = self.request
oauth_token = request.get('oauth_token') # 从回调链接获取oauth_token
if not oauth_token:
logging.error('Missing oauth_token:' + oauth_token)
self.response.out.write('交换access token失败,缺少oauth_token,请返回重试。')
return
oauth_token_secret = memcache.get(oauth_token, 'TwitterRequestToken') # 在memcache里根据oauth_token取回oauth_token_secret
if not oauth_token_secret:
logging.error('Missing or wrong oauth_token:' + oauth_token)
self.response.out.write('交换access token失败,可能是oauth_token无效或已过期,请重试。')
return
params = get_oauth_params({ # 这次要用到2个新参数,刚才已经获取了
'oauth_token': oauth_token,
'oauth_verifier': request.get('oauth_verifier')
}, ACCESS_TOKEN_URL, oauth_token_secret) # oauth_token_secret也已经拿到了,也要加进来
res = urlfetch.fetch(url=ACCESS_TOKEN_URL, headers={'Authorization': 'OAuth ' + dict2qs(params)}, method='POST')
if res.status_code != 200:
logging.error('Exchanging access token error.\n%s\n%s\n%s' % (res.status_code, res.headers, params))
self.response.out.write('交换access token失败,可能是Google App Engine或Twitter出现了问题,请稍后再试。')
return
dic = qs2dict(res.content) # 拿到oauth_token和oauth_token_secret,实际上还有用户的Twitter名和ID,不过我们用不到
Twitter(key_name=users.get_current_user().email(), token=dic['oauth_token'], secret=dic['oauth_token_secret']).put() # 保存到数据库以备以后使用
# 然后告诉用户关联成功即可
到此最关键的认证部分就结束了,Consumer已经可以拿oauth_token和oauth_token_secret来为所欲为了。最后就来实现最基本的逻辑,也就是转发消息的部分。这里我只实现了发推,你还可以依葫芦画瓢地实现列出时间线、回复、RT等操作。
发推的base URL是http://api.twitter.com/version/statuses/update.format,支持的format有xml和json,这里当然选择方便的json。详细文档可以看POST statuses/update。
参数如下:
POST body
- 必选参数是status
oauth_consumer_key
oauth_nonce
oauth_signature_method
- HMAC-SHA1
oauth_token
oauth_timestamp
oauth_version
- 1.0
要注意的是POST body需要在计算签名时用到,但Authorization头字段里不要写出,实现如下:
RESOURCE_URL = 'https://api.twitter.com/1/statuses/update.json'
class XMPPHandler(CommandHandler):
def text_message(self, message):
email = message.sender.split('/')[0] # sender的格式是"邮箱地址/数字ID",只取第一部分的邮箱;由于Google Talk也是用Google账号登录,所以可以通用
twitter = Twitter.get_by_key_name(email)
if not twitter:
message.reply('您的Google账号还没有与Twitter账号关联,请先去 %s 关联' % HOME_URL)
return
if len(message.body) > 140:
message.reply('您发的推超过140字,请精简后再发送')
return
msg = message.body.encode('UTF-8') # 注意要编码成UTF-8
params = get_oauth_params({
'oauth_token': twitter.token,
'status': qt(msg) # 后面的Authorization还得转义一次,相当于转义了2次
}, RESOURCE_URL, twitter.secret, '') # 这里的token和secret是从数据库里取出来的
del params['status'] # 删掉status,避免在Authorization里生成,实际上可以修改get_oauth_params的实现,自动删除这些字段
res = urlfetch.fetch(url=RESOURCE_URL, payload=urlencode({'status': msg}), headers={'Authorization': 'OAuth ' + dict2qs(params)}, method='POST') # payload需要urlencode
if res.status_code != 200:
logging.error('Access resource error.\n%s\n%s\n%s' % (res.status_code, res.headers, params))
message.reply('访问Twitter出错,可能是Google App Engine或Twitter出现了问题,请稍后再试。')
return
message.reply('发送成功')
到此本次介绍就结束了,大家可以试试我的机器人。由于GAE被墙,你可能需要代理来访问,例如google.cn:80。
向下滚动可载入更多评论,或者点这里禁止自动加载。