如何在 Android 设备中从 Gmail 登录后获取访问令牌?

43

我正在遵循Google Android登录。现在我可以获取idToken,但是之前我使用的后端服务器期望访问令牌,因为我之前使用的是Google+ 登录。现在我不想更改我的服务器端。但是,我该如何在我的android应用程序中使用Google登录并获取访问令牌,以便我可以向我的后端服务器验证用户。

我以前使用的是GooglePlay服务7.5.0,现在我正在使用最新的GooglePlay服务8.3.0。


你尝试过使用 GoogleSignInAccount 对象的 getServerAuthCode() 方法吗? - Nouman Ghaffar
看起来 getServerAuthCode() 会给你一个一次性的授权码,你需要将其上传到你的服务器。然后你的服务器使用这个授权码请求 AccessToken。 - scottyab
您想使用G+登录,并在登录后获取Google身份验证令牌。 - Rahul Chaudhary
7个回答

81

为了满足您的要求,您可以使用以下代码:

首先,请确保您拥有一个有效的Web OAuth 2.0客户端ID:

<!-- Server Client ID.  This should be a valid Web OAuth 2.0 Client ID obtained
         from https://console.developers.google.com/ -->
    <string name="server_client_id">...e4p8.apps.googleusercontent.com</string>

然后在 Activity 类内部:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ...

    // For sample only: make sure there is a valid server client ID.
    validateServerClientID();

    // [START configure_signin]
    // Configure sign-in to request offline access to the user's ID, basic
    // profile, and Google Drive. The first time you request a code you will
    // be able to exchange it for an access token and refresh token, which
    // you should store. In subsequent calls, the code will only result in
    // an access token. By asking for profile access (through
    // DEFAULT_SIGN_IN) you will also get an ID Token as a result of the
    // code exchange.
    String serverClientId = getString(R.string.server_client_id);
    GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestScopes(new Scope(Scopes.DRIVE_APPFOLDER))
            .requestServerAuthCode(serverClientId)
            .requestEmail()
            .build();
    // [END configure_signin]

    // Build GoogleAPIClient with the Google Sign-In API and the above options.
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
            .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
            .build();
}

private void getAuthCode() {
    // Start the retrieval process for a server auth code.  If requested, ask for a refresh
    // token.  Otherwise, only get an access token if a refresh token has been previously
    // retrieved.  Getting a new access token for an existing grant does not require
    // user consent.
    Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
    startActivityForResult(signInIntent, RC_GET_AUTH_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == RC_GET_AUTH_CODE) {
        GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
        Log.d(TAG, "onActivityResult:GET_AUTH_CODE:success:" + result.getStatus().isSuccess());

        if (result.isSuccess()) {
            // [START get_auth_code]
            GoogleSignInAccount acct = result.getSignInAccount();
            String authCode = acct.getServerAuthCode();

            // Show signed-in UI.
            mAuthCodeTextView.setText(getString(R.string.auth_code_fmt, authCode));
            updateUI(true);

            // TODO(user): send code to server and exchange for access/refresh/ID tokens.
            // [END get_auth_code]
        } else {
            // Show signed-out UI.
            updateUI(false);
        }
    }
}
您可以在以下ServerAuthCodeActivity.java中查看完整的代码。
如果使用该示例,则结果看起来如下屏幕截图: BNK's screenshot 然后,您可以按照Google文档中提到的步骤进行操作(从第3步开始,使用HTTPS POST将认证代码发送到应用程序的后端):

Android上的Google登录-启用服务器端访问


更新:根据评论,如果要直接从Android客户端应用程序获取访问令牌,请使用以下示例代码(替换为您的client_id、client_secret和auth code)。
OkHttpClient client = new OkHttpClient();
    RequestBody requestBody = new FormEncodingBuilder()
            .add("grant_type", "authorization_code")
            .add("client_id", "812741506391-h38jh0j4fv0ce1krdkiq0hfvt6n5amrf.apps.googleusercontent.com")
            .add("client_secret", "{clientSecret}")
            .add("redirect_uri","")
            .add("code", "4/4-GMMhmHCXhWEzkobqIHGG_EnNYYsAkukHspeYUk9E8")
            .build();
    final Request request = new Request.Builder()
            .url("https://www.googleapis.com/oauth2/v4/token")
            .post(requestBody)
            .build();
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(final Request request, final IOException e) {
            Log.e(LOG_TAG, e.toString());                
        }

        @Override
        public void onResponse(Response response) throws IOException {
            try {
                JSONObject jsonObject = new JSONObject(response.body().string());
                final String message = jsonObject.toString(5);
                Log.i(LOG_TAG, message);                    
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    });

请使用compile 'com.squareup.okhttp:okhttp:2.6.0'(版本3-RC1将有不同的类)

如果响应成功,您将在logcat中获得以下信息:

I/onResponse: {
              "expires_in": 3600,
              "token_type": "Bearer",
              "refresh_token": "1\/xz1eb0XU3....nxoALEVQ",
              "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjQxMWY1Ym......yWVsUA",
              "access_token": "ya29.bQKKYah-........_tkt980_qAGIo9yeWEG4"
         }

2
没错,这就是我想要的。100声望赏金是为了一个可用的代码示例。使用GoogleSignInOptions和返回的GoogleSignInAccount的详细信息。谢谢。 - scottyab
2
非常好的回答。即使在官方文档上也找不到解决这个问题的地方。不知道为什么 Android 没有直接公开一个获取访问令牌的公共 API? - thedarkpassenger
2
@RobertoFrontado 我之前也遇到了类似的问题。请求中缺少了"id_token"参数。一旦我添加了这个参数,请求就成功了。请查看我的答案以获取详细信息(只需在此答案中添加一行即可)! - Narayan Acharya
1
我已经发送了相同的请求 - grant_typeclient_idclient_secretcodeid_token,但仍然收到错误消息 - {"error":"invalid_client","error_description":"Unauthorized"}。客户端ID和密钥是Web客户端的。有任何想法吗??? - iMDroid
1
谢谢。获取访问令牌的完美方式。 - Kunwar Shekhar Singh
显示剩余21条评论

8
这是我的使用Kotlin的方法(这是我在StackOverflow上第一个回答,如果有错误、遗漏或者我能做得更好的地方,请告诉我)。在登录活动中:
private fun configureGoogleSignIn() {
    mGoogleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestIdToken(getString(R.string.default_web_client_id))
        .requestServerAuthCode(getString(R.string.server_client_id_oauth))
        .requestEmail()
        .build()
    mGoogleSignInClient = GoogleSignIn.getClient(this, mGoogleSignInOptions)
}

private fun signInWithGoogle() {
    val signInIntent: Intent = mGoogleSignInClient.signInIntent
    startActivityForResult(signInIntent, RC_SIGN_IN)
}

请确保在OnCreate函数中调用configureGoogleSignIn()函数。

然后,获取结果。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    callbackManager?.onActivityResult(requestCode, resultCode, data)


    if (requestCode == RC_SIGN_IN) {
        val tag = "onActivityResult RC_SIGN_IN"
        val task: Task<GoogleSignInAccount> = GoogleSignIn.getSignedInAccountFromIntent(data)
        try {
            val account = task.getResult(ApiException::class.java)
            firebaseAuthWithGoogle(account!!)
            getIdTokenFromFirebaseAuth()

            var acct = GoogleSignIn.getLastSignedInAccount(this)
            if (acct != null) {
                var personName = acct.displayName
                firstName = acct.givenName!!
                lastName = acct.familyName!!
                userEmail = acct.email!!
                authCode = acct.serverAuthCode!! //THIS is what you looking for
                googleIdToken2 = acct.idToken!!
                Log.d(tag, authCode)
                Log.d(tag, googleIdToken2)
                var personId = acct.id
                //todo pegar foto do google e por no cadastro do usuario
                var personPhoto = acct.photoUrl
                spinner.visibility = View.GONE
                getGoogleAccessToken()
            }
        } catch (e: ApiException) {
            spinner.visibility = View.GONE
            infoToUserTextView.text = getString(R.string.ops_we_had_a_problem)
        }
    }
}

然后使用谷歌API(我使用Retrofit)进行调用,使用此接口进行操作:
@FormUrlEncoded
@POST
fun getAccessTokenGoogle(
    @Url url: String,
    @Field("grant_type") grant_type: String,
    @Field("client_id") client_id: String,
    @Field("client_secret") client_secret: String,
    @Field("redirect_uri") redirect_uri: String,
    @Field("code") authCode: String,
    @Field("id_token") id_token: String
):Call<GoogleSignInAccessTokenDataClass>

这里是GoogleSignInAccessTokenDataClass

data class GoogleSignInAccessTokenDataClass(
val access_token: String,
val expires_in: Int,
val id_token: String,
val token_type: String

决定登录活动的调用

private fun getGoogleAccessToken(){
    val call = RetrofitGet().userInfoGson().getAccessTokenGoogle(
        grant_type = "authorization_code", client_id = getString(R.string.server_client_id_oauth),
        client_secret = getString(R.string.server_client_secret_oauth), redirect_uri = "",
        authCode = authCode, id_token =googleIdToken2, url = googleTokenUrl
    )

    call.enqueue(object : Callback<GoogleSignInAccessTokenDataClass>{
        val tag = "getGoogleAccessToken"
        override fun onFailure(call: Call<GoogleSignInAccessTokenDataClass>, t: Throwable) {
            Log.e(tag, t.toString())
        }

        override fun onResponse(
            call: Call<GoogleSignInAccessTokenDataClass>,
            response: Response<GoogleSignInAccessTokenDataClass>
        ) {
            if (response.isSuccessful){
                val responseBody = response.body()
                googleAccessToken = responseBody!!.access_token
                Log.d(tag, googleAccessToken)
            }else{
                try {
                    val responseError = response.errorBody()!!.string()
                    Log.e(tag, responseError)
                }catch (e:Exception){Log.e(tag, e.toString())}
            }
        }
    })
}

如何获取client_secret?我们有client_secret的文件,但没有可用的字符串。 - undefined

7

在大部分情况下,BNK的解答是正确的。Activity类与BNK的答案相同,只需在onActivityResult()方法中添加OkHttp部分即可。

但是,在使用OkHttp请求部分仍然出现了错误。最终在Postman中测试和尝试了一段时间后(也有一些运气因素),我发现我漏掉了id_token参数。这个OkHttp请求缺少一个参数,即id_token。使用从GoogleSignInAccount获得的ID token,就像这样:

GoogleSignInAccount acct = result.getSignInAccount();
String idTokenString = acct.getIdToken();

现在,您可以将idTokenString与BNK答案中的所有参数一起在OkHttp部分中使用,就像这样。
...

RequestBody requestBody = new FormEncodingBuilder()
            .add("grant_type", "authorization_code")
            .add("client_id", "alpha-numeric-string-here.apps.googleusercontent.com")
            .add("client_secret", "{clientSecret}")
            .add("redirect_uri","")
            .add("code", "4/4-alphabetic-string-here")
            .add("id_token", idTokenString) // Added this extra parameter here
            .build();

...

你得到的响应与BNK的答案相同

{
  "access_token": "ya29.CjBgA_I58IabCJ...remainingAccessTokenHere",
  "token_type": "Bearer",
  "expires_in": 3577,
  "id_token": "eyJhbGciOiJS...veryLongStringHere"
}

现在,将此访问令牌发送到您的后端服务器进行身份验证,就像您以前在使用GoogleAuthUtil和PlusAPI时所做的一样。

希望这可以帮助您 :) 特别感谢BNK!


谢谢:),我有空的时候会查看文档。也许现在已经有一些变化,因为我发布答案的时间已经过去了。 - BNK
啊,你的响应中没有包含“refresh_token”。你使用的是和我相同的API端点吗?即'https://www.googleapis.com/oauth2/v4/token'。 - BNK
是的,我确实使用了相同的端点。嗯,奇怪为什么我没有收到 refresh_token。之前没有注意到 :| - Narayan Acharya
嗯,一些链接可能会有不同的大小写,请阅读 https://developers.google.com/identity/protocols/OpenIDConnect 和 https://developers.google.com/identity/protocols/OAuth2ForDevices 以及 https://developers.google.com/identity/protocols/OAuth2InstalledApp。下周我会检查我的代码。 - BNK
谢谢!如果你找到了什么,请告诉我! - Narayan Acharya
严肃点吧?你必须在代码中使用你的客户端密钥吗? - Emil

5

这是在Android中获取accessToken的最简单方法。

 val httpTransport = AndroidHttp.newCompatibleTransport()
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()

tokenResponse = GoogleAuthorizationCodeTokenRequest(
                httpTransport,
                jsonFactory,
                "https://www.googleapis.com/oauth2/v4/token",
                clientId,
                clientSecret,
                account.serverAuthCode,
                "" //optional param (redirect url)
            ).execute()

将其在后台线程上运行

Android使用这些库

    implementation 'com.google.android.gms:play-services-auth:19.0.0'

  implementation('com.google.api-client:google-api-client-android:1.23.0') {
    exclude group: 'org.apache.httpcomponents'
  }

太棒了,运行得非常顺利。不需要写冗长的代码。</br> 为什么谷歌不能在官方文档中修正它呢! - undefined

5
感谢@BNK,他提供了可行的解决方案。这里有一个官方指南,介绍如何从“授权代码”获取“访问令牌”:https://developers.google.com/identity/protocols/OAuth2WebServer#exchange-authorization-code 在这里,我想用纯Android SDK类提供我的解决方案。如果您不想为此目的添加花哨的库:
private String mAccessToken;
private long mTokenExpired;

private String requestAccessToken(GoogleSignInAccount googleAccount) {
    if (mAccessToken != null && SystemClock.elapsedRealtime() < mTokenExpired) return mAccessToken;
    mTokenExpired = 0;
    mAccessToken = null;

    HttpURLConnection conn = null;
    OutputStream os = null;
    InputStream is = null;
    InputStreamReader isr = null;
    BufferedReader br = null;

    try {
        final URL url = new URL("https://www.googleapis.com/oauth2/v4/token");
        conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setUseCaches(false);
        conn.setDoInput(true);
        conn.setDoOutput(true);
        conn.setConnectTimeout(3000);
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

        final StringBuilder b = new StringBuilder();
        b.append("code=").append(googleAccount.getServerAuthCode()).append('&')
         .append("client_id=").append(getString(R.string.default_web_client_id)).append('&')
         .append("client_secret=").append(getString(R.string.client_secret)).append('&')
         .append("redirect_uri=").append("").append('&')
         .append("grant_type=").append("authorization_code");

        final byte[] postData = b.toString().getBytes("UTF-8");

        os = conn.getOutputStream();
        os.write(postData);

        final int responseCode = conn.getResponseCode();
        if (200 <= responseCode && responseCode <= 299) {
            is = conn.getInputStream();
            isr = new InputStreamReader(is);
            br = new BufferedReader(isr);
        } else {
            Log.d("Error:", conn.getResponseMessage());
            return null;
        }

        b.setLength(0);
        String output;
        while ((output = br.readLine()) != null) {
            b.append(output);
        }

        final JSONObject jsonResponse = new JSONObject(b.toString());
        mAccessToken = jsonResponse.getString("access_token");
        mTokenExpired = SystemClock.elapsedRealtime() + jsonResponse.getLong("expires_in") * 1000;
        return mAccessToken;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (os != null) {
            try {
                os.close();
            } catch (IOException e) {
            }
        }
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
            }
        }
        if (isr != null) {
            try {
                isr.close();
            } catch (IOException e) {
            }
        }
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
            }
        }
        if (conn != null) {
            conn.disconnect();
        }
    }
    return null;
}

在后台线程上运行此方法。你需要从 Google APIs 控制台获取 client_idclient_secret。请参考以下截图:Google APIs console id and secret

2
如果有人在从谷歌获取访问令牌时遇到问题,以下是一个经过测试和有效的方法,截至2018年11月1日。使用retrofit2。
首先,这是关于令牌交换端点的谷歌文档链接:https://developers.google.com/identity/protocols/OAuth2WebServer#exchange-authorization-code
public interface GoogleService {

@POST("token")
@FormUrlEncoded
@Headers("Content-Type:application/x-www-form-urlencoded")
Call<GoogleAuthData> getToken(
        @Field("grant_type") String grantType,
        @Field("client_id") String clientId,
        @Field("client_secret") String clientSecret,
        @Field("redirect_uri") String redirectUri,
        @Field("code") String code);
}

那么就这样调用它:
Call<GoogleAuthData> call = RetroClient.getGoogleService().getToken(
            "authorization_code", context.getString(R.string.server_client_id),
            context.getString(R.string.server_client_secret), "", authCode);

@Suresh的代码是ServerAuthCode: signInOptions.requestServerAuthCode(getString(R.string.default_web_client_id)); .... onSuccess{ GoogleSignInAccount.getServerAuthCode(); <<< - Oleksandr Albul

1
我找到了一个获取访问令牌的方法,不需要 idToken、code、secret 或任何请求(例如向 "https://www.googleapis.com/oauth2/v4/token" 发送 post 请求)。 你只需要 "客户端 ID"。 按照以下步骤操作:
  1. Use "GoogleSignIn" to get sign in and get the "Account" object.

    GoogleSignIn.getClient(
            ctx,
            GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                    .requestEmail()
                    .requestProfile()
                    .requestIdToken(KEY.GOOGLE_CLIENT_ID)
                    .requestServerAuthCode(KEY.GOOGLE_CLIENT_ID, true)
                    .build())
            .let { client ->
                client.signOut()
                    .let { task ->
                        Observable.create<GoogleSignInClient> { ob ->
                            task.addOnCompleteListener { ob.onNext(client) }
                        }
                    }
            }
            .flatMap {
                ctx.startActivityForResult(it.signInIntent, RC_SIGN_IN)
                ctx.activityResultObservable
            }
            .filter { it.requestCode == RC_SIGN_IN }
            .map {
                GoogleSignIn
                        .getSignedInAccountFromIntent(it.data)
                        .getResult(ApiException::class.java)
            }
    

我在此使用RxJava编写代码,但你也可以不使用它来编写代码。

  1. Within the "Account" object, you can get the access token by using "GoogleAuthUtil".

            .flatMap { result ->
                Observable.create<AuthData> {
                    val scope = "oauth2:https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/userinfo.profile"
                    val accessToken = GoogleAuthUtil.getToken(context, result.account, scope)
                    // now you can use this token
                    it.onNext(accessToken)
                }
            }
    
函数“GoogleAuthUtil :: getToken”会发出请求,因此您不能在UI线程中运行它。现在,您可以将此令牌发送到您的服务器。

2
GoogleAuthUtil.getToken - 已经很久以前被弃用了。并且最终将停止工作。 - Oleksandr Albul
@OleksandrAlbul 官方文档表示这不是一个已弃用的API。https://developers.google.com/android/reference/com/google/android/gms/auth/GoogleAuthUtil.html#getToken(android.content.Context,%20java.lang.String,%20java.lang.String,%20android.os.Bundle) - yuriel
1
@yuriel 现在可以了。 - PhilBa

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