使用Flask与Google OAuth2进行认证

62

有人能为我指出一个完整的例子,使用OAuth2和Flask进行Google账户认证,而且不在App Engine上吗?

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

我已经查看了Google的oauth2client库,并可以开始获取授权码,但是从那里开始有点困惑。通过查看Google的OAuth 2.0 Playground,我了解到需要请求刷新令牌和访问令牌,但库中提供的示例仅适用于App Engine和Django。

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

我可能可以手动编写请求,但更愿意使用或调整现有的Python模块,使请求变得容易,正确处理可能的响应,并甚至协助token的存储。

有这样的东西吗?

9个回答

46

另一个答案提到了 Flask-Rauth,但没有详细介绍如何使用它。虽然有一些与 Google 相关的问题,但我最终已经实现了它,并且它运行良好。我将其与 Flask-Login 集成,以便可以使用有用的语法装饰我的视图,例如 @login_required

我希望能够支持多个 OAuth2 提供者,因此代码的一部分是通用的,基于 Miguel Grinberg 的博客文章,该文章介绍了如何支持 Facebook 和 Twitter 的 OAuth2。这里

首先,将来自 Google 的特定身份验证信息添加到应用程序的配置中:

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 格式 在此处)。这些信息可能会发生变化,因此此方法可以确保信息始终是最新的。其中一个限制是,如果 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')

登录页面:

