Android Google+ 集成 - 重复 UserRecoverableAuthException

Posted

技术标签:

【中文标题】Android Google+ 集成 - 重复 UserRecoverableAuthException【英文标题】:Android Google+ integration - repeated UserRecoverableAuthException 【发布时间】:2013-07-16 19:28:46 【问题描述】:

我们已就此与 Google 联系,we are on chat

对于除了三星手机的设备,该问题似乎已得到解决。

我正在根据official instructions 向应用添加 Google+ 登录选项。一旦用户选择了他们的帐户,我希望我的服务器检索他们的 Google+ 个人资料信息并更新他们在我们网站上的个人资料以匹配。

第一部分 - 让用户在本地选择一个 Google 帐户 - 似乎工作得很好。当我尝试为所选帐户请求令牌时,会显示带有适当参数的 Google 身份验证对话框;但是,当我使用该对话框授权应用程序并重新请求令牌时,GoogleAuthUtil.getToken(...) 再次抛出 UserRecoverableAuthExceptionNeedPermission,而不是 GooglePlayServicesAvailabilityException)并且我得到相同的对话框,要求我批准!

运行 Android 4.1.1(具有 3 个 Google 帐户)的 Samsung S3 和运行 4.0.3 的 Acer A100 存在此行为。它不存在于运行 2.3.4 的 HTC Glacier 上。相反,HTC Glacier 给了我一个有效的验证码。所有设备都安装了最新版本的 Google Play 服务,并且使用不同的 Google+ 帐户。

有人见过这个吗?我可以从哪里开始调试?

这是完整的代码 - 有什么明显的错误吗?

public class MyGooglePlusClient 
private static final String LOG_TAG = "GPlus";
private static final String SCOPES_LOGIN = Scopes.PLUS_LOGIN + " " + Scopes.PLUS_PROFILE;
private static final String ACTIVITIES_LOGIN = "http://schemas.google.com/AddActivity";
private static MyGooglePlusClient myGPlus = null;
private BaseActivity mRequestingActivity = null;
private String mSelectedAccount = null;
    
/**
 * Get the GPlus singleton
 * @return GPlus
 */
public synchronized static MyGooglePlusClient getInstance() 
    if (myGPlus == null)
        myGPlus = new MyGooglePlusClient();
    return myGPlus;


public boolean login(BaseActivity requester) 
    Log.w(LOG_TAG, "Starting login...");
    if (mRequestingActivity != null) 
        Log.w(LOG_TAG, "Login attempt already in progress.");
        return false; // Cannot launch a new request; already in progress
    
    
    mRequestingActivity = requester;
    if (mSelectedAccount == null) 
        Intent intent = AccountPicker.newChooseAccountIntent(null, null, new String[]GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE, false,
                null, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE, null, null);
        mRequestingActivity.startActivityForResult(intent, BaseActivity.REQUEST_GPLUS_SELECT);
    
    return true;


public void loginCallback(String accountName) 
    mSelectedAccount = accountName;
    authorizeCallback();

    
public void logout() 
    Log.w(LOG_TAG, "Logging out...");
    mSelectedAccount = null;


public void authorizeCallback() 
    Log.w(LOG_TAG, "User authorized");

    AsyncTask<Void, Void, String> task = new AsyncTask<Void, Void, String>() 
        @Override
        protected String doInBackground(Void... params) 
            String token = null;
            try 
                Bundle b = new Bundle();
                b.putString(GoogleAuthUtil.KEY_REQUEST_VISIBLE_ACTIVITIES, ACTIVITIES_LOGIN);
                token = GoogleAuthUtil.getToken(mRequestingActivity,
                        mSelectedAccount,
                        "oauth2:server:client_id:"+Constants.GOOGLE_PLUS_SERVER_OAUTH_CLIENT
                        +":api_scope:" + SCOPES_LOGIN,
                        b);
             catch (IOException transientEx) 
                // Network or server error, try later
                Log.w(LOG_TAG, transientEx.toString());
                onCompletedLoginAttempt(false);
             catch (GooglePlayServicesAvailabilityException e) 
                Log.w(LOG_TAG, "Google Play services not available.");
                Intent recover = e.getIntent();
                mRequestingActivity.startActivityForResult(recover, BaseActivity.REQUEST_GPLUS_AUTHORIZE);
             catch (UserRecoverableAuthException e) 
                // Recover (with e.getIntent())
                Log.w(LOG_TAG, "User must approve "+e.toString());
                Intent recover = e.getIntent();
                mRequestingActivity.startActivityForResult(recover, BaseActivity.REQUEST_GPLUS_AUTHORIZE);
             catch (GoogleAuthException authEx) 
                // The call is not ever expected to succeed
                Log.w(LOG_TAG, authEx.toString());
                onCompletedLoginAttempt(false);
            

            Log.w(LOG_TAG, "Finished with task; token is "+token);
            if (token != null) 
                authorizeCallback(token);
            
            
            return token;
        

    ;
    task.execute();


