Android Google+ 集成 - 重复的 UserRecoverableAuthException

28

我们已经联系了谷歌并且正在聊天

这个问题似乎已经得到解决,但三星手机除外。

我按照官方指南在应用程序中添加了Google+登录选项。当用户选择他们的账户后,我希望我的服务器可以检索他们的Google+个人资料信息并更新我们网站上的个人资料以匹配。

第一步 - 让用户在本地选择Google账户 - 似乎工作正常。当我尝试为所选账户请求令牌时,Google授权对话框会显示相应参数;然而,当我使用该对话框授权应用程序并重新请求令牌时,GoogleAuthUtil.getToken(...)再次抛出一个UserRecoverableAuthException(NeedPermission,而不是GooglePlayServicesAvailabilityException),让我批准同样的对话框!

这种行为出现在运行Android 4.1.1(带有3个Google账户)的三星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;
}
}

1
我在这篇文章发布一年后仍然不断收到重复的UserRecoverableAuthException。OAuth序列不断要求离线权限。我怀疑GoogleAuthUtil.getToken在处理access_type=offline时存在问题。 - grebulon
8个回答

13

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

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");
    }
}

使用额外提供的新的一次性令牌,您可以向服务器提交以正确连接。

希望这能够帮到您!


这个答案应该被标记为正确的。这是处理未获得令牌的适当方式。 - Yash Sampat

5

虽然回复有些晚,但是对于未来有相同担忧的人可能会有所帮助。

教程中提到,当您第一次调用GoogleAuthUtil.getToken()时,它将始终抛出UserRecoverableAuthException异常。第二次将会成功。

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)protected void onActivityResult(int requestCode, int responseCode, Intent intent)中各执行一次new GetAuthTokenFromGoogle().execute();
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)  
        { 
        }
    }

你认为这个方法是“正确的做法”还是“看起来运行良好的一种方式”?此外,如果用户取消一个或两个对话框(一次或多次),这个方法是否仍然有效? - Dev-iL
这在我的情况下效果很好。在第一次调用GoogleAuthUtil.getToken()时,用户将被呈现脱机访问同意屏幕。一旦用户点击“确定”按钮,他将看到“正在登录”屏幕(仅一次),显示可能需要几分钟的信息。此后,用户将不会看到任何屏幕,第二次调用GoogleAuthUtil.getToken()将返回访问代码。 - appdroid

2

我通过使用基于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还是其他什么? - Arkaaito
是的,我正在使用 WebView……不过它不是最好的实现,因为无论用户是否已经在手机上使用 G+ 帐户登录,他们总是需要在 WebView 中独立输入详细信息。我希望本机登录可以得到解决。 - user2608643

2

最近我也遇到了同样的问题 - 看起来是特定于设备的(我在一个S3上每次都出现这个问题,但在另一个运行相同操作系统的S3上没有出现这个问题,即使使用相同的账户)。我的猜测是这是客户端应用程序的一个bug,可能是G+应用或Google Play Services应用。我通过恢复工厂设置(摩托罗拉Defy)来解决了其中一个设备上的问题,然后重新安装Google Play Services应用程序,但对于用户来说这完全是一个无用的解决方案。


2

编辑(2013年8月6日):对我来说,这个问题似乎已经被修复了,没有任何更改我的代码。

我能看到的第一个潜在问题是,在你获取onConnected()回调之后调用GoogleAuthUtil.getToken()。这是一个问题,因为使用GoogleAuthUtil.getToken()请求服务器的授权码总是会向用户显示一个同意屏幕。所以你只应该为新用户获取授权码,并在解决PlusClient的任何连接失败之前,在服务器上获取授权码并交换它,以避免向新用户显示两个同意屏幕。

其次,请确保你实际需要一个PlusClient和一个授权码用于你的服务器。只有当你打算从Android客户端你的服务器调用Google APIs时,才需要获取PlusClient和授权码。就像在这个答案中解释的那样。

这些问题只会导致显示两个同意对话框(这显然不是无限循环)-你看到的同意对话框超过两个吗?


好的。我对此感到困惑。似乎我对于用户批准后如何检索令牌存在根本误解 - 如果我不通过重复调用getToken()来检索令牌,那么我该如何检索它呢?(传递给onActivityResult()的意图似乎没有包含任何额外信息,并且我在API文档中也没有找到其他相关方法。) - Arkaaito
不,你第一次的想法是正确的。在处理UserRecoverableAuthExceptions后,你需要重复调用getToken()来获取令牌。但这并不是多个同意对话框出现的原因。双重同意发生的原因是因为你可能需要解决UserRecoverableAuthException才能成功从PlusClient.connect()中获得onConnected。但你随后保证会收到另一个授权码调用的同意对话框。 - Lee
1
你是正确的 - 不幸的是,你不能使用内置的账户选择器。相反,你需要使用AccountPicker.newChooseAccountIntent()来呈现自己的账户选择器,然后在创建PlusClient时使用PlusClient.Builder.setAccountName()来设置账户名称。 - Lee
啊,好的。我换了(但是我仍然看到相同的行为 - 即使完全删除了PlusClient和相关回调函数)。 - Arkaaito
1
Google Play服务版本肯定是一样的,所以我猜这是服务器端的更改。:-/ - Lee
显示剩余5条评论

1
我曾遇到一个类似的问题,表现为一个看似认证循环导致 "正在登录..." 和权限请求对话框不断弹出,同时反复出现上述异常。
这个问题在一些稍作修改的示例代码中出现,我(以及其他人,我猜)从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!");
}

请注意我有一个{可能是多余的}双重保险来防止任务执行多次:
  1. if(task.getStatus() != AsyncTask.Status.RUNNING){...} - 确保在尝试执行任务之前任务未在运行。
  2. task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);- 确保此任务的副本是“同步”的(即,一个队列已经建立,以便在任何给定时间只能执行此类型的一个任务)。
附注: 小澄清:LogHelper.log('e',...) 等同于 Log.e(...)

0

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

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

0

我曾经遇到过权限请求无限循环的同样问题。原因是我的手机时间不准确。当我选择自动检测时间时,这个问题就消失了。希望这能帮到你!


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