2012-02-29 75 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)的不錯起點。我建議模仿,然後擴大以滿足您的需求。

相關問題