第七章:外部服務認證

2018-02-24 15:49 更新

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

7.1 Tornado的auth模塊

作為一個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ā)應用提供了簡單的工作流程。

7.1.1 認證流程

這些認證方法的工作流程雖然有一些輕微的不同,但對于大多數而言,都使用了authorize_redirect和get_authenticated_user方法。authorize_rediect方法用來將一個未授權用戶重定向到外部服務的驗證頁面。在驗證頁面中,用戶登錄服務,并讓你的應用擁有訪問他賬戶的權限。通常情況下,你會在用戶帶著一個臨時訪問碼返回你的應用時使用get_authenticated_user方法。調用get_authenticated_user方法會把授權跳轉過程提供的臨時憑證替換成屬于用戶的長期憑證。Twitter、Facebook、FriendFeed和Google的具體驗證類提供了他們自己的函數來使API調用它們的服務。

7.1.2 異步請求

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

我們將簡單的看下如何使用異步請求,然后在一個例子中使用它們進行深入。每個發(fā)起異步調用的處理方法必須在它前面加上@tornado.web.asynchronous裝飾器。

7.2 示例:登錄Twitter

讓我們來看一個使用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)的應用作為一個練習留給讀者。

7.3 示例:Facebook認證和Graph API

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應用時非常有用的功能。這不僅在原型設計中是一筆巨大的財富,同時也非常適合是生產中的應用。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號