public void authorizeCallback(String token) 
    Log.w(LOG_TAG, "Token obtained: "+token);
    // <snipped - do some more stuff involving connecting to the server and resetting the state locally>


public void onCompletedLoginAttempt(boolean success) 
    Log.w(LOG_TAG, "Login attempt "+(success ? "succeeded" : "failed"));
    mRequestingActivity.hideProgressDialog();
    mRequestingActivity = null;


【问题讨论】:

在这篇文章发布一年多之后,我仍然收到重复的 UserRecoverableAuthException 异常。 oauth 序列不断请求离线许可。我怀疑 GoogleAuthUtil.getToken 没有正确处理 access_type=offline。 【参考方案1】:

我遇到这个问题已经有一段时间了,并想出了一个合适的解决方案。

String token = GoogleAuthUtil.getToken(this, accountName, scopeString, appActivities);

此行将返回一次性令牌或触发 UserRecoverableAuthException。 在 Google Plus 登录指南中,它说要打开正确的恢复活动。

startActivityForResult(e.getIntent(), RECOVERABLE_REQUEST_CODE);

当活动返回结果时,它会返回意图中的一些额外内容,这就是新令牌所在的位置:

@Override
protected void onActivityResult(int requestCode, int responseCode, Intent intent) 
    if (requestCode == RECOVERABLE_REQUEST_CODE && responseCode == RESULT_OK) 
        Bundle extra = intent.getExtras();
        String oneTimeToken = extra.getString("authtoken");
    

使用extra给出的新oneTimeToken,您可以提交到服务器以正确连接。

我希望这会有所帮助!

【讨论】:

此答案应标记为正确。这是处理未获得令牌的正确方法。【参考方案2】:

回复为时已晚,但可能对以后有同样问题的人有所帮助。

他们在tutorial 中提到它总是会抛出 UserRecoverableAuthException 当您第一次调用 GoogleAuthUtil.getToken() 时。第二次就成功了。

catch (UserRecoverableAuthException e) 
  // Requesting an authorization code will always throw
  // UserRecoverableAuthException on the first call to GoogleAuthUtil.getToken
  // because the user must consent to offline access to their data.  After
  // consent is granted control is returned to your activity in onActivityResult
  // and the second call to GoogleAuthUtil.getToken will succeed.
  startActivityForResult(e.getIntent(), AUTH_CODE_REQUEST_CODE);
  return;

我使用下面的代码从谷歌获取访问代码。

public void onConnected(Bundle connectionHint) 执行此new GetAuthTokenFromGoogle().execute(); 一次,从protected void onActivityResult(int requestCode, int responseCode, Intent intent) 执行一次

private class GetAuthTokenFromGoogle extends AsyncTask<Void, Integer, Void>
        @Override  
        protected void onPreExecute()  
          

        
        @Override
        protected Void doInBackground(Void... params) 
            // TODO Auto-generated method stub

            try 
                accessCode = GoogleAuthUtil.getToken(mContext, Plus.AccountApi.getAccountName(mGoogleApiClient), SCOPE);
                new ValidateTokenWithPhoneOmega().execute();
                Log.d("Token  -- ", accessCode);
             catch (IOException transientEx) 
                // network or server error, the call is expected to succeed if you try again later.
                // Don't attempt to call again immediately - the request is likely to
                // fail, you'll hit quotas or back-off.

                return null;
             catch (UserRecoverableAuthException e) 
                // Recover
                startActivityForResult(e.getIntent(), RC_ACCESS_CODE);
                e.printStackTrace();
             catch (GoogleAuthException authEx) 
                // Failure. The call is not expected to ever succeed so it should not be
                // retried.
                authEx.printStackTrace();
                return null;
             catch (Exception e) 
                throw new RuntimeException(e);
            
            return null;  
        

        @Override  
        protected void onPostExecute(Void result)  
         
        
    

【讨论】:

