выполнить доступ к API-интерфейсу Google Sheets с учетными данными Android GoogleSignInApi

Я знаю, что есть несколько постов, посвященных входу в систему/oauth для Android, но ни один из них не решает мою проблему.

Я изо всех сил пытаюсь согласовать поток аутентификации с GoogleSignInApi с использованием AccountManager в учебнике Google Sheets v4.

Используя GoogleSignInApi, я получаю код авторизации. Все идет нормально. Далее в документах рекомендуется обменять код авторизации на токен авторизации/восстановления. https://developers.google.com/identity/sign-in/android/offline-access есть отличный пример того, как отправить код авторизации в серверную часть для обмена.

Единственная проблема с этим потоком — у меня нет собственного бэкэнда, так как я просто хочу получить доступ к API-интерфейсу Google Sheets. Вызов API листов ожидает объект GoogleCredential, который я не могу получить из кода авторизации или иным образом через объект GoogleSignInAccount.

Итак, мои вопросы:

  1. Куда я могу отправить код авторизации, который я получил через GoogleSignInApi, чтобы обменять его на токен аутентификации.
  2. Есть ли библиотека, которая обрабатывает запрос на обмен и магию обновления, или я должен поймать токен обновления и сам выдать другой запрос токена аутентификации.
  3. Есть ли лучший способ получить правильные учетные данные для доступа к листам, а также использовать GoogleSignInApi для служб Firebase?
  4. Если я в конечном итоге использую GoogleAuthorizationCodeTokenRequest, как рекомендуется для доступа на стороне сервера, приемлемо ли использовать секрет клиента в клиенте? Возможно нет.

Вот упрощенная версия вызова API листов, который я пытаюсь сделать.

GoogleCredential credential = new GoogleCredential().setAccessToken("TEST_ACCESS_TOKEN_FROM_OAUTH_PLAYGROUND");

mService = new com.google.api.services.sheets.v4.Sheets.Builder(
                        transport, jsonFactory, credential)
                        .setApplicationName("Google Sheets API Android Quickstart")
                        .build();

ОБНОВЛЕНИЕ: чтобы добиться некоторого прогресса, я реализовал поток на стороне сервера для обмена токеном. Я почти уверен, что это неправильный метод, поскольку он требует использования client_secret в приложении.

Часть 1: SignInActivity основана на лаборатории кода Firebase. Мне нужна учетная запись firebase, поэтому я чувствую, что должен использовать GoogleSignInApi.

public class SignInActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener,
        View.OnClickListener {


    private static final int RC_SIGN_IN = 9001;

    private GoogleApiClient mGoogleApiClient;
    private FirebaseAuth mFirebaseAuth;

    public static final String PREF_ACCOUNT_NAME = "accountName";
    public static final String PREF_ID_TOKEN = "idToken";
    public static final String PREF_AUTH_CODE = "authCode";

    public static final Scope SHEETS_SCOPE = new Scope(SheetsScopes.SPREADSHEETS);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sign_in);

        SignInButton signInButton = (SignInButton) findViewById(R.id.sign_in_button);
        signInButton.setOnClickListener(this);

        Log.d(TAG, getString(R.string.default_web_client_id));

        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestScopes(SHEETS_SCOPE)
                .requestServerAuthCode(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();

        // Initialize FirebaseAuth
        mFirebaseAuth = FirebaseAuth.getInstance();
    }

    private void handleFirebaseAuthResult(AuthResult authResult) {
        // ...
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.sign_in_button:
                signIn();
                break;
            default:
                return;
        }
    }

    private void signIn() {
        Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

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

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            if (result.isSuccess()) {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = result.getSignInAccount();

                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext());
                SharedPreferences.Editor editor = prefs.edit();
                editor.putString(PREF_ACCOUNT_NAME, account.getEmail());
                editor.putString(PREF_ID_TOKEN, account.getIdToken());
                editor.putString(PREF_AUTH_CODE, account.getServerAuthCode());               
                editor.apply();

                // TODO: it would be great to do the exchange of the authcode now but it's doing a
                // network call and can't be on the main thread.

                // I really need this one
                firebaseAuthWithGoogle(account);
            } else {
                // Google Sign In failed
                Log.e(TAG, "Google Sign In failed.");
            }
        }
    }

    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId());
        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mFirebaseAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                        // If sign in fails, display a message to the user. If sign in succeeds
                        // the auth state listener will be notified and logic to handle the
                        // signed in user can be handled in the listener.
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "signInWithCredential", task.getException());
                            Toast.makeText(SignInActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        } else {
                            startActivity(new Intent(SignInActivity.this, MainActivity.class));
                            finish();
                        }
                    }
                });
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        // An unresolvable error has occurred and Google APIs (including Sign-In) will not
        // be available.
        Log.d(TAG, "onConnectionFailed:" + connectionResult);
        Toast.makeText(this, "Google Play Services error.", Toast.LENGTH_SHORT).show();
    }
}

Часть 2. DataManager — это служебный класс, который используется приложением для доступа к данным листов. Он не использует поток, рекомендованный в лаборатории кода листов, поскольку он не позволяет мне настроить учетную запись firebase с теми же данными пользователя.

public class DataManager {

    public static final String UNDEF = "undefined";

    private com.google.api.services.sheets.v4.Sheets mService = null;
    // this is the play copy
    private static String mSheetID = SHEET_ID;

    private static final String PREF_ACCESS_TOKEN = "accessToken";
    private static final String PREF_REFRESH_TOKEN = "refreshToken";
    private static final String PREF_EXPIRES_IN_SECONDS = "expiresInSec";

    private Context mContext;
    private String mAccessToken;
    private String mRefreshToken;
    private Long mExpiresInSeconds;
    private String mAuthCode;

    public DataManager(Context context) {
        mContext = context;

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
        mAuthCode = prefs.getString(SignInActivity.PREF_AUTH_CODE, UNDEF);
        mAccessToken =  prefs.getString(PREF_ACCESS_TOKEN, UNDEF);
        mRefreshToken = prefs.getString(PREF_REFRESH_TOKEN, UNDEF);
        mExpiresInSeconds = prefs.getLong(PREF_EXPIRES_IN_SECONDS, 0);
    }

    private void exchangeCodeForToken(String authCode) {

        try {
            GoogleTokenResponse tokenResponse =
                    new GoogleAuthorizationCodeTokenRequest(
                            new NetHttpTransport(),
                            JacksonFactory.getDefaultInstance(),
                            "https://www.googleapis.com/oauth2/v4/token",
                            mContext.getString(R.string.default_web_client_id),
                            // TODO: the app shouldn't have to use the client secret
                            {CLIENT_SECRET},
                            authCode,
                            "")
                            .execute();

            mAccessToken = tokenResponse.getAccessToken();
            mRefreshToken = tokenResponse.getRefreshToken();
            mExpiresInSeconds = tokenResponse.getExpiresInSeconds();

            // TODO: do I really need to store and pass the three values individually?
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
            SharedPreferences.Editor editor = prefs.edit();
            editor.putString(PREF_ACCESS_TOKEN, mAccessToken);
            editor.putString(PREF_REFRESH_TOKEN, mRefreshToken);
            editor.putLong(PREF_EXPIRES_IN_SECONDS, mExpiresInSeconds);
            editor.remove(SignInActivity.PREF_AUTH_CODE);
            editor.apply();

        } catch (Exception e) {
            Log.e(TAG, "Token exchange failed with " + e.getMessage());
        }
    }

    private void refreshAccessToken(String refreshToken) {
        try {
            // TODO: what to do here?
            throw new Exception("TBD");
        } catch (Exception e) {
            Log.e(TAG, "Token refresh failed with " + e.getMessage());
        }
    }

    private GoogleCredential getCredential() {

        if (mAuthCode != UNDEF) {
            exchangeCodeForToken(mAuthCode);
        }

        // TODO: handle missing or expired token
        if (mRefreshToken !=  UNDEF && mExpiresInSeconds < 30) {
            refreshAccessToken(mRefreshToken);
        }

        GoogleCredential credential = new GoogleCredential.Builder()
                .setTransport(new NetHttpTransport())
                .setJsonFactory(JacksonFactory.getDefaultInstance())
                .build();
        credential.setAccessToken(mAccessToken);
        if (mRefreshToken !=  UNDEF) {
            credential.setRefreshToken(mRefreshToken);
            credential.setExpiresInSeconds(mExpiresInSeconds);
        }

        return credential;
    }

    // Set up credential and service object, then issue api call.
    public ArrayList<Foo> getFooListFromServer() throws IOException {
        try {
            GoogleCredential credential = getCredential();

            HttpTransport transport = AndroidHttp.newCompatibleTransport();
            JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
            mService = new com.google.api.services.sheets.v4.Sheets.Builder(
                    transport, jsonFactory, credential)
                   .setApplicationName(mContext.getString(R.string.app_name))
                    .build();

            return getDataFromServer();
        } catch (IOException exception) {
            // ...
            throw exception;
        } catch (Exception e) {
            Log.e(TAG, "something else is going on " + e.toString());
            throw e;
        }
    }

    /**
     * Actually fetch the data from google
     *
     * @return List of Foos
     * @throws IOException
     */
    private ArrayList<Foo> getDataFromServer() throws IOException {

        ArrayList<Foo> foos = new ArrayList<Foo>();

        ValueRange response = this.mService.spreadsheets().values()
                .get(mSheetID, mRange)
                .setValueRenderOption("UNFORMATTED_VALUE")
                .setDateTimeRenderOption("FORMATTED_STRING")
                .execute(); 
        //...
        return foos;
    }
}

person duffy    schedule 21.11.2016    source источник


Ответы (2)


Проблем с учетными данными можно легко избежать, если вы используете Android Quickstart for Sheets API.

Вот шаги, упомянутые в руководстве:

Step 1: Acquire a SHA1 fingerprint
Step 2: Turn on the Google Sheets API
Step 3: Create a new Android project
Step 4: Prepare the project
Step 5: Setup the sample

Идентификатор клиента OAuth можно найти в Google Dev Console.

person noogui    schedule 23.11.2016
comment
Я согласен, быстрый старт помогает с доступом к таблицам, и у меня это работает без проблем. Однако мне также необходимо пройти аутентификацию с помощью firebase, чтобы использовать некоторые из их компонентов, а учетные данные, которые использует быстрый запуск (GoogleAccountCredential), не позволяют этого. Мой первоначальный вопрос заключался в том, как объединить учетные данные из учебника по листам с учетными данными, необходимыми для учебника по firebase (полученные там через GoogleSignInApi.) - person duffy; 25.11.2016

Я знаю, что очень опоздал с этим ответом... но я просто хочу всем помочь. После трехдневного RnD я, наконец, нашел рабочее решение для токена Refresh, равного null в oAuth Google через вход google в android

Все используют это:

GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestScopes(SHEETS_SCOPE)
                .requestServerAuthCode(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();

добавить еще один параметр:

GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestScopes(SHEETS_SCOPE)
                .requestServerAuthCode(getString(R.string.default_web_client_id), true) //For offline access or background access token generation
                .requestEmail()
                .build();

после этого вы получите токен авторизации сервера, который возвращает refresh_token вместе с access_token

person KKSINGLA    schedule 23.04.2020