第六章的例子像咱們展現瞭如何使用安全cookies和tornado.web.authenticated裝飾器來實現一個簡單的用戶驗證表單。在本章中,咱們將着眼於如何對第三方服務進行身份驗證。流行的Web API,好比Facebbok和Twitter,使用OAuth協議安全驗證某人的身份,同時容許他們的用戶保持第三方應用訪問他們我的信息的控制權。Tornado提供了一些Python mix-in來幫助開發者驗證外部服務,既包括顯式地支持流行服務,也包括經過通用的OAuth支持。在本章中,咱們將探討兩個使用Tornado的auth模塊的示例應用:一個鏈接Twitter,另外一個鏈接Facebook。html
做爲一個Web應用開發者,你可能想讓用戶直接經過你的應用在Twitter上發表更新或讀取最新的Facebook狀態。大多數社交網絡和單一登陸的API爲驗證你應用中的用戶提供了一個標準的流程。Tornado的auth模塊爲OpenID、OAuth、OAuth 2.0、Twitter、FriendFeed、Google OpenID、Facebook REST API和Facebook Graph API提供了相應的類。儘管你能夠本身實現對於特定外部服務認證過程的處理,不過Tornado的auth模塊爲鏈接任何支持的服務開發應用提供了簡單的工做流程。python
這些認證方法的工做流程雖然有一些輕微的不一樣,但對於大多數而言,都使用了authorize_redirect和get_authenticated_user方法。authorize_rediect方法用來將一個未受權用戶重定向到外部服務的驗證頁面。在驗證頁面中,用戶登陸服務,並讓你的應用擁有訪問他帳戶的權限。一般狀況下,你會在用戶帶着一個臨時訪問碼返回你的應用時使用get_authenticated_user方法。調用get_authenticated_user方法會把受權跳轉過程提供的臨時憑證替換成屬於用戶的長期憑證。Twitter、Facebook、FriendFeed和Google的具體驗證類提供了他們本身的函數來使API調用它們的服務。web
關於auth模塊須要注意的一件事是它使用了Tornado的異步HTTP請求。正如咱們在第五章所看到的,異步HTTP請求容許Tornado服務器在一個掛起的請求等待傳出請求返回時處理傳入的請求。api
咱們將簡單的看下如何使用異步請求,而後在一個例子中使用它們進行深刻。每一個發起異步調用的處理方法必須在它前面加上@tornado.web.asynchronous裝飾器。安全
讓咱們來看一個使用Twitter API驗證用戶的例子。這個應用將重定向一個沒有登陸的用戶到Twitter的驗證頁面,提示用戶輸入用戶名和密碼。而後Twitter會將用戶重定向到你在Twitter應用設置頁指定的URL。服務器
首先,你必須在Twitter註冊一個新應用。若是你尚未應用,能夠從Twitter開發者網站的"Create a new application"連接開始。一旦你建立了你的Twitter應用,你將被指定一個access token和一個secret來標識你在Twitter上的應用。你須要在本節下面代碼的合適位置填充那些值。cookie
如今讓咱們看看代碼清單7-1中的代碼。網絡
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目錄下。app
<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>
<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請求返回時,執行回調函數,咱們在代碼更靠下的地方對其進行了定義。
若是oauth_token參數沒有被發現,咱們繼續測試是否以前已經看到過這個特定用戶了。
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發出請求。在這裏,你會再一次看到異步回調函數,此次是咱們稍後將要定義的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參數被做爲關鍵字參數傳遞進來。
若是上面咱們討論的狀況都沒有發生,這說明用戶是首次訪問咱們的應用(或者已經註銷或刪除了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被設置好,咱們將用戶重定向到根目錄,即咱們以前談論的發起請求到/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模板。這個模板展現了用戶的我的圖像、用戶名、詳細信息、一些關注和粉絲的統計信息以及用戶最新的狀態更新。
LogoutHandler方法只是清除了咱們爲應用用戶存儲的cookies。它渲染了logout.html模板,來給用戶提供反饋,並跳轉到Twitter驗證頁面容許其從新登陸。就是這些!
咱們剛纔看到的Twitter應用只是爲一個受權用戶展現了用戶信息,但它同時也說明了Tornado的auth模塊是如何使開發社交應用更簡單的。建立一個在Twitter上發表狀態的應用做爲一個練習留給讀者。
Facebook的這個例子在結構上和剛纔看到的Twitter的例子很是類似。Facebook有兩種不一樣的API標準,原始的REST API和Facebook Graph API。目前兩種API都被支持,但Graph API被推薦做爲開發新Facebook應用的方式。Tornado在auth模塊中支持這兩種API,但在這個例子中咱們將關注Graph API。
爲了開始這個例子,你須要登陸到Facebook的開發者網站,並建立一個新的應用。你將須要填寫應用的名稱,並證實你不是一個機器人。爲了從你本身的域名中驗證用戶,你還須要指定你應用的域名。而後點擊"Select how your app integrates with Facbook"下的"Website"。同時你須要輸入你網站的URL。要得到完整的建立Facebook應用的手冊,能夠從https://developers.facebook.com/docs/guides/web/開始。
你的應用創建好以後,你將使用基本設置頁面的應用ID和secret來鏈接Facebook Graph API。
回想一下上一節的提到的單一登陸工做流程,它將引導用戶到Facebook平臺驗證應用,Facebook將使用一個HTTP重定向將一個帶有驗證碼的用戶返回給你的服務器。一旦你接收到含有這個認證碼的請求,你必須請求用於標識API請求用戶身份的驗證令牌。
這個例子將渲染用戶的時間軸,並容許用戶經過咱們的接口更新她的Facebook狀態。讓咱們看下代碼清單7-4。
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參數做爲查詢字符串參數。這個碼是一個用於換取永久憑證的臨時令牌。若是發現了code參數,應用將發出一個Facebook Graph API請求來取得認證的用戶,並存儲她的用戶ID、全名和訪問令牌,以便在應用發起Graph API調用時標識該用戶。
存儲了這些值以後,用戶被重定向到根URL。用戶此次回到根頁面時,將取得最新Facebook消息列表。應用查看access_cookie是否被設置,並使用facebook_request方法向Graph API請求用戶訂閱。咱們把OAuth令牌傳遞給facebook_request方法,此外,這個方法還須要一個回調函數參數--在代碼清單7-4中,它是_on_facebook_user_feed方法。
<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。應用發送這個調用給Graph API來發表一個更新。
爲了發表更新,咱們再次使用了facebook_request方法。此次,除了access_token參數以外,咱們還包括了一個post_args參數,這個參數是一個成爲Graph請求post主體的參數字典。當調用成功時,咱們將用戶重定向回首頁,並請求更新後的時間軸。
正如你所看到的,Tornado的auth模塊提供的Facebook驗證類包括不少構建Facebook應用時很是有用的功能。這不只在原型設計中是一筆巨大的財富,同時也很是適合是生產中的應用。