您会说这种方法是“正确的方法”还是“一种似乎行之有效的方法”?此外,这在用户取消一个(或两个)对话框(一次或多次)的情况下是否有效? 这对我来说效果很好。在第一次调用 GoogleAuthUtil.getToken() 时,用户将看到离线访问同意屏幕。一旦用户单击确定按钮,他将看到“登录”屏幕(仅一次),其中包含类似这样的消息可能需要几分钟。之后,用户将看不到任何屏幕,第二次调用 GoogleAuthUtil.getToken() 将返回访问代码。【参考方案3】:

我已经通过使用基于 Web 的登录解决了这个问题。我打开一个这样的网址

String url = "https://accounts.google.com/o/oauth2/auth?scope=" + Scopes.PLUS_LOGIN + "&client_id=" + webLoginClientId + "&response_type=code&access_type=offline&approval_prompt=force&redirect_uri=" + redirect;

然后重定向 url 处理响应并返回到我的应用程序。

就我使用 Google Play 服务的发现而言,我发现:

HTC One 是 3.1.59 (736673-30) - 不工作 Galaxy Note 是 3.1.59 (736673-36) - 不工作 Nexus S 是 3.1.59 (736673-34) - 工作

而且我想参与正在进行的聊天,但是我没有足够高的声誉来这样做。

【讨论】:

感谢您的解决方法!我会在我的应用程序上查看并报告。你用什么打开 URL - WebView 或其他东西? 是的,我正在使用 WebView...虽然这不是最好的实现,因为用户是否已经在手机上使用 G+ 帐户登录并不重要,因为他们总是必须在 WebView 中独立输入他们的详细信息。我希望本机登录可以得到解决。【参考方案4】:

我最近遇到了同样的问题 - 它似乎是特定于设备的(我每次在一台 S3 上都会发生这种情况,但在另一台运行相同操作系统的 S3 上却没有发生,即使使用相同的帐户也是如此) .我的预感是这是客户端应用程序中的错误,无论是 G+ 应用程序还是 Google Play 服务应用程序。我设法通过恢复出厂设置(摩托罗拉 Defy),然后重新安装 Google Play 服务应用程序解决了我的一台设备上的问题,但告诉用户这是一个完全无用的解决方案。

【讨论】:

【参考方案5】:

编辑(2013 年 8 月 6 日):这似乎已为我修复,无需对我的代码进行任何更改。

我可以看到的第一个潜在问题是您在收到 onConnected() 回调后调用了 GoogleAuthUtil.getToken()。这是一个问题,因为使用 GoogleAuthUtil.getToken() 为您的服务器请求授权代码将始终向您的用户显示同意屏幕。因此,您应该只为新用户获取授权码,并且为了避免向新用户显示两个同意屏幕,您必须在解决来自 PlusClient 的任何连接失败之前获取授权码并在您的服务器上交换它。

其次,确保您确实需要 PlusClient 和服务器的授权代码。如果您打算从 android 客户端您的服务器调用 Google API,您只需要获取 PlusClient 和授权代码。如this answer 中所述。

这些问题只会导致显示两个同意对话框(这显然不是无限循环) - 您是否看到两个以上的同意对话框?

【讨论】:

好的。我想知道这一点。似乎我对用户批准后如何检索令牌有一个基本的误解 - 如果我没有通过重复调用 getToken() 来检索令牌,那么我如何do 检索它? (传递给 onActivityResult() 的意图似乎没有任何包含它的额外内容,而且我在 API 文档中没有找到任何其他相关方法。) 不,你一开始是对的。在处理 UserRecoverableAuthExceptions 之后,您需要重复调​​用 getToken() 来获取令牌。但这不是多重同意对话的来源。发生双重同意是因为您可能需要解决 UserRecoverableAuthException 才能从 PlusClient.connect() 成功获取 onConnected。但是您可以保证获得另一个授权代码调用的同意对话框。 嗯。我知道了。这就是你的意思,没有解释为什么我得到两个以上。但是如果设备上有多个谷歌账户,我不是必须从 PlusClient.connect() 的结果中获取他们的账户选择吗? 你是对的 - 不幸的是你不能使用内置的帐户选择。相反,您需要 AccountPicker.newChooseAccountIntent() 来显示您自己的帐户选择器,然后在创建 PlusClient 时使用 PlusClient.Builder.setAccountName() 设置帐户名称。 Google Play 服务版本肯定是一样的,所以我猜这是服务器端的变化。 :-/【参考方案6】:

我有一个类似的问题,一个明显的身份验证循环不断创建 read: spamming 这些“Signing In...”和权限请求对话框,同时还反复发出讨论的异常。

