第六章的例子像我們展示了如何使用安全cookies和tornado.web.authenticated裝飾器來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的用戶驗(yàn)證表單。在本章中,我們將著眼于如何對(duì)第三方服務(wù)進(jìn)行身份驗(yàn)證。流行的Web API,比如Facebbok和Twitter,使用OAuth協(xié)議安全驗(yàn)證某人的身份,同時(shí)允許他們的用戶保持第三方應(yīng)用訪問他們個(gè)人信息的控制權(quán)。Tornado提供了一些Python mix-in來幫助開發(fā)者驗(yàn)證外部服務(wù),既包括顯式地支持流行服務(wù),也包括通過通用的OAuth支持。在本章中,我們將探討兩個(gè)使用Tornado的auth模塊的示例應(yīng)用:一個(gè)連接Twitter,另一個(gè)連接Facebook。
作為一個(gè)Web應(yīng)用開發(fā)者,你可能想讓用戶直接通過你的應(yīng)用在Twitter上發(fā)表更新或讀取最新的Facebook狀態(tài)。大多數(shù)社交網(wǎng)絡(luò)和單一登錄的API為驗(yàn)證你應(yīng)用中的用戶提供了一個(gè)標(biāo)準(zhǔn)的流程。Tornado的auth模塊為OpenID、OAuth、OAuth 2.0、Twitter、FriendFeed、Google OpenID、Facebook REST API和Facebook Graph API提供了相應(yīng)的類。盡管你可以自己實(shí)現(xiàn)對(duì)于特定外部服務(wù)認(rèn)證過程的處理,不過Tornado的auth模塊為連接任何支持的服務(wù)開發(fā)應(yīng)用提供了簡(jiǎn)單的工作流程。
這些認(rèn)證方法的工作流程雖然有一些輕微的不同,但對(duì)于大多數(shù)而言,都使用了authorize_redirect和get_authenticated_user方法。authorize_rediect方法用來將一個(gè)未授權(quán)用戶重定向到外部服務(wù)的驗(yàn)證頁面。在驗(yàn)證頁面中,用戶登錄服務(wù),并讓你的應(yīng)用擁有訪問他賬戶的權(quán)限。通常情況下,你會(huì)在用戶帶著一個(gè)臨時(shí)訪問碼返回你的應(yīng)用時(shí)使用get_authenticated_user方法。調(diào)用get_authenticated_user方法會(huì)把授權(quán)跳轉(zhuǎn)過程提供的臨時(shí)憑證替換成屬于用戶的長(zhǎng)期憑證。Twitter、Facebook、FriendFeed和Google的具體驗(yàn)證類提供了他們自己的函數(shù)來使API調(diào)用它們的服務(wù)。
關(guān)于auth模塊需要注意的一件事是它使用了Tornado的異步HTTP請(qǐng)求。正如我們?cè)诘谖逭滤吹降?,異步HTTP請(qǐng)求允許Tornado服務(wù)器在一個(gè)掛起的請(qǐng)求等待傳出請(qǐng)求返回時(shí)處理傳入的請(qǐng)求。
我們將簡(jiǎn)單的看下如何使用異步請(qǐng)求,然后在一個(gè)例子中使用它們進(jìn)行深入。每個(gè)發(fā)起異步調(diào)用的處理方法必須在它前面加上@tornado.web.asynchronous裝飾器。
讓我們來看一個(gè)使用Twitter API驗(yàn)證用戶的例子。這個(gè)應(yīng)用將重定向一個(gè)沒有登錄的用戶到Twitter的驗(yàn)證頁面,提示用戶輸入用戶名和密碼。然后Twitter會(huì)將用戶重定向到你在Twitter應(yīng)用設(shè)置頁指定的URL。
首先,你必須在Twitter注冊(cè)一個(gè)新應(yīng)用。如果你還沒有應(yīng)用,可以從Twitter開發(fā)者網(wǎng)站的"Create a new application"鏈接開始。一旦你創(chuàng)建了你的Twitter應(yīng)用,你將被指定一個(gè)access token和一個(gè)secret來標(biāo)識(shí)你在Twitter上的應(yīng)用。你需要在本節(jié)下面代碼的合適位置填充那些值。
現(xiàn)在讓我們看看代碼清單7-1中的代碼。
代碼清單7-1 查看Twitter時(shí)間軸: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的模板文件應(yīng)該被放在應(yīng)用的templates目錄下。
代碼清單7-2 Twitter時(shí)間軸: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時(shí)間軸: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>
讓我們分塊進(jìn)行分析,首先從twitter.py開始。在Application類的init方法中,你將注意到有兩個(gè)新的鍵出現(xiàn)在設(shè)置字典中:twitter_consumer_key和twitter_consumer_secret。它們需要被設(shè)置為你的Twitter應(yīng)用詳細(xì)設(shè)置頁面中列出的值。同樣,你還會(huì)注意到我們聲明了兩個(gè)處理程序:TwitterHandler和LogoutHandler。讓我們立刻看看這兩個(gè)類吧。
TwitterHandler類包含我們應(yīng)用邏輯的主要部分。有兩件事情需要立刻引起我們的注意,其一是這個(gè)類繼承自能給我們提供Twitter功能的tornado.auth.TwitterMixin類,其二是get方法使用了我們?cè)?a rel="external nofollow" target="_blank" target="_blank">第五章中討論的@tornado.web.asynchronous裝飾器?,F(xiàn)在讓我們看看第一個(gè)異步調(diào)用:
if self.get_argument('oauth_token', None):
self.get_authenticated_user(self.async_callback(self._twitter_on_auth))
return
當(dāng)一個(gè)用戶請(qǐng)求我們應(yīng)用的根目錄時(shí),我們首先檢查請(qǐng)求是否包括一個(gè)oauth_token查詢字符串參數(shù)。如果有,我們把這個(gè)請(qǐng)求看作是一個(gè)來自Twitter驗(yàn)證過程的回調(diào)。
然后,我們使用auth模塊的get_authenticated方法把給我們的臨時(shí)令牌換為用戶的訪問令牌。這個(gè)方法期待一個(gè)回調(diào)函數(shù)作為參數(shù),在這里是self._teitter_on_auth方法。當(dāng)?shù)絋witter的API請(qǐng)求返回時(shí),執(zhí)行回調(diào)函數(shù),我們?cè)诖a更靠下的地方對(duì)其進(jìn)行了定義。
如果oauth_token參數(shù)沒有被發(fā)現(xiàn),我們繼續(xù)測(cè)試是否之前已經(jīng)看到過這個(gè)特定用戶了。
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
這段代碼片段尋找我們應(yīng)用在Twitter給定一個(gè)合法用戶時(shí)設(shè)置的access_key和access_secret?cookies。如何這個(gè)值被設(shè)置了,我們就用key和secret組裝訪問令牌,然后使用self.twitter_request方法來向Twitter API的/users/show發(fā)出請(qǐng)求。在這里,你會(huì)再一次看到異步回調(diào)函數(shù),這次是我們稍后將要定義的self._twitter_on_user方法。
twitter_quest方法期待一個(gè)路徑地址作為它的第一個(gè)參數(shù),另外還有一些可選的關(guān)鍵字參數(shù),如access_token、post_args和callback。access_token參數(shù)應(yīng)該是一個(gè)字典,包括用戶OAuth訪問令牌的key鍵,和用戶OAuth secret的secret鍵。
如果API調(diào)用使用了POST方法,請(qǐng)求參數(shù)需要綁定一個(gè)傳遞post_args參數(shù)的字典。查詢字符串參數(shù)在方法調(diào)用時(shí)只需指定為一個(gè)額外的關(guān)鍵字參數(shù)。在/users/show?API調(diào)用時(shí),我們使用了HTTP?GET請(qǐng)求,所以這里不需要post_args參數(shù),而所需的user_id?API參數(shù)被作為關(guān)鍵字參數(shù)傳遞進(jìn)來。
如果上面我們討論的情況都沒有發(fā)生,這說明用戶是首次訪問我們的應(yīng)用(或者已經(jīng)注銷或刪除了cookies),此時(shí)我們想將其重定向到Twitter的驗(yàn)證頁面。調(diào)用self.authorize_redirect()來完成這項(xiàng)工作。
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請(qǐng)求的回調(diào)方法非常的直接。_twitter_on_auth使用一個(gè)user參數(shù)進(jìn)行調(diào)用,這個(gè)參數(shù)是已授權(quán)用戶的用戶數(shù)據(jù)字典。我們的方法實(shí)現(xiàn)只需要驗(yàn)證我們接收到的用戶是否合法,并設(shè)置應(yīng)有的cookies。一旦cookies被設(shè)置好,我們將用戶重定向到根目錄,即我們之前談?wù)摰陌l(fā)起請(qǐng)求到/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方法是我們?cè)趖witter_request方法中指定調(diào)用的回調(diào)函數(shù)。當(dāng)Twitter響應(yīng)用戶的個(gè)人信息時(shí),我們的回調(diào)函數(shù)使用響應(yīng)的數(shù)據(jù)渲染home.html模板。這個(gè)模板展示了用戶的個(gè)人圖像、用戶名、詳細(xì)信息、一些關(guān)注和粉絲的統(tǒng)計(jì)信息以及用戶最新的狀態(tài)更新。
LogoutHandler方法只是清除了我們?yōu)閼?yīng)用用戶存儲(chǔ)的cookies。它渲染了logout.html模板,來給用戶提供反饋,并跳轉(zhuǎn)到Twitter驗(yàn)證頁面允許其重新登錄。就是這些!
我們剛才看到的Twitter應(yīng)用只是為一個(gè)授權(quán)用戶展示了用戶信息,但它同時(shí)也說明了Tornado的auth模塊是如何使開發(fā)社交應(yīng)用更簡(jiǎn)單的。創(chuàng)建一個(gè)在Twitter上發(fā)表狀態(tài)的應(yīng)用作為一個(gè)練習(xí)留給讀者。
Facebook的這個(gè)例子在結(jié)構(gòu)上和剛才看到的Twitter的例子非常相似。Facebook有兩種不同的API標(biāo)準(zhǔn),原始的REST API和Facebook Graph API。目前兩種API都被支持,但Graph API被推薦作為開發(fā)新Facebook應(yīng)用的方式。Tornado在auth模塊中支持這兩種API,但在這個(gè)例子中我們將關(guān)注Graph API。
為了開始這個(gè)例子,你需要登錄到Facebook的開發(fā)者網(wǎng)站,并創(chuàng)建一個(gè)新的應(yīng)用。你將需要填寫應(yīng)用的名稱,并證明你不是一個(gè)機(jī)器人。為了從你自己的域名中驗(yàn)證用戶,你還需要指定你應(yīng)用的域名。然后點(diǎn)擊"Select how your app integrates with Facbook"下的"Website"。同時(shí)你需要輸入你網(wǎng)站的URL。要獲得完整的創(chuàng)建Facebook應(yīng)用的手冊(cè),可以從https://developers.facebook.com/docs/guides/web/開始。
你的應(yīng)用建立好之后,你將使用基本設(shè)置頁面的應(yīng)用ID和secret來連接Facebook Graph API。
回想一下上一節(jié)的提到的單一登錄工作流程,它將引導(dǎo)用戶到Facebook平臺(tái)驗(yàn)證應(yīng)用,F(xiàn)acebook將使用一個(gè)HTTP重定向?qū)⒁粋€(gè)帶有驗(yàn)證碼的用戶返回給你的服務(wù)器。一旦你接收到含有這個(gè)認(rèn)證碼的請(qǐng)求,你必須請(qǐng)求用于標(biāo)識(shí)API請(qǐng)求用戶身份的驗(yàn)證令牌。
這個(gè)例子將渲染用戶的時(shí)間軸,并允許用戶通過我們的接口更新她的Facebook狀態(tài)。讓我們看下代碼清單7-4。
代碼清單7-4 Facebook驗(yàn)證: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()
我們將按照訪客與應(yīng)用交互的順序來講解這些處理。當(dāng)請(qǐng)求根URL時(shí),F(xiàn)eedHandler將尋找access_token?cookie。如果這個(gè)cookie不存在,用戶會(huì)被重定向到/auth/login?URL。
登錄頁面使用了authorize_redirect方法來講用戶重定向到Facebook的驗(yàn)證對(duì)話框,如果需要的話,用戶在這里登錄Facebook,審查應(yīng)用程序請(qǐng)求的權(quán)限,并批準(zhǔn)應(yīng)用。在點(diǎn)擊"Approve"之后,她將被跳轉(zhuǎn)回應(yīng)用在authorize_redirect調(diào)用中redirect_uri指定的URL。
當(dāng)從Facebook驗(yàn)證頁面返回后,到/auth/login的請(qǐng)求將包括一個(gè)code參數(shù)作為查詢字符串參數(shù)。這個(gè)碼是一個(gè)用于換取永久憑證的臨時(shí)令牌。如果發(fā)現(xiàn)了code參數(shù),應(yīng)用將發(fā)出一個(gè)Facebook Graph API請(qǐng)求來取得認(rèn)證的用戶,并存儲(chǔ)她的用戶ID、全名和訪問令牌,以便在應(yīng)用發(fā)起Graph API調(diào)用時(shí)標(biāo)識(shí)該用戶。
存儲(chǔ)了這些值之后,用戶被重定向到根URL。用戶這次回到根頁面時(shí),將取得最新Facebook消息列表。應(yīng)用查看access_cookie是否被設(shè)置,并使用facebook_request方法向Graph API請(qǐng)求用戶訂閱。我們把OAuth令牌傳遞給facebook_request方法,此外,這個(gè)方法還需要一個(gè)回調(diào)函數(shù)參數(shù)--在代碼清單7-4中,它是_on_facebook_user_feed方法。
代碼清單7-5 Facebook驗(yàn)證: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>
當(dāng)包含來自Facebook的用戶訂閱消息的響應(yīng)的回調(diào)函數(shù)被調(diào)用時(shí),應(yīng)用渲染home.html模板,其中使用了FeedListItem這個(gè)UI模塊來渲染列表中的每個(gè)條目。在模板開始處,我們渲染了一個(gè)表單,可以用message參數(shù)post到我們服務(wù)器的/resource。應(yīng)用發(fā)送這個(gè)調(diào)用給Graph API來發(fā)表一個(gè)更新。
為了發(fā)表更新,我們?cè)俅问褂昧薴acebook_request方法。這次,除了access_token參數(shù)之外,我們還包括了一個(gè)post_args參數(shù),這個(gè)參數(shù)是一個(gè)成為Graph請(qǐng)求post主體的參數(shù)字典。當(dāng)調(diào)用成功時(shí),我們將用戶重定向回首頁,并請(qǐng)求更新后的時(shí)間軸。
正如你所看到的,Tornado的auth模塊提供的Facebook驗(yàn)證類包括很多構(gòu)建Facebook應(yīng)用時(shí)非常有用的功能。這不僅在原型設(shè)計(jì)中是一筆巨大的財(cái)富,同時(shí)也非常適合是生產(chǎn)中的應(yīng)用。
更多建議: