2012-02-29 71 views
51

任何人都可以指出我在App Engine上使用OAuth2和Flask以及而不是对Google帐户进行身份验证的完整示例吗?将Google OAuth2与Flask一起使用

我在尝试让用户授予Google日历访问权限,然后使用该权限从日历中检索信息并进一步处理。我还需要存储并稍后刷新OAuth2令牌。

我已经看过Google的oauth2client图书馆,并且可以开始跳舞以检索授权码,但我从那里有点失落。查看Google的OAuth 2.0 Playground我明白我需要请求刷新令牌和访问令牌,但库中提供的示例仅适用于App Engine和Django。

我也尝试使用Flask's OAuth module,其中包含对OAuth2的引用,但我没有看到任何交换授权代码的方法。

我可能会手动编写请求代码,但会更愿意使用或修改现有的使得请求容易的python模块,正确处理可能的响应,甚至可能协助存储令牌。

有这样的事吗?

回答

33

另一个答案提到Flask-Rauth,但没有详细说明如何使用它。有几个谷歌特有的陷阱,但我最终实现了它,它运作良好。我将它与Flask-Login整合在一起,所以我可以用有用的糖来装饰我的视图,如@login_required

我希望能够支持多个OAuth2提供程序,因此部分代码是通用的,并基于Miguel Grinberg关于通过Facebook和Twitter支持OAuth2的出色文章here

首先,从谷歌添加特定的谷歌认证信息到你的应用程序配置:

GOOGLE_LOGIN_CLIENT_ID = "<your-id-ending-with>.apps.googleusercontent.com" 
GOOGLE_LOGIN_CLIENT_SECRET = "<your-secret>" 

OAUTH_CREDENTIALS={ 
     'google': { 
      'id': GOOGLE_LOGIN_CLIENT_ID, 
      'secret': GOOGLE_LOGIN_CLIENT_SECRET 
     } 
} 

当你创建你的应用程序(在我的情况下,模块的__init__.py):

app = Flask(__name__) 
app.config.from_object('config') 

在您的应用程序模块,创建auth.py

from flask import url_for, current_app, redirect, request 
from rauth import OAuth2Service 

import json, urllib2 

class OAuthSignIn(object): 
    providers = None 

    def __init__(self, provider_name): 
     self.provider_name = provider_name 
     credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name] 
     self.consumer_id = credentials['id'] 
     self.consumer_secret = credentials['secret'] 

    def authorize(self): 
     pass 

    def callback(self): 
     pass 

    def get_callback_url(self): 
     return url_for('oauth_callback', provider=self.provider_name, 
         _external=True) 

    @classmethod 
    def get_provider(self, provider_name): 
     if self.providers is None: 
      self.providers={} 
      for provider_class in self.__subclasses__(): 
       provider = provider_class() 
       self.providers[provider.provider_name] = provider 
     return self.providers[provider_name] 

class GoogleSignIn(OAuthSignIn): 
    def __init__(self): 
     super(GoogleSignIn, self).__init__('google') 
     googleinfo = urllib2.urlopen('https://accounts.google.com/.well-known/openid-configuration') 
     google_params = json.load(googleinfo) 
     self.service = OAuth2Service(
       name='google', 
       client_id=self.consumer_id, 
       client_secret=self.consumer_secret, 
       authorize_url=google_params.get('authorization_endpoint'), 
       base_url=google_params.get('userinfo_endpoint'), 
       access_token_url=google_params.get('token_endpoint') 
     ) 

    def authorize(self): 
     return redirect(self.service.get_authorize_url(
      scope='email', 
      response_type='code', 
      redirect_uri=self.get_callback_url()) 
      ) 

    def callback(self): 
     if 'code' not in request.args: 
      return None, None, None 
     oauth_session = self.service.get_auth_session(
       data={'code': request.args['code'], 
         'grant_type': 'authorization_code', 
         'redirect_uri': self.get_callback_url() 
        }, 
       decoder = json.loads 
     ) 
     me = oauth_session.get('').json() 
     return (me['name'], 
       me['email']) 

这创建了一个可以分类的通用OAuthSignIn类。Google子类从Google发布的信息列表中提取信息(采用JSON格式here)。这是可以更改的信息,所以这种方法将确保它始终是最新的。其中一个限制是,如果在Flask应用程序初始化(模块导入)时,您的服务器上没有可用的Internet连接,它将不会正确实例化。这应该几乎不会是一个问题,但将最后一个已知值存储在配置数据库中以涵盖这种可能性是个不错的主意。

最后,该类返回callback()函数中的name, email的元组。 Google实际上会返回更多信息,包括Google+个人资料(如果有)。检查由oauth_session.get('').json()返回的字典以查看全部内容。 如果在authorize()函数中扩展范围(对于我的应用程序,email已足够),则可以通过Google API访问更多信息。

接下来,编写意见,以配合它一起:

from flask.ext.login import login_user, logout_user, current_user, login_required 

@app.route('/authorize/<provider>') 
def oauth_authorize(provider): 
    # Flask-Login function 
    if not current_user.is_anonymous(): 
     return redirect(url_for('index')) 
    oauth = OAuthSignIn.get_provider(provider) 
    return oauth.authorize() 

@app.route('/callback/<provider>') 
def oauth_callback(provider): 
    if not current_user.is_anonymous(): 
     return redirect(url_for('index')) 
    oauth = OAuthSignIn.get_provider(provider) 
    username, email = oauth.callback() 
    if email is None: 
     # I need a valid email address for my user identification 
     flash('Authentication failed.') 
     return redirect(url_for('index')) 
    # Look if the user already exists 
    user=User.query.filter_by(email=email).first() 
    if not user: 
     # Create the user. Try and use their name returned by Google, 
     # but if it is not set, split the email address at the @. 
     nickname = username 
     if nickname is None or nickname == "": 
      nickname = email.split('@')[0] 

     # We can do more work here to ensure a unique nickname, if you 
     # require that. 
     user=User(nickname=nickname, email=email) 
     db.session.add(user) 
     db.session.commit() 
    # Log in the user, by default remembering them for their next visit 
    # unless they log out. 
    login_user(user, remember=True) 
    return redirect(url_for('index')) 

最后,我/login视图和模板,使这一切发生:

@app.route('/login', methods=['GET', 'POST']) 
def login(): 
    if g.user is not None and g.user.is_authenticated(): 
     return redirect(url_for('index')) 
    return render_template('login.html', 
          title='Sign In') 

的login.html:

{% extends "base.html" %} 

{% block content %} 

    <div id="sign-in"> 
     <h1>Sign In</h1> 
     <p> 
     <a href={{ url_for('oauth_authorize', provider='google') }}><img src="{{ url_for('static', filename='img/sign-in-with-google.png') }}" /></a> 
    </div> 
{% endblock %} 

确保正确的回调广告连衣裙已在Google注册,用户只需点击登录页面的“使用Google登录”,即可注册并登录。

+1

多么美妙而完整的例子!非常感谢你。 – emning 2015-04-15 18:06:53

+2

我在过去3年的Rauth github页面上看不到任何活动。看起来像死了的模块。 – Rusty 2016-04-22 16:08:52

1

Flask-oauth可能是您现在最好的选择,因为我们知道它不支持令牌刷新,但它可以与Facebook一起使用,我们使用它来做它,并且它是oauth 2.如果它不需要特定的瓶子,你可以看看请求 - oauth

32

我已经搜索了很多关于使用不同的库,但他们都在某种意义上似乎醚过度杀伤(你可以使用它在任何平台上,但你需要大量的代码)或文档没有解释我想要的。长话短说 - 我从头开始编写它,从而了解真正的Google API认证过程。这听起来不那么难。基本上你需要遵循https://developers.google.com/accounts/docs/OAuth2WebServer准则,就是这样。 为此,您还需要注册https://code.google.com/apis/console/以生成凭证并注册您的链接。我使用了简单的子域指向我的办公室IP,因为它只允许域名。

对于用户登录/管理和会话我已经使用这个插件烧瓶http://packages.python.org/Flask-Login/ - 会有一些基于此的代码。

所以第一件事第一 - 索引视图:

from flask import render_template 
from flask.ext.login import current_user 
from flask.views import MethodView 

from myapp import app 


class Index(MethodView): 
    def get(self): 
     # check if user is logged in 
     if not current_user.is_authenticated(): 
      return app.login_manager.unauthorized() 

     return render_template('index.html') 

所以这个观点不会打开,直到我们将通过身份验证的用户。 谈到用户 - 用户模型:

from sqlalchemy.orm.exc import NoResultFound 
from sqlalchemy import Column, Integer, DateTime, Boolean, String 

from flask.ext.login import UserMixin 
from myapp.metadata import Session, Base 