问题出现在我(以及我怀疑的其他类似我)"cargo-culted" 来自AndroidHive 的一些稍微修改的示例代码中。对我有用的解决方案是确保在任何给定时间只有一个后台令牌检索任务在后台运行。

为了使我的代码更易于理解,这是我的应用程序中的身份验证流程(几乎与 AndoidHive 上的示例代码相同):Activity -> onConnected(...) -> getProfileInformation() -> getOneTimeToken()

这里是 getOneTimeToken() 被调用的地方:

private void getProfileInformation() 
    try 
        if (Plus.PeopleApi.getCurrentPerson(mGoogleApiClient) != null) 
            Person currentPerson = Plus.PeopleApi
                    .getCurrentPerson(mGoogleApiClient);
            String personName = currentPerson.getDisplayName();
            String personPhotoUrl = currentPerson.getImage().getUrl();
            String personGooglePlusProfile = currentPerson.getUrl();
            String email = Plus.AccountApi.getAccountName(mGoogleApiClient);
            getOneTimeToken(); // <-------
            ...

这是我的getOneTimeToken()

private void getOneTimeToken()
    if (task==null)
    task = new AsyncTask<Void, Void, String>() 
        @Override
        protected String doInBackground(Void... params) 
            LogHelper.log('d',LOGTAG, "Executing background task....");
            Bundle appActivities = new Bundle();
            appActivities.putString(
                         GoogleAuthUtil.KEY_REQUEST_VISIBLE_ACTIVITIES,
                         ACTIVITIES_LOGIN);
            String scopes = "oauth2:server" + 
                            ":client_id:" + SERVER_CLIENT_ID + 
                            ":api_scope:" + SCOPES_LOGIN;
            String token = null;
            try 
                token = GoogleAuthUtil.getToken(
                        ActivityPlus.this,
                        Plus.AccountApi.getAccountName(mGoogleApiClient),
                        scopes,
                        appActivities
                );
             catch (IOException transientEx) 
                /* Original comment removed*/
                LogHelper.log('e',LOGTAG, transientEx.toString());
             catch (UserRecoverableAuthException e) 
                /* Original comment removed*/
                LogHelper.log('e',LOGTAG, e.toString());
                startActivityForResult(e.getIntent(), AUTH_CODE_REQUEST);
             catch (GoogleAuthException authEx) 
                /* Original comment removed*/
                LogHelper.log('e',LOGTAG, authEx.toString());
             catch (IllegalStateException stateEx)
                LogHelper.log('e',LOGTAG, stateEx.toString());
            
            LogHelper.log('d',LOGTAG, "Background task finishing....");
            return token;
        

        @Override
        protected void onPostExecute(String token) 
            LogHelper.log('i',LOGTAG, "Access token retrieved: " + token);
        

    ;
    
    LogHelper.log('d',LOGTAG, "Task setup successful.");
    if(task.getStatus() != AsyncTask.Status.RUNNING)
        task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); //double safety!
     else
        LogHelper.log('d',LOGTAG, 
                       "Attempted to restart task while it is running!");

请注意,对于多次执行的任务,我有一个可能是多余的双重安全:

    if(task .getStatus() != AsyncTask.Status.RUNNING)... - 确保任务在尝试执行之前没有运行。 task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);- 确保此任务的副本是“同步的”(即,有一个队列,以便在给定时间只能执行一个此类任务)。

附: 小说明:LogHelper.log('e',...) 等价于Log.e(...) 等。

【讨论】:

【参考方案7】:

你应该在 UI 线程中启动活动

try 
    ....
 catch (IOException transientEx) 
    ....
 catch (final UserRecoverableAuthException e) 
    ....
    runOnUiThread(new Runnable() 
        public void run()          
            startActivityForResult(e1.getIntent(), AUTH_CODE_REQUEST);
        
    );

【讨论】:

【参考方案8】:

与无限循环的权限请求有相同的错误。对我来说,这是因为我手机上的时间发生了变化。当我自动检查检测时间时,此错误消失了。希望这会有所帮助!

【讨论】:

以上是关于Android Google+ 集成 - 重复 UserRecoverableAuthException的主要内容,如果未能解决你的问题,请参考以下文章

android应用集成google登录

Android Google Pay 集成

google支付Android集成文档

在未集成 Google 移动广告的情况下在 Android 上使用 Google 广告 ID

将 Google 登录集成到您的 Android 应用程序错误

在没有 Cocoapods 的情况下手动集成 Firebase 和 Google Sign In SDK 时出现重复符号