在Google App Engine中管理用户身份验证

时间:2015-02-22 09:18:05

标签: python google-app-engine google-api google-cloud-datastore

我正在开发基于谷歌应用引擎的网络应用程序。 该应用程序使用谷歌身份验证api。 基本上每个处理程序都从这个BaseHandler扩展,并且作为任何get / post的第一个操作,checkAuth被执行。

class BaseHandler(webapp2.RequestHandler):
googleUser = None
userId = None
def checkAuth(self):
    user = users.get_current_user()
    self.googleUser = user;
    if user:
        self.userId = user.user_id()
        userKey=ndb.Key(PROJECTNAME, 'rootParent', 'Utente', self.userId)
        dbuser = MyUser.query(MyUser.key==userKey).get(keys_only=True)
        if dbuser:
            pass
        else:
            self.redirect('/')
    else:
        self.redirect('/')

这个想法是它重定向到/如果没有用户通过谷歌登录或者如果我的用户数据库中没有用户具有该谷歌ID。

问题是我可以成功登录我的网络应用程序并进行操作。然后,从gmail,o从任何谷歌帐户注销,但如果我尝试继续使用它的工作的网络应用程序。 这意味着users.get_current_user()仍然返回有效用户(有效但实际上是OLD)。 这可能吗?

重要更新 我理解Alex Martelli评论中解释的内容:有一个cookie可以保持前GAE认证有效。 问题是,同一个网络应用程序还利用Google Api Client Library for Python https://developers.google.com/api-client-library/python/在Drive和Calendar上执行操作。在GAE应用程序中,可以通过实现整个OAuth2 Flow(https://developers.google.com/api-client-library/python/guide/google_app_engine)的装饰器轻松使用此类库。

因此我的处理程序get / post方法用oauth_required装饰,就像这样

class SomeHandler(BaseHandler):
    @DECORATOR.oauth_required
    def get(self):
        super(SomeHandler,self).checkAuth()
        uid = self.googleUser.user_id()
        http = DECORATOR.http()
        service = build('calendar', 'v3')
        calendar_list = service.calendarList().list(pageToken=page_token).execute(http=http)

装饰者在哪里

   from oauth2client.appengine import OAuth2Decorator

   DECORATOR = OAuth2Decorator(
  client_id='XXXXXX.apps.googleusercontent.com',
  client_secret='YYYYYYY',
  scope='https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file'

        )

通常工作正常。 然而(!!)当应用程序闲置很长时间时,oauth2装饰器会将我重定向到Google身份验证页面,如果我更改帐户(我有2个不同的帐户),则会发生一些事情: 应用程序仍然记录为以前的帐户(通过users.get_current_user()检索),而api客户端库,以及oauth2装饰器,返回属于第二个帐户的数据(驱动器,日历等)。

这真的不合适。

按照上面的示例(SomeHandler类)假设我被记录为帐户A. users.get_current_user()始终按预期返回A.现在假设我停止使用该应用程序,很长一段时间后oauth_required将我重定向到Google帐户页面。因此,我决定(或犯错误)将日志记录为帐户B.当访问SomeHandler类的Get方法时,userId(通过users.get_current_user()重试是A,而通过服务对象返回的日历列表(Google Api) client Library)是属于B(实际当前登录的用户)的日历列表。

我做错了吗?是预期的东西?

另一次更新

这是在Martelli的答案之后。 我更新了这样的处理程序:

class SomeHandler(BaseHandler):
    @DECORATOR.oauth_aware
    def get(self):
        if DECORATOR.has_credentials():
            super(SomeHandler,self).checkAuth()
            uid = self.googleUser.user_id()
            try:
               http = DECORATOR.http()
               service = build('calendar', 'v3')
               calendar_list = service.calendarList().list(pageToken=page_token).execute(http=http)
            except (AccessTokenRefreshError, appengine.InvalidXsrfTokenError):
                self.redirect(users.create_logout_url(
                    DECORATOR.authorize_url()))
        else:
           self.redirect(users.create_logout_url(
              DECORATOR.authorize_url()))

所以基本上我现在使用oauth_aware,如果没有凭据,我会注销用户并将其重定向到DECORATOR.authorize_url()

我注意到在一段时间不活动之后,处理程序引发了AccessTokenRefreshError和appengine.InvalidXsrfTokenError异常(但has_credentials()方法返回True)。我抓住它们并(再次)将流重定向到logout和authorize_url()

它似乎工作,似乎对帐户切换很健壮。 这是一个合理的解决方案,还是我没有考虑问题的某些方面?

1 个答案:

答案 0 :(得分:2)

我理解这种混乱,但系统“按设计工作”。

在任何时候,GAE处理程序都可以有零个或一个“登录用户”(users.get_current_user()返回的对象,如果没有登录用户,则为None 零或更多“oauth2授权令牌”(对于已授予和未撤销的任何用户和范围)。

在任何意义上,没有约束强制oauth2 thingies匹配“登录用户,如果有的话”。

我建议在https://code.google.com/p/google-api-python-client/source/browse/samples/appengine/main.py查看非常简单的示例(要运行它,你必须克隆整个“google-api-python-client”包,然后复制到{{1}来自同一个软件包的目录目录google-api-python-client/source/browse/samples/appengineapiclient/以及来自https://github.com/jcgregorio/httplib2oauth2client/ - 还可以自定义httplib2 - 但是,你不要我需要运行它,只是为了阅读并遵循代码)。

此示例甚至不使用 client_secrets.json - 它不需要它也不关心它:它只显示如何使用users.get_current_user(),并且持有oauth2 - 授权令牌与oauth2服务之间的连接。 (这允许你举例说cron代表一个或多个用户稍后执行某些任务--cron没有登录,但没关系 - 如果oauth2令牌被正确存储和检索,那么它可以使用它们)。

因此,代码使用users从客户端机密创建装饰器,然后在处理程序的scope='https://www.googleapis.com/auth/plus.me'上使用@decorator.oauth_required以确保授权,并使用装饰器的授权get ,它取出

http

user = service.people().get(userId='me').execute(http=http) 之前构建为service(使用不同的非授权discovery.build("plus", "v1", http=http))。

如果你在本地运行它,很容易添加虚假登录(请记住,用户登录伪造dev_appserver),以便http返回users.get_current_user()或您在虚假登录时输入的任何其他假电子邮件屏幕 - 这绝不会阻止完全独立的princess@bride.com流程仍按预期执行(即,与没有任何此类虚假登录的方式完全相同)。

如果您将修改后的应用程序(带有额外的用户登录名)部署到生产环境,那么登录必须是真实的 - 但它与oauth2部分无关,并且与oauth2部分无关。应用

如果您的应用程序的逻辑确实需要将oauth2令牌限制为也登录到您的应用程序的特定用户,则您必须自己实现 - 例如将scope设置为{{1 (除了你需要的任何其他东西),你将从'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.profile.emails.read'一个service.people().get(userId='me')对象获得一个user属性(在许多其他事物中),你可以在其中检查授权令牌用于具有您要授权的电子邮件的用户(并采取补救措施,例如通过注销URL& c)。 ((这可以更简单地完成,无论如何我怀疑你真的需要这样的功能,但是,只是想提一下))。