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

Managing users authentication in Google App Engine

我正在开发基于 google 应用引擎的网络应用程序。 该应用程序使用 google 身份验证 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('/')

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

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

重要更新 我确实理解 Alex Martelli 的评论中的解释:有一个 cookie 可以使以前的 GAE 身份验证有效。 问题是同一个网络应用程序还利用 Google Api 客户端库 Python https://developers.google.com/api-client-library/python/ to perform operations on Drive and Calendar. In GAE apps such library can be easily used through decorators implementing the whole OAuth2 Flow (https://developers.google.com/api-client-library/python/guide/google_app_engine)。

因此我的 Handlers 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 装饰器,returns 数据(驱动器、日历等)属于第二个账户。

这真的不合适。

按照上面的示例 (SomeHandler class) 假设我登录为帐户 A。users.get_current_user() 始终如预期的那样 returns A。现在假设我停止使用该应用程序,很长一段时间后 oauth_required 将我重定向到 Google 帐户页面。因此,我决定(或犯错误)记录为帐户 B。当访问 SomeHandler class 的 Get 方法时,userId(通过 users.get_current_user() 检索到的是 A,而日历列表通过返回服务对象(Google Api 客户端库)是属于 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 并且,在 none 凭据的情况下,我注销用户并将其重定向到 DECORATOR.authorize_url()

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

它似乎有效并且似乎对帐户切换很有效。 这是一个合理的解决方案还是我没有考虑到问题的某些方面?

我理解混乱,但系统是 "working as designed"。

在任何时间点,GAE 处理程序都可以有零个或一个 "logged-in user"(users.get_current_user() 返回的对象,如果没有登录用户,则 None零个或多个"oauth2 authorization tokens"(对于已授予且未撤销的任何用户和范围)。

没有强制 oauth2 东西匹配的约束,在任何意义上,"logged-in user, if any"。

我建议您查看 https://code.google.com/p/google-api-python-client/source/browse/samples/appengine/main.py (to run it, you'll have to clone the whole "google-api-python-client" package, then copy into the google-api-python-client/source/browse/samples/appengine directory directories apiclient/ and oauth2client/ from this same package as well as httplib2 from https://github.com/jcgregorio/httplib2 上的非常简单的示例 -- 并自定义 client_secrets.json -- 但是,您不需要 运行 它,只是为了阅读并遵循代码)。

这个示例甚至 use users.get_current_user() -- 它不需要也不关心它:它只展示了如何使用 oauth2,持有 oauth2 授权令牌与 users 服务之间没有 联系。 (例如,这允许您让 cron 代表一个或多个用户稍后执行某些任务——cron 不登录,但没关系——如果 oauth2 令牌被正确存储和检索,那么它可以使用他们)。

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

user = service.people().get(userId='me').execute(http=http)

with service build earlier as discovery.build("plus", "v1", http=http) (with a different non-authorized http).

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

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

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