第六章的例子像咱们展现了如何使用安全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应用时很是有用的功能。这不只在原型设计中是一笔巨大的财富,同时也很是适合是生产中的应用。