class User(Base): 
    __tablename__ = 'myapp_users' 

    id = Column(Integer, primary_key=True) 
    email = Column(String(80), unique=True, nullable=False) 
    username = Column(String(80), unique=True, nullable=False) 

    def __init__(self, email, username): 
     self.email = email 
     self.username = username 

    def __repr__(self): 
     return "<User('%d', '%s', '%s')>" \ 
       % (self.id, self.username, self.email) 

    @classmethod 
    def get_or_create(cls, data): 
     """ 
     data contains: 
      {u'family_name': u'Surname', 
      u'name': u'Name Surname', 
      u'picture': u'https://link.to.photo', 
      u'locale': u'en', 
      u'gender': u'male', 
      u'email': u'[email protected]', 
      u'birthday': u'0000-08-17', 
      u'link': u'https://plus.google.com/id', 
      u'given_name': u'Name', 
      u'id': u'Google ID', 
      u'verified_email': True} 
     """ 
     try: 
      #.one() ensures that there would be just one user with that email. 
      # Although database should prevent that from happening - 
      # lets make it buletproof 
      user = Session.query(cls).filter_by(email=data['email']).one() 
     except NoResultFound: 
      user = cls(
        email=data['email'], 
        username=data['given_name'], 
       ) 
      Session.add(user) 
      Session.commit() 
     return user 

    def is_active(self): 
     return True 

    def is_authenticated(self): 
     """ 
     Returns `True`. User is always authenticated. Herp Derp. 
     """ 
     return True 

    def is_anonymous(self): 
     """ 
     Returns `False`. There are no Anonymous here. 
     """ 
     return False 

    def get_id(self): 
     """ 
     Assuming that the user object has an `id` attribute, this will take 
     that and convert it to `unicode`. 
     """ 
     try: 
      return unicode(self.id) 
     except AttributeError: 
      raise NotImplementedError("No `id` attribute - override get_id") 

    def __eq__(self, other): 
     """ 
     Checks the equality of two `UserMixin` objects using `get_id`. 
     """ 
     if isinstance(other, UserMixin): 
      return self.get_id() == other.get_id() 
     return NotImplemented 

    def __ne__(self, other): 
     """ 
     Checks the inequality of two `UserMixin` objects using `get_id`. 
     """ 
     equal = self.__eq__(other) 
     if equal is NotImplemented: 
      return NotImplemented 
     return not equal 

有可能出错UserMixin,但我会处理这个后者。您的用户模型看起来不同,只需使其与flask-login兼容即可。

那么剩下的是什么 - 它是自我认证。我设置为flask-login登录视图是'login'Login视图呈现与登录按钮指向谷歌的HTML - 谷歌重定向到Auth视图。应该可能只是将用户重定向到谷歌,以防其登录用户的网站。

import logging 
import urllib 
import urllib2 
import json 

from flask import render_template, url_for, request, redirect 
from flask.views import MethodView 
from flask.ext.login import login_user 

from myapp import settings 
from myapp.models import User 


logger = logging.getLogger(__name__) 


class Login(BaseViewMixin): 
    def get(self): 
     logger.debug('GET: %s' % request.args) 
     params = { 
      'response_type': 'code', 
      'client_id': settings.GOOGLE_API_CLIENT_ID, 
      'redirect_uri': url_for('auth', _external=True), 
      'scope': settings.GOOGLE_API_SCOPE, 
      'state': request.args.get('next'), 
     } 
     logger.debug('Login Params: %s' % params) 
     url = settings.GOOGLE_OAUTH2_URL + 'auth?' + urllib.urlencode(params) 

     context = {'login_url': url} 
     return render_template('login.html', **context) 


class Auth(MethodView): 
    def _get_token(self): 
     params = { 
      'code': request.args.get('code'), 
      'client_id': settings.GOOGLE_API_CLIENT_ID, 
      'client_secret': settings.GOOGLE_API_CLIENT_SECRET, 
      'redirect_uri': url_for('auth', _external=True), 
      'grant_type': 'authorization_code', 
     } 
     payload = urllib.urlencode(params) 
     url = settings.GOOGLE_OAUTH2_URL + 'token' 

     req = urllib2.Request(url, payload) # must be POST 

     return json.loads(urllib2.urlopen(req).read()) 

    def _get_data(self, response): 
     params = { 
      'access_token': response['access_token'], 
     } 
     payload = urllib.urlencode(params) 
     url = settings.GOOGLE_API_URL + 'userinfo?' + payload 

     req = urllib2.Request(url) # must be GET 

     return json.loads(urllib2.urlopen(req).read()) 

    def get(self): 
     logger.debug('GET: %s' % request.args) 

     response = self._get_token() 
     logger.debug('Google Response: %s' % response) 

     data = self._get_data(response) 
     logger.debug('Google Data: %s' % data) 

     user = User.get_or_create(data) 
     login_user(user) 
     logger.debug('User Login: %s' % user) 
     return redirect(request.args.get('state') or url_for('index')) 

所以一切都是splited两个部分 - 一个用于获取谷歌令牌_get_token。其他用于在_get_data中使用它并检索基本用户数据。

我的设置文件包含:

GOOGLE_API_CLIENT_ID = 'myid.apps.googleusercontent.com' 
GOOGLE_API_CLIENT_SECRET = 'my secret code' 
GOOGLE_API_SCOPE = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email' 
GOOGLE_OAUTH2_URL = 'https://accounts.google.com/o/oauth2/' 
GOOGLE_API_URL = 'https://www.googleapis.com/oauth2/v1/' 

记住的观点必须有URL路径连接到应用程序,所以我一直用这个urls.py文件,这样就可以更容易地跟踪我的看法,减少进口的东西以烧瓶应用程序创建文件:

from myapp import app 
from myapp.views.auth import Login, Auth 
from myapp.views.index import Index 


urls = { 
    '/login/': Login.as_view('login'), 
    '/auth/': Auth.as_view('auth'), 
    '/': Index.as_view('index'), 
} 

for url, view in urls.iteritems(): 
    app.add_url_rule(url, view_func=view) 

所有这一切使在谷歌授权在Flask工作。如果您复制粘贴它 - 它可能需要一些补丁与烧瓶登录文档和SQLAlchemy映射,但想法是在那里。

+1

谢谢您的全面回答好先生 – corvid 2014-04-06 23:14:41

1

它看起来像新的模块烧瓶Rauth是这个问题的答案:

Flask-Rauth is a Flask extensions that allows you to easily interact with OAuth 2.0, OAuth 1.0a, and Ofly enabled applications. [...] This means that Flask-Rauth will allow users on your Flask website to sign in to external web services (i.e. the Twitter API, Facebook Graph API, GitHub, etc).

参见:Flask-Rauth

18

Authomatic一试(我是该项目的维护者)。它的使用方法很简单,用任何的Python框架工作,并支持16的OAuth 2.010 OAuth的1.0A提供商和OpenID的

下面是关于如何将用户与谷歌进行身份验证和得到他/她的YouTube视频列表一个简单的例子:

# main.py 

from flask import Flask, request, make_response, render_template 
from authomatic.adapters import WerkzeugAdapter 
from authomatic import Authomatic 
from authomatic.providers import oauth2 


CONFIG = { 
    'google': { 
     'class_': oauth2.Google, 
     'consumer_key': '########################', 
     'consumer_secret': '########################', 
     'scope': oauth2.Google.user_info_scope + ['https://gdata.youtube.com'], 
    }, 
} 

app = Flask(__name__) 
authomatic = Authomatic(CONFIG, 'random secret string for session signing') 


@app.route('/login/<provider_name>/', methods=['GET', 'POST']) 
def login(provider_name): 
    response = make_response() 

    # Authenticate the user 
    result = authomatic.login(WerkzeugAdapter(request, response), provider_name) 

    if result: 
     videos = [] 
     if result.user: 
      # Get user info 
      result.user.update() 

      # Talk to Google YouTube API 
      if result.user.credentials: 
       response = result.provider.access('https://gdata.youtube.com/' 
        'feeds/api/users/default/playlists?alt=json') 
       if response.status == 200: 
        videos = response.data.get('feed', {}).get('entry', []) 

     return render_template(user_name=result.user.name, 
           user_email=result.user.email, 
           user_id=result.user.id, 
           youtube_videos=videos) 
    return response 


if __name__ == '__main__': 
    app.run(debug=True) 

还有一个非常简单的Flask tutorial它展示了如何对用户进行认证通过Facebook和Twitter与他们的API交谈来阅读用户的新闻传播。

+0

这看起来像另一个很好的选择,非常感谢。我一定会评估这一点。 – emning 2014-02-24 17:12:16

+0

谢谢你的这个例子。你介意把这个例子扩展到下面的场景吗? 1.当我尝试使用Twitter并重新加载页面(登录后显示)时,出现错误“FailureError:无法从https://api.twitter.com/oauth/access_token获取OAuth 1.0a oauth_token! HTTP状态码:401.',清楚地表明我做错了什么。 2.如何保护API终点并将用户重定向到登录页面(如果他们没有登录)?真的很感谢你的时间。 – Legend 2014-07-09 04:43:49

+0

理想情况下,如果您可以覆盖像第一个答案那样的场景,那对我这样的初学者会有很大帮助:1.使用FB/Twitter/Google对用户进行身份验证,2.存储必要的详细信息(例如,在sqlite中)和当前会话,3.保护某些API端点。感谢您的工作! – Legend 2014-07-09 04:56:39

0

由于oauth2client现在已被弃用,我推荐使用bluemoon建议的内容OAuth2的Bruno Rocha's model Flask中的Google身份验证是使用lepture强大的Flask-OAuthlib(可安装pip)的不错起点。我建议模仿,然后扩大以满足您的需求。

相关问题