第七章:外部服務認證

第六章的例子像咱們展現瞭如何使用安全cookies和tornado.web.authenticated裝飾器來實現一個簡單的用戶驗證表單。在本章中,咱們將着眼於如何對第三方服務進行身份驗證。流行的Web API,好比Facebbok和Twitter,使用OAuth協議安全驗證某人的身份,同時容許他們的用戶保持第三方應用訪問他們我的信息的控制權。Tornado提供了一些Python mix-in來幫助開發者驗證外部服務,既包括顯式地支持流行服務,也包括經過通用的OAuth支持。在本章中,咱們將探討兩個使用Tornado的auth模塊的示例應用:一個鏈接Twitter,另外一個鏈接Facebook。html

7.1 Tornado的auth模塊

做爲一個Web應用開發者,你可能想讓用戶直接經過你的應用在Twitter上發表更新或讀取最新的Facebook狀態。大多數社交網絡和單一登陸的API爲驗證你應用中的用戶提供了一個標準的流程。Tornado的auth模塊爲OpenID、OAuth、OAuth 2.0、Twitter、FriendFeed、Google OpenID、Facebook REST API和Facebook Graph API提供了相應的類。儘管你能夠本身實現對於特定外部服務認證過程的處理,不過Tornado的auth模塊爲鏈接任何支持的服務開發應用提供了簡單的工做流程。python

7.1.1 認證流程

這些認證方法的工做流程雖然有一些輕微的不一樣,但對於大多數而言,都使用了authorize_redirectget_authenticated_user方法。authorize_rediect方法用來將一個未受權用戶重定向到外部服務的驗證頁面。在驗證頁面中,用戶登陸服務,並讓你的應用擁有訪問他帳戶的權限。一般狀況下,你會在用戶帶着一個臨時訪問碼返回你的應用時使用get_authenticated_user方法。調用get_authenticated_user方法會把受權跳轉過程提供的臨時憑證替換成屬於用戶的長期憑證。Twitter、Facebook、FriendFeed和Google的具體驗證類提供了他們本身的函數來使API調用它們的服務。web

7.1.2 異步請求

關於auth模塊須要注意的一件事是它使用了Tornado的異步HTTP請求。正如咱們在第五章所看到的,異步HTTP請求容許Tornado服務器在一個掛起的請求等待傳出請求返回時處理傳入的請求。api

咱們將簡單的看下如何使用異步請求,而後在一個例子中使用它們進行深刻。每一個發起異步調用的處理方法必須在它前面加上@tornado.web.asynchronous裝飾器。安全

7.2 示例:登陸Twitter

讓咱們來看一個使用Twitter API驗證用戶的例子。這個應用將重定向一個沒有登陸的用戶到Twitter的驗證頁面,提示用戶輸入用戶名和密碼。而後Twitter會將用戶重定向到你在Twitter應用設置頁指定的URL。服務器

首先,你必須在Twitter註冊一個新應用。若是你尚未應用,能夠從Twitter開發者網站的"Create a new application"連接開始。一旦你建立了你的Twitter應用,你將被指定一個access token和一個secret來標識你在Twitter上的應用。你須要在本節下面代碼的合適位置填充那些值。cookie

如今讓咱們看看代碼清單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目錄下。app

代碼清單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_keytwitter_consumer_secret。它們須要被設置爲你的Twitter應用詳細設置頁面中列出的值。一樣,你還會注意到咱們聲明瞭兩個處理程序:TwitterHandlerLogoutHandler。讓咱們馬上看看這兩個類吧。異步

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_keyaccess_secret cookies。如何這個值被設置了,咱們就用key和secret組裝訪問令牌,而後使用self.twitter_request方法來向Twitter API的/users/show發出請求。在這裏,你會再一次看到異步回調函數,此次是咱們稍後將要定義的self._twitter_on_user方法。

twitter_quest方法期待一個路徑地址做爲它的第一個參數,另外還有一些可選的關鍵字參數,如access_tokenpost_argscallbackaccess_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上發表狀態的應用做爲一個練習留給讀者。

7.3 示例:Facebook認證和Graph API

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。

代碼清單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參數做爲查詢字符串參數。這個碼是一個用於換取永久憑證的臨時令牌。若是發現了code參數,應用將發出一個Facebook Graph API請求來取得認證的用戶,並存儲她的用戶ID、全名和訪問令牌,以便在應用發起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。應用發送這個調用給Graph API來發表一個更新。

爲了發表更新,咱們再次使用了facebook_request方法。此次,除了access_token參數以外,咱們還包括了一個post_args參數,這個參數是一個成爲Graph請求post主體的參數字典。當調用成功時,咱們將用戶重定向回首頁,並請求更新後的時間軸。

正如你所看到的,Tornado的auth模塊提供的Facebook驗證類包括不少構建Facebook應用時很是有用的功能。這不只在原型設計中是一筆巨大的財富,同時也很是適合是生產中的應用。

相關文章
相關標籤/搜索