{% 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登录”,它将为他们注册并登入。


2
我发现 Rauth 的 Github 页面在过去的三年里没有任何活动。看起来这个模块已经死了。 - rusty
1
现在已经有6年没有Rauth的提交了。:( - new name
Rauth 已经 7 年没有新的提交了。新年快乐!https://github.com/joelverhagen/flask-rauth - Paul
1
8年没有提交! - Rohit Swami

33

我曾经搜索了很多使用不同库的方法,但它们中的所有库似乎在某种意义上都过于复杂(你可以在任何平台上使用它,但你需要大量代码),或者文档没有解释我想要的内容。 简而言之 - 我从头开始编写了认证 Google API 的过程,因此理解了整个流程。 它并不像听起来那么难。 基本上,您需要遵循https://developers.google.com/accounts/docs/OAuth2WebServer的指南,就这样。为此,您还需要在https://code.google.com/apis/console/注册以生成凭据并注册您的链接。 我使用了一个简单的子域名指向我的办公室IP,因为它只允许域名。

对于用户登录/管理和会话,我使用了这个Flask插件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'propper@email.com',
            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视图呈现带有指向Google的登录按钮的HTML - Google重定向到Auth视图。如果这是仅供已登录用户使用的网站,则可以直接将用户重定向到Google。

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'))

所以所有内容都被分为两部分 - 一个用于在_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文件,以便更轻松地跟踪我的视图并在Flask应用程序创建文件中导入较少的内容:

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中工作Google授权变得容易。如果你复制粘贴它 - 你可能需要在flask-login的文档和SQLAlchemy映射中进行一些修改,但是思路已经在那里。


19

试试 Authomatic (我是该项目的维护者)。它使用非常简单,可以与任何Python框架一起使用,并支持16个OAuth 2.010个OAuth 1.0a 提供商和OpenID

下面是一个简单的示例,演示如何使用Google进行用户认证并获取他/她的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教程,展示了如何通过Facebook和Twitter验证用户并与他们的API交互以读取用户的新闻动态。


那看起来像是另一个很好的选择,非常感谢。我一定会评估这个选项。 - emning
@Legend,虽然其他库为不同的OAuth流程阶段使用单独的请求处理程序,但Authomatic使用相同的处理程序,并通过请求参数区分各个阶段。如果没有参数,它将重定向用户到提供程序;如果请求包含参数oauth_verifieroauth_tokendenied(OAuth 1.0),Authomatic将触发授权流程的第二阶段。当您使用所有参数刷新页面时,Authomatic将再次触发第二阶段,但使用过时的值会导致提供程序出现未经授权的错误 - Peter Hudec
@Legend,在实际场景中,您将会在会话中存储用户,并在授权流程结束后将用户重定向到其他处理程序。 - Peter Hudec
@Legend,流程的第二阶段受OAuth 2.0中“code”参数中的CSRF令牌以及OAuth 1.0a中“oauth_nonce”和“timestampl”参数中的nonce保护。实际上,您遇到的错误是由一个带有过期nonce的第二阶段OAuth 1.0a请求导致的。 - Peter Hudec
1
@Legend,这里有一个教程,其中用户信息被保存到cookie中,然后用户被重定向回主页,他/她可以重复发布FB状态和推文。不幸的是,该教程基于Google App Engine和Webapp2。我将把这个例子移植到Flask。 - Peter Hudec
显示剩余3条评论

11

谢谢,它非常好用。https://gist.github.com/ericcgu/6eae82aa99a37e6a4369cd5513fc4a54 - ericgu
1
快速入门链接已不存在。已更新为 https://github.com/singingwolfboy/flask-dance-google,原链接为 https://flask-dance.readthedocs.io/en/latest/quickstart.html。 - gisc
我使用了这个库,但是发现了很多问题:首先,文档很差。例如,这段代码(https://github.com/singingwolfboy/flask-dance-google)在本地对我来说无法工作,因为我使用的是http而不是https... - ASSILI Taher
1
@ASSILITaher,你能告诉我文档有哪些问题吗?至于使用http还是https,OAuth要求使用https,而不能使用http。 - singingwolfboy
我本地跟着这个例子(https://github.com/singingwolfboy/flask-dance-google)操作过。但是,在本地使用http时,该示例无法正常工作。我也在使用Google App Engine。不管怎样,我正在尝试使用https来解决这个问题,并使我的项目正常工作。谢谢。 - ASSILI Taher

2
我已经成功将原始答案移植到使用Requests-OAuthlib而不是Rauth。截至本文撰写时,该软件包的最后一次提交是在2019年6月,并且目前被30K+个存储库使用。

要安装,请运行:

$ pip install requests_oauthlib

请注意,在IT技术中,OAUTHLIB_RELAX_TOKEN_SCOPE 环境变量必须设置为 True 才能抑制 警告:Scope has changed。在Windows上,可以通过运行以下命令来完成:

$ set OAUTHLIB_RELAX_TOKEN_SCOPE=1

...
from requests_oauthlib import OAuth2Session
from urllib.request import urlopen


class GoogleSignIn(OAuthSignIn):
    openid_url = "https://accounts.google.com/.well-known/openid-configuration"

    def __init__(self):
        super(GoogleLogin, self).__init__("google")
        self.openid_config = json.load(urlopen(self.openid_url))
        self.session = OAuth2Session(
            client_id=self.consumer_id,
            redirect_uri=self.get_callback_url(),
            scope=self.openid_config["scopes_supported"]
        )

    def authorize(self):
        auth_url, _ = self.session.authorization_url(
            self.openid_config["authorization_endpoint"])
        return redirect(auth_url)

    def callback(self):
        if "code" not in request.args:
            return None, None

        self.session.fetch_token(
            token_url=self.openid_config["token_endpoint"],
            code=request.args["code"],
            client_secret=self.consumer_secret,
        )

        me = self.session.get(self.openid_config["userinfo_endpoint"]).json()
        return me["name"], me["email"]

最初的回答:Requests-OAuthlib文档可以在这里找到https://requests-oauthlib.readthedocs.io/en/latest/index.html

1
看起来新的模块Flask-Rauth就是这个问题的答案:
Flask-Rauth是一个Flask扩展,允许您轻松地与OAuth 2.0、OAuth 1.0a和Ofly启用的应用程序进行交互。这意味着Flask-Rauth将允许您在Flask网站上的用户登录到外部Web服务(即Twitter API、Facebook Graph API、GitHub等)。
请参见: Flask-Rauth

1

Flask-oauth 目前可能是 Flask 特定方式中最好的选择,据我所知它不支持令牌刷新,但可以与 Facebook 配合使用,我们就是用它来进行 OAuth 2 认证。如果不需要特定于 Flask 的解决方案,您可以考虑使用 requests-oauth。


1
由于oauth2client现已过时,我建议采用bluemoon的建议。Bruno Rocha在Flask中使用OAuth2 Google身份验证的模型是使用lepture强大的Flask-OAuthlib(可通过pip安装)的良好起点。我建议模仿这个模型,然后根据您的需求进行扩展。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接