如何使用Python从客户端OAuth流中程序化获取access_token?
这个问题最初是在 StackApps 上提的,但可能更像是编程方面的问题,而不是认证问题,所以我觉得这里更合适。
我正在用Python为StackOverflow开发一个桌面通知工具,目的是提醒用户收件箱的新消息。
我正在编写的脚本首先会让用户登录StackExchange,然后请求应用程序的授权。假设用户通过网页浏览器完成了授权,那么这个应用就可以用认证信息向API发送请求,因此它需要一个特定于用户的访问令牌。这个过程是通过以下网址完成的:https://stackexchange.com/oauth/dialog?client_id=54&scope=read_inbox&redirect_uri=https://stackexchange.com/oauth/login_success
。
在通过网页浏览器请求授权时,会发生重定向,并且在#
后面会返回一个访问代码。但是,当我用Python(urllib2)请求同样的网址时,响应中没有返回哈希或密钥。
为什么我的urllib2请求和在Firefox或W3m中发出的相同请求处理方式不同?我该怎么做才能通过编程方式模拟这个请求并获取access_token
?
这是我的脚本(它还在实验阶段),请记住:它假设用户已经授权了这个应用。
#!/usr/bin/env python
import urllib
import urllib2
import cookielib
from BeautifulSoup import BeautifulSoup
from getpass import getpass
# Define URLs
parameters = [ 'client_id=54',
'scope=read_inbox',
'redirect_uri=https://stackexchange.com/oauth/login_success'
]
oauth_url = 'https://stackexchange.com/oauth/dialog?' + '&'.join(parameters)
login_url = 'https://openid.stackexchange.com/account/login'
submit_url = 'https://openid.stackexchange.com/account/login/submit'
authentication_url = 'http://stackexchange.com/users/authenticate?openid_identifier='
# Set counter for requests:
counter = 0
# Build opener
jar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(jar))
def authenticate(username='', password=''):
'''
Authenticates to StackExchange using user-provided username and password
'''
# Build up headers
user_agent = 'Mozilla/5.0 (Ubuntu; X11; Linux i686; rv:8.0) Gecko/20100101 Firefox/8.0'
headers = {'User-Agent' : user_agent}
# Set Data to None
data = None
# 1. Build up URL request with headers and data
request = urllib2.Request(login_url, data, headers)
response = opener.open(request)
# Build up POST data for authentication
html = response.read()
fkey = BeautifulSoup(html).findAll(attrs={'name' : 'fkey'})[0].get('value').encode()
values = {'email' : username,
'password' : password,
'fkey' : fkey}
data = urllib.urlencode(values)
# 2. Build up URL for authentication
request = urllib2.Request(submit_url, data, headers)
response = opener.open(request)
# Check if logged in
if response.url == 'https://openid.stackexchange.com/user':
print ' Logged in! :) '
else:
print ' Login failed! :( '
# Find user ID URL
html = response.read()
id_url = BeautifulSoup(html).findAll('code')[0].text.split('"')[-2].encode()
# 3. Build up URL for OpenID authentication
data = None
url = authentication_url + urllib.quote_plus(id_url)
request = urllib2.Request(url, data, headers)
response = opener.open(request)
# 4. Build up URL request with headers and data
request = urllib2.Request(oauth_url, data, headers)
response = opener.open(request)
if '#' in response.url:
print 'Access code provided in URL.'
else:
print 'No access code provided in URL.'
if __name__ == '__main__':
username = raw_input('Enter your username: ')
password = getpass('Enter your password: ')
authenticate(username, password)
针对下面的评论:
在Firefox中,Tamper Data请求上述网址(在代码中作为oauth_url
)时,使用了以下请求头:
Host=stackexchange.com
User-Agent=Mozilla/5.0 (Ubuntu; X11; Linux i686; rv:9.0.1) Gecko/20100101 Firefox/9.0.1
Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language=en-us,en;q=0.5
Accept-Encoding=gzip, deflate
Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection=keep-alive
Cookie=m=2; __qca=P0-556807911-1326066608353; __utma=27693923.1085914018.1326066609.1326066609.1326066609.1; __utmb=27693923.3.10.1326066609; __utmc=27693923; __utmz=27693923.1326066609.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); gauthed=1; ASP.NET_SessionId=nt25smfr2x1nwhr1ecmd4ok0; se-usr=t=z0FHKC6Am06B&s=pblSq0x3B0lC
在urllib2请求中,头部只提供了用户代理值。虽然没有明确传递cookie,但在请求时se-usr
已经在cookie jar中可用。
响应头部首先是重定向:
Status=Found - 302
Server=nginx/0.7.65
Date=Sun, 08 Jan 2012 23:51:12 GMT
Content-Type=text/html; charset=utf-8
Connection=keep-alive
Cache-Control=private
Location=https://stackexchange.com/oauth/login_success#access_token=OYn42gZ6r3WoEX677A3BoA))&expires=86400
Set-Cookie=se-usr=t=kkdavslJe0iq&s=pblSq0x3B0lC; expires=Sun, 08-Jul-2012 23:51:12 GMT; path=/; HttpOnly
Content-Length=218
然后会通过另一个请求使用来自该头部的新se-usr
值进行重定向。
我不知道如何在urllib2中捕获302状态码,它会自动处理(这很好)。不过,如果能看到位置头部中提供的访问令牌是否可用,那就太好了。
在最后的响应头中没有什么特别的,Firefox和Urllib返回的内容类似:
Server: nginx/0.7.65
Date: Sun, 08 Jan 2012 23:48:16 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Cache-Control: private
Content-Length: 5664
我希望我没有提供任何机密信息,如果有的话请告诉我 :D
1 个回答
这个令牌没有出现是因为urllib2处理重定向的方式。我对具体细节不太了解,所以就不详细说了。
解决办法是,在urllib2处理重定向之前,先捕捉到302状态码。这可以通过创建一个urllib2.HTTPRedirectHandler
的子类来实现,这样就能获取到带有哈希标签和令牌的重定向。下面是一个简单的子类示例:
class MyHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
print "Going through 302:\n"
print headers
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
在请求头中,location
属性会提供完整的重定向URL,也就是说,包括哈希标签和令牌:
输出示例:
...
Going through 302:
Server: nginx/0.7.65
Date: Mon, 09 Jan 2012 20:20:11 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Cache-Control: private
Location: https://stackexchange.com/oauth/login_success#access_token=K4zKd*HkKw5Opx(a8t12FA))&expires=86400
Content-Length: 218
...
关于如何使用urllib2捕捉重定向的更多信息,可以在StackOverflow上找到(当然啦)。