第六章的例子像我們展示了如何使用安全cookies和tornado.web.authenticated裝飾器來實現一個簡單的用戶驗證表單。在本章中,我們將著眼于如何對第三方服務進行身份驗證。流行的Web API,比如Facebbok和Twitter,使用OAuth協議安全驗證某人的身份,同時允許他們的用戶保持第三方應用訪問他們個人信息的控制權。Tornado提供了一些Python mix-in來幫助開發(fā)者驗證外部服務,既包括顯式地支持流行服務,也包括通過通用的OAuth支持。在本章中,我們將探討兩個使用Tornado的auth模塊的示例應用:一個連接Twitter,另一個連接Facebook。
作為一個Web應用開發(fā)者,你可能想讓用戶直接通過你的應用在Twitter上發(fā)表更新或讀取最新的Facebook狀態(tài)。大多數社交網絡和單一登錄的API為驗證你應用中的用戶提供了一個標準的流程。Tornado的auth模塊為OpenID、OAuth、OAuth 2.0、Twitter、FriendFeed、Google OpenID、Facebook REST API和Facebook Graph API提供了相應的類。盡管你可以自己實現對于特定外部服務認證過程的處理,不過Tornado的auth模塊為連接任何支持的服務開發(fā)應用提供了簡單的工作流程。
這些認證方法的工作流程雖然有一些輕微的不同,但對于大多數而言,都使用了authorize_redirect和get_authenticated_user方法。authorize_rediect方法用來將一個未授權用戶重定向到外部服務的驗證頁面。在驗證頁面中,用戶登錄服務,并讓你的應用擁有訪問他賬戶的權限。通常情況下,你會在用戶帶著一個臨時訪問碼返回你的應用時使用get_authenticated_user方法。調用get_authenticated_user方法會把授權跳轉過程提供的臨時憑證替換成屬于用戶的長期憑證。Twitter、Facebook、FriendFeed和Google的具體驗證類提供了他們自己的函數來使API調用它們的服務。
關于auth模塊需要注意的一件事是它使用了Tornado的異步HTTP請求。正如我們在第五章所看到的,異步HTTP請求允許Tornado服務器在一個掛起的請求等待傳出請求返回時處理傳入的請求。
我們將簡單的看下如何使用異步請求,然后在一個例子中使用它們進行深入。每個發(fā)起異步調用的處理方法必須在它前面加上@tornado.web.asynchronous裝飾器。
讓我們來看一個使用Twitter API驗證用戶的例子。這個應用將重定向一個沒有登錄的用戶到Twitter的驗證頁面,提示用戶輸入用戶名和密碼。然后Twitter會將用戶重定向到你在Twitter應用設置頁指定的URL。
首先,你必須在Twitter注冊一個新應用。如果你還沒有應用,可以從Twitter開發(fā)者網站的"Create a new application"鏈接開始。一旦你創(chuàng)建了你的Twitter應用,你將被指定一個access token和一個secret來標識你在Twitter上的應用。你需要在本節(jié)下面代碼的合適位置填充那些值。
現在讓我們看看代碼清單7-1中的代碼。
代碼清單7-1 查看Twitter時間軸:twitter.py
import tornado.web
import tornado.httpserver
import tornado.auth
import tornado.ioloop
class TwitterHandler(tornado.web.RequestHandler, tornado.auth.TwitterMixin):
@tornado.web.asynchronous
def get(self):
oAuthToken = self.get_secure_cookie('oauth_token')
oAuthSecret = self.get_secure_cookie('oauth_secret')
userID = self.get_secure_cookie('user_id')
if self.get_argument('oauth_token', None):
self.get_authenticated_user(self.async_callback(self._twitter_on_auth))
return
elif oAuthToken and oAuthSecret:
accessToken = {
'key': oAuthToken,
'secret': oAuthSecret
}
self.twitter_request('/users/show',
access_token=accessToken,
user_id=userID,
callback=self.async_callback(self._twitter_on_user)
)
return
self.authorize_redirect()
def _twitter_on_auth(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, 'Twitter authentication failed')
self.set_secure_cookie('user_id', str(user['id']))
self.set_secure_cookie('oauth_token', user['access_token']['key'])
self.set_secure_cookie('oauth_secret', user['access_token']['secret'])
self.redirect('/')
def _twitter_on_user(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, "Couldn't retrieve user information")
self.render('home.html', user=user)
class LogoutHandler(tornado.web.RequestHandler):
def get(self):
self.clear_all_cookies()
self.render('logout.html')
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/', TwitterHandler),
(r'/logout', LogoutHandler)
]
settings = {
'twitter_consumer_key': 'cWc3 ... d3yg',
'twitter_consumer_secret': 'nEoT ... cCXB4',
'cookie_secret': 'NTliOTY5NzJkYTVlMTU0OTAwMTdlNjgzMTA5M2U3OGQ5NDIxZmU3Mg==',
'template_path': 'templates',
}
tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__':
app = Application()
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop.instance().start()
代碼清單7-2和7-3的模板文件應該被放在應用的templates目錄下。
代碼清單7-2 Twitter時間軸:home.html
<html>
<head>
<title>{{ user['name'] }} ({{ user['screen_name'] }}) on Twitter</title>
</head>
<body>
<div>
<a href="/logout">Sign out</a>
</div>
<div>
<img src="{{ user['profile_image_url'] }}" style="float:left" />
<h2>About @{{ user['screen_name'] }}</h2>
<p style="clear:both"><em>{{ user['description'] }}</em></p>
</div>
<div>
<ul>
<li>{{ user['statuses_count'] }} tweets.</li>
<li>{{ user['followers_count'] }} followers.</li>
<li>Following {{ user['friends_count'] }} users.</li>
</ul>
</div>
{% if 'status' in user %}
<hr />
<div>
<p>
<strong>{{ user['screen_name'] }}</strong>
<em>on {{ ' '.join(user['status']['created_at'].split()[:2]) }}
at {{ user['status']['created_at'].split()[3] }}</em>
</p>
<p>{{ user['status']['text'] }}</p>
</div>
{% end %}
</body>
</html>
代碼清單7-3 Twitter時間軸:logout.html
<html>
<head>
<title>Tornadoes on Twitter</title>
</head>
<body>
<div>
<h2>You have successfully signed out.</h2>
<a href="/">Sign in</a>
</div>
</body>
</html>
讓我們分塊進行分析,首先從twitter.py開始。在Application類的init方法中,你將注意到有兩個新的鍵出現在設置字典中:twitter_consumer_key和twitter_consumer_secret。它們需要被設置為你的Twitter應用詳細設置頁面中列出的值。同樣,你還會注意到我們聲明了兩個處理程序:TwitterHandler和LogoutHandler。讓我們立刻看看這兩個類吧。
TwitterHandler類包含我們應用邏輯的主要部分。有兩件事情需要立刻引起我們的注意,其一是這個類繼承自能給我們提供Twitter功能的tornado.auth.TwitterMixin類,其二是get方法使用了我們在第五章中討論的@tornado.web.asynchronous裝飾器。現在讓我們看看第一個異步調用:
if self.get_argument('oauth_token', None):
self.get_authenticated_user(self.async_callback(self._twitter_on_auth))
return
當一個用戶請求我們應用的根目錄時,我們首先檢查請求是否包括一個oauth_token查詢字符串參數。如果有,我們把這個請求看作是一個來自Twitter驗證過程的回調。
然后,我們使用auth模塊的get_authenticated方法把給我們的臨時令牌換為用戶的訪問令牌。這個方法期待一個回調函數作為參數,在這里是self._teitter_on_auth方法。當到Twitter的API請求返回時,執(zhí)行回調函數,我們在代碼更靠下的地方對其進行了定義。
如果oauth_token參數沒有被發(fā)現,我們繼續(xù)測試是否之前已經看到過這個特定用戶了。
elif oAuthToken and oAuthSecret:
accessToken = {
'key': oAuthToken,
'secret': oAuthSecret
}
self.twitter_request('/users/show',
access_token=accessToken,
user_id=userID,
callback=self.async_callback(self._twitter_on_user)
)
return
這段代碼片段尋找我們應用在Twitter給定一個合法用戶時設置的access_key和access_secret?cookies。如何這個值被設置了,我們就用key和secret組裝訪問令牌,然后使用self.twitter_request方法來向Twitter API的/users/show發(fā)出請求。在這里,你會再一次看到異步回調函數,這次是我們稍后將要定義的self._twitter_on_user方法。
twitter_quest方法期待一個路徑地址作為它的第一個參數,另外還有一些可選的關鍵字參數,如access_token、post_args和callback。access_token參數應該是一個字典,包括用戶OAuth訪問令牌的key鍵,和用戶OAuth secret的secret鍵。
如果API調用使用了POST方法,請求參數需要綁定一個傳遞post_args參數的字典。查詢字符串參數在方法調用時只需指定為一個額外的關鍵字參數。在/users/show?API調用時,我們使用了HTTP?GET請求,所以這里不需要post_args參數,而所需的user_id?API參數被作為關鍵字參數傳遞進來。
如果上面我們討論的情況都沒有發(fā)生,這說明用戶是首次訪問我們的應用(或者已經注銷或刪除了cookies),此時我們想將其重定向到Twitter的驗證頁面。調用self.authorize_redirect()來完成這項工作。
def _twitter_on_auth(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, 'Twitter authentication failed')
self.set_secure_cookie('user_id', str(user['id']))
self.set_secure_cookie('oauth_token', user['access_token']['key'])
self.set_secure_cookie('oauth_secret', user['access_token']['secret'])
self.redirect('/')
我們的Twitter請求的回調方法非常的直接。_twitter_on_auth使用一個user參數進行調用,這個參數是已授權用戶的用戶數據字典。我們的方法實現只需要驗證我們接收到的用戶是否合法,并設置應有的cookies。一旦cookies被設置好,我們將用戶重定向到根目錄,即我們之前談論的發(fā)起請求到/users/show?API方法。
def _twitter_on_user(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, "Couldn't retrieve user information")
self.render('home.html', user=user)
_twitter_on_user方法是我們在twitter_request方法中指定調用的回調函數。當Twitter響應用戶的個人信息時,我們的回調函數使用響應的數據渲染home.html模板。這個模板展示了用戶的個人圖像、用戶名、詳細信息、一些關注和粉絲的統(tǒng)計信息以及用戶最新的狀態(tài)更新。
LogoutHandler方法只是清除了我們?yōu)閼糜脩舸鎯Φ腸ookies。它渲染了logout.html模板,來給用戶提供反饋,并跳轉到Twitter驗證頁面允許其重新登錄。就是這些!
我們剛才看到的Twitter應用只是為一個授權用戶展示了用戶信息,但它同時也說明了Tornado的auth模塊是如何使開發(fā)社交應用更簡單的。創(chuàng)建一個在Twitter上發(fā)表狀態(tài)的應用作為一個練習留給讀者。
Facebook的這個例子在結構上和剛才看到的Twitter的例子非常相似。Facebook有兩種不同的API標準,原始的REST API和Facebook Graph API。目前兩種API都被支持,但Graph API被推薦作為開發(fā)新Facebook應用的方式。Tornado在auth模塊中支持這兩種API,但在這個例子中我們將關注Graph API。
為了開始這個例子,你需要登錄到Facebook的開發(fā)者網站,并創(chuàng)建一個新的應用。你將需要填寫應用的名稱,并證明你不是一個機器人。為了從你自己的域名中驗證用戶,你還需要指定你應用的域名。然后點擊"Select how your app integrates with Facbook"下的"Website"。同時你需要輸入你網站的URL。要獲得完整的創(chuàng)建Facebook應用的手冊,可以從https://developers.facebook.com/docs/guides/web/開始。
你的應用建立好之后,你將使用基本設置頁面的應用ID和secret來連接Facebook Graph API。
回想一下上一節(jié)的提到的單一登錄工作流程,它將引導用戶到Facebook平臺驗證應用,Facebook將使用一個HTTP重定向將一個帶有驗證碼的用戶返回給你的服務器。一旦你接收到含有這個認證碼的請求,你必須請求用于標識API請求用戶身份的驗證令牌。
這個例子將渲染用戶的時間軸,并允許用戶通過我們的接口更新她的Facebook狀態(tài)。讓我們看下代碼清單7-4。
代碼清單7-4 Facebook驗證:facebook.py
import tornado.web
import tornado.httpserver
import tornado.auth
import tornado.ioloop
import tornado.options
from datetime import datetime
class FeedHandler(tornado.web.RequestHandler, tornado.auth.FacebookGraphMixin):
@tornado.web.asynchronous
def get(self):
accessToken = self.get_secure_cookie('access_token')
if not accessToken:
self.redirect('/auth/login')
return
self.facebook_request(
"/me/feed",
access_token=accessToken,
callback=self.async_callback(self._on_facebook_user_feed))
def _on_facebook_user_feed(self, response):
name = self.get_secure_cookie('user_name')
self.render('home.html', feed=response['data'] if response else [], name=name)
@tornado.web.asynchronous
def post(self):
accessToken = self.get_secure_cookie('access_token')
if not accessToken:
self.redirect('/auth/login')
userInput = self.get_argument('message')
self.facebook_request(
"/me/feed",
post_args={'message': userInput},
access_token=accessToken,
callback=self.async_callback(self._on_facebook_post_status))
def _on_facebook_post_status(self, response):
self.redirect('/')
class LoginHandler(tornado.web.RequestHandler, tornado.auth.FacebookGraphMixin):
@tornado.web.asynchronous
def get(self):
userID = self.get_secure_cookie('user_id')
if self.get_argument('code', None):
self.get_authenticated_user(
redirect_uri='http://example.com/auth/login',
client_id=self.settings['facebook_api_key'],
client_secret=self.settings['facebook_secret'],
code=self.get_argument('code'),
callback=self.async_callback(self._on_facebook_login))
return
elif self.get_secure_cookie('access_token'):
self.redirect('/')
return
self.authorize_redirect(
redirect_uri='http://example.com/auth/login',
client_id=self.settings['facebook_api_key'],
extra_params={'scope': 'read_stream,publish_stream'}
)
def _on_facebook_login(self, user):
if not user:
self.clear_all_cookies()
raise tornado.web.HTTPError(500, 'Facebook authentication failed')
self.set_secure_cookie('user_id', str(user['id']))
self.set_secure_cookie('user_name', str(user['name']))
self.set_secure_cookie('access_token', str(user['access_token']))
self.redirect('/')
class LogoutHandler(tornado.web.RequestHandler):
def get(self):
self.clear_all_cookies()
self.render('logout.html')
class FeedListItem(tornado.web.UIModule):
def render(self, statusItem):
dateFormatter = lambda x: datetime.
strptime(x,'%Y-%m-%dT%H:%M:%S+0000').strftime('%c')
return self.render_string('entry.html', item=statusItem, format=dateFormatter)
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/', FeedHandler),
(r'/auth/login', LoginHandler),
(r'/auth/logout', LogoutHandler)
]
settings = {
'facebook_api_key': '2040 ... 8759',
'facebook_secret': 'eae0 ... 2f08',
'cookie_secret': 'NTliOTY5NzJkYTVlMTU0OTAwMTdlNjgzMTA5M2U3OGQ5NDIxZmU3Mg==',
'template_path': 'templates',
'ui_modules': {'FeedListItem': FeedListItem}
}
tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__':
tornado.options.parse_command_line()
app = Application()
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop.instance().start()
我們將按照訪客與應用交互的順序來講解這些處理。當請求根URL時,FeedHandler將尋找access_token?cookie。如果這個cookie不存在,用戶會被重定向到/auth/login?URL。
登錄頁面使用了authorize_redirect方法來講用戶重定向到Facebook的驗證對話框,如果需要的話,用戶在這里登錄Facebook,審查應用程序請求的權限,并批準應用。在點擊"Approve"之后,她將被跳轉回應用在authorize_redirect調用中redirect_uri指定的URL。
當從Facebook驗證頁面返回后,到/auth/login的請求將包括一個code參數作為查詢字符串參數。這個碼是一個用于換取永久憑證的臨時令牌。如果發(fā)現了code參數,應用將發(fā)出一個Facebook Graph API請求來取得認證的用戶,并存儲她的用戶ID、全名和訪問令牌,以便在應用發(fā)起Graph API調用時標識該用戶。
存儲了這些值之后,用戶被重定向到根URL。用戶這次回到根頁面時,將取得最新Facebook消息列表。應用查看access_cookie是否被設置,并使用facebook_request方法向Graph API請求用戶訂閱。我們把OAuth令牌傳遞給facebook_request方法,此外,這個方法還需要一個回調函數參數--在代碼清單7-4中,它是_on_facebook_user_feed方法。
代碼清單7-5 Facebook驗證:home.html
<html>
<head>
<title>{{ name }} on Facebook</title>
</head>
<body>
<div>
<a href="/auth/logout">Sign out</a>
<h1>{{ name }}</h1>
</div>
<div>
<form action="/facebook/" method="POST">
<textarea rows="3" cols="50" name="message"></textarea>
<input type="submit" value="Update Status" />
</form>
</div>
<hr />
{% for item in feed %}
{% module FeedListItem(item) %}
{% end %}
</body>
</html>
當包含來自Facebook的用戶訂閱消息的響應的回調函數被調用時,應用渲染home.html模板,其中使用了FeedListItem這個UI模塊來渲染列表中的每個條目。在模板開始處,我們渲染了一個表單,可以用message參數post到我們服務器的/resource。應用發(fā)送這個調用給Graph API來發(fā)表一個更新。
為了發(fā)表更新,我們再次使用了facebook_request方法。這次,除了access_token參數之外,我們還包括了一個post_args參數,這個參數是一個成為Graph請求post主體的參數字典。當調用成功時,我們將用戶重定向回首頁,并請求更新后的時間軸。
正如你所看到的,Tornado的auth模塊提供的Facebook驗證類包括很多構建Facebook應用時非常有用的功能。這不僅在原型設計中是一筆巨大的財富,同時也非常適合是生產中的應用。
更多建議: