文章

Google Identity

Google Identity

Authentication 认证

  • Authentication
  • Legacy Sign In
  • Sign In with Google SDKs
  • Industry standards

Google Sign In (Google 登录,过时)

Google Sign-In for Android 已经过时了,现在用 Google Identity Services One Tap sign-in/sign-up

Google Sign-In API (Legacy)

准备工作 配置 Google Console API

Add Google Play services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// project's top-level build.gradle
allprojects {
    repositories {
        google()
        // If you're using a version of Gradle lower than 4.1, you must instead use:
        // maven {
        //     url 'https://maven.google.com'
        // }
    }
}

// app-level build.gradle
apply plugin: 'com.android.application'
// ...
dependencies {
    implementation 'com.google.android.gms:play-services-auth:20.5.0'
}
Configure a Google API Console project
Get your backend server’s OAuth 2.0 client ID

Sign-in 用的是 Web Client Id
image.png

Integrating Google Sign-In into Your Android App

Configure Google Sign-in and the GoogleSignInClient object 配置请求登录要获取的基本信息
  • GoogleSignInOptions 配置必要的信息,请求 id 及用户的基本信息
  • 如果需要请求其他 scope 的信息,加上 requestScopes()(尽可能请求获取所需的最小数据原则),具体可参考 Requesting Additional Scopes
  • 配置好后,得到一个 GoogleSignInClient 实例
Check for an existing signed-in user 检测是否已经登录过
1
2
3
4
// Check for existing Google Sign In account, if the user is already signed in
// the GoogleSignInAccount will be non-null.
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
updateUI(account);

返回了 GoogleSignInAccount 不为 null 说明之前已经登录过了,否则就是没有登录过
如果需要检查用户账号的状态,用下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 *  If you need to detect changes to a user's auth state that happen outside your app, such as access token or ID token revocation, or to perform cross-device sign-in, you might also call GoogleSignInClient.silentSignIn when your app starts.
 */
public static void silentSignIn() {
    if (googleApiClient == null) return;
    Task<GoogleSignInAccount> googleSignInAccountTask = googleApiClient.silentSignIn();
    googleSignInAccountTask.addOnSuccessListener(new OnSuccessListener<GoogleSignInAccount>() {
        @Override
        public void onSuccess(GoogleSignInAccount account) {
            if (account == null) return;
        }
    });
}

Add the Google Sign-in button to your app 用自带登录按钮(也可以自定义登录按钮)

  • xml
1
2
3
4
<com.google.android.gms.common.SignInButton
 android:id="@+id/sign_in_button"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" />
  • 配置
1
2
3
// Set the dimensions of the sign-in button.
SignInButton signInButton = findViewById(R.id.sign_in_button);
signInButton.setSize(SignInButton.SIZE_STANDARD);
  • 效果

image.png

Start the sign-in flow 开始登录

  • 发起登录请求
1
2
3
4
5
private void signIn() {
    Intent signInIntent = mGoogleSignInClient.getSignInIntent();
    startActivityForResult(signInIntent, RC_SIGN_IN);
}

  • 请求回调到onActivityResult
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    // Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
    if (requestCode == RC_SIGN_IN) {
        // The Task returned from this call is always completed, no need to attach
        // a listener.
        Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
        handleSignInResult(task);
    }
}
private void handleSignInResult(Task<GoogleSignInAccount> completedTask) {
    try {
        GoogleSignInAccount account = completedTask.getResult(ApiException.class);

        // Signed in successfully, show authenticated UI.
        updateUI(account);
    } catch (ApiException e) {
        // The ApiException status code indicates the detailed failure reason.
        // Please refer to the GoogleSignInStatusCodes class reference for more information.
        Log.w(TAG, "signInResult:failed code=" + e.getStatusCode());
        updateUI(null);
    }
}
  • getEmail() 获取 user’s email address
  • getId() 获取 user’s Google ID (for client-side use)
  • getToken() 获取 ID token
  • 将登录获取的信息传递到自己的后端

Authenticate with a backend server

获取 id token
1
2
3
4
5
6
7
8
// Request only the user's ID token, which can be used to identify the
// user securely to your backend. This will contain the user's basic
// profile (name, profile picture URL, etc) so you should not need to
// make an additional call to personalize your application.
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestIdToken(getString(R.string.server_client_id))
        .requestEmail()
        .build();
发送给你的后端服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost("https://yourbackend.example.com/tokensignin");

try {
    List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(1);
    nameValuePairs.add(new BasicNameValuePair("idToken", idToken));
    httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
    
    HttpResponse response = httpClient.execute(httpPost);
    int statusCode = response.getStatusLine().getStatusCode();
    final String responseBody = EntityUtils.toString(response.getEntity());
Log.i(TAG, "Signed in as: " + responseBody);
} catch (ClientProtocolException e) {
	Log.e(TAG, "Error sending ID token to backend.", e);
} catch (IOException e) {
	Log.e(TAG, "Error sending ID token to backend.", e);
}

Signing Out Users and Disconnecting Accounts 退出登录

Sign out user
1
2
3
4
5
6
7
8
9
private void signOut() {
    mGoogleSignInClient.signOut()
            .addOnCompleteListener(this, new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    // ...
                }
            });
}
Disconnect accounts
1
2
3
4
5
6
7
8
9
private void revokeAccess() {
    mGoogleSignInClient.revokeAccess()
            .addOnCompleteListener(this, new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    // ...
                }
            });
}

Enabling Server-Side Access

  • requestServerAuthCode 来请求 ServerAuthCode
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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();
1
2
3
4
5
6
7
8
9
10
11
12
13
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
try {
    GoogleSignInAccount account = task.getResult(ApiException.class);
    String authCode = account.getServerAuthCode();

    // Show signed-un UI
    updateUI(account);

    // TODO(developer): send code to server and exchange for access/refresh/ID tokens
} catch (ApiException e) {
    Log.w(TAG, "Sign-in failed", e);
    updateUI(null);
}
  • 将 ServerAuthCode 发送到你自己的服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HttpPost httpPost = new HttpPost("https://yourbackend.example.com/authcode");

try {
    List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(1);
    nameValuePairs.add(new BasicNameValuePair("authCode", authCode));
    httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

    HttpResponse response = httpClient.execute(httpPost);
    int statusCode = response.getStatusLine().getStatusCode();
    final String responseBody = EntityUtils.toString(response.getEntity());
} catch (ClientProtocolException e) {
    Log.e(TAG, "Error sending auth code to backend.", e);
} catch (IOException e) {
    Log.e(TAG, "Error sending auth code to backend.", e);
}
  • 你的后端拿到这个 ServerAuthCode 来交换获取 access and refresh tokens,用来代表用户来调用 Google APIs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// (Receive authCode via HTTPS POST)


if (request.getHeader("X-Requested-With") == null) {
  // Without the `X-Requested-With` header, this request could be forged. Aborts.
}

// Set path to the Web application client_secret_*.json file you downloaded from the
// Google API Console: https://console.cloud.google.com/apis/credentials
// You can also find your Web application client ID and client secret from the
// console and specify them directly when you create the GoogleAuthorizationCodeTokenRequest
// object.
String CLIENT_SECRET_FILE = "/path/to/client_secret.json";

// Exchange auth code for access token
GoogleClientSecrets clientSecrets =
    GoogleClientSecrets.load(
        JacksonFactory.getDefaultInstance(), new FileReader(CLIENT_SECRET_FILE));
GoogleTokenResponse tokenResponse =
          new GoogleAuthorizationCodeTokenRequest(
              new NetHttpTransport(),
              JacksonFactory.getDefaultInstance(),
              "https://oauth2.googleapis.com/token",
              clientSecrets.getDetails().getClientId(),
              clientSecrets.getDetails().getClientSecret(),
              authCode,
              REDIRECT_URI)  // Specify the same redirect URI that you use with your web
                             // app. If you don't have a web version of your app, you can
                             // specify an empty string.
              .execute();

String accessToken = tokenResponse.getAccessToken();

// Use access token to call API
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Drive drive =
    new Drive.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential)
        .setApplicationName("Auth Code Exchange Demo")
        .build();
File file = drive.files().get("appfolder").execute();

// Get profile info from ID token
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String userId = payload.getSubject();  // Use this value as a key to identify a user.
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class GoogleSignInHelper {
    private static final String TAG = "hacket";
    // Web client (auto created by Google Service)
    private static final String webClientId = "448384031016-i7ai1nu1426359lag8kd8i0v6p8663o1.apps.googleusercontent.com";
    @SuppressLint("StaticFieldLeak")

    private static GoogleSignInClient googleApiClient;

    /**
     * 检查谷歌服务是否可用
     */
    public static boolean isGooglePlayServiceEnable(Context context) {
        try {
//            GooglePlayServicesUtil.isGooglePlayServicesAvailable(this)
            int available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
            return available == ConnectionResult.SUCCESS;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 登录
     *
     * @param activity Activity
     */
    public static void signInLegacy(Activity activity) {
        // Configure sign-in to request the user's ID, email address, and basic profile. ID and
        // basic profile are included in DEFAULT_SIGN_IN.
        GoogleSignInOptions gso = new GoogleSignInOptions
                .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)//获取用户的基本信息
                .requestId() // 明文id
                .requestIdToken(webClientId)
//                .requestIdToken(getString(R.string.default_web_client_id)) // 从这里获取clientId: https://console.cloud.google.com/apis/credentials?project=the-monkey-king-assistant
                .requestEmail()//获取邮箱
                .requestProfile()
//                .requestScopes()
                .build();

        // Build a GoogleApiClient with access to GoogleSignIn.API and the options above.
        googleApiClient = GoogleSignIn.getClient(activity, gso);//创建 GoogleSignInClient 对象
        Intent signInIntent = googleApiClient.getSignInIntent();
        activity.startActivityForResult(signInIntent, 10086); // 开始请求授权,请求码自己定
    }
    /**
     * If you need to detect changes to a user's auth state that happen outside your app, such as
     * access token or ID token revocation, or to perform cross-device sign-in, you might also call
     * GoogleSignInClient.silentSignIn when your app starts.
     */
    public static void silentSignIn() {
        if (googleApiClient == null) return;
        Task<GoogleSignInAccount> googleSignInAccountTask = googleApiClient.silentSignIn();
        googleSignInAccountTask.addOnSuccessListener(new OnSuccessListener<GoogleSignInAccount>() {
            @Override
            public void onSuccess(GoogleSignInAccount account) {
                if (account == null) return;
            }
        });
    }
    /**
     * 登出
     *
     * @param activity Activity
     */
    public static void signOutLegacy(Activity activity) {
        if (googleApiClient == null) return;

//        Auth.GoogleSignInApi.signOut(googleApiClient) // 过时写法
        googleApiClient.signOut()
                .addOnCompleteListener(activity, new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        boolean successful = task.isSuccessful();
                        Log.w("hacket", "google signOut successful = " + successful);
                    }
                });
    }
    public static void revokeAccess(Activity activity) {
        if (googleApiClient == null) {
            return;
        }
        // 断开连接--可选
        googleApiClient.revokeAccess()
                .addOnCompleteListener(activity, new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        boolean successful = task.isSuccessful();
                        Log.w("hacket", "google revokeAccess successful = " + successful);
                    }
                });
    }
}

UI 展示:
image.png

Ref

Google 登录参考这个即可

New Google Sign-In API

Google Identity Services (GIS) 是一系列用来 Google 登录和退出新的 API。

New Sign-In API 概述

你不应该用这些 API 在 app launch 或触发加入购物车时提示用户登录,这些场景你应该用 One Tap for Android
登录过程中会展示这些 UI
image.png

Make a sign-in request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static final int REQUEST_CODE_GOOGLE_SIGN_IN = 1; /* unique request id */
private void signIn() {
    GetSignInIntentRequest request =
        GetSignInIntentRequest.builder()
            .setServerClientId(getString(R.string.server_client_id))
            .build();
    Identity.getSignInClient(activity)
        .getSignInIntent(request)
        .addOnSuccessListener(
                result -> {
                    try {
                        startIntentSenderForResult(
                                result.getIntentSender(),
                                REQUEST_CODE_GOOGLE_SIGN_IN,
                                /* fillInIntent= */ null,
                                /* flagsMask= */ 0,
                                /* flagsValue= */ 0,
                                /* extraFlags= */ 0,
                                /* options= */ null);
                    } catch (IntentSender.SendIntentException e) {
                        Log.e(TAG, "Google Sign-in failed");
                    }
                })
        .addOnFailureListener(
                e -> {
                    Log.e(TAG, "Google Sign-in failed", e);
                });
}

需要注意:web_client_id 用的是这个红色框的,而不是 Android client 的那个:
image.png

Handle sign in results

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(resultCode == Activity.RESULT_OK) {
        if (requestCode == REQUEST_CODE_GOOGLE_SIGN_IN) {
            try {
                SignInCredential credential = Identity.getSignInClient(this).getSignInCredentialFromIntent(data);
                // Signed in successfully - show authenticated UI
                updateUI(credential);
            } catch (ApiException e) {
                // The ApiException status code indicates the detailed failure reason.
            }
        }
    }
}

也可以用 Activity Result API 来简化 onActivityResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private val googleLoginLauncher =
    registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
        val resultCode = result.resultCode
        Log.i(TAG, "google login get account info result.resultCode:$resultCode")
        if (resultCode == Activity.RESULT_OK) {
            try {
                val credential =
                    Identity.getSignInClient(this).getSignInCredentialFromIntent(result.data)
                Log.i(TAG, "google login get account info id:${credential.id}")
                Log.i(
                    TAG,
                    "google login get account info googleIdToken:${credential.googleIdToken}"
                )
                Log.i(TAG, "google login get account info password:${credential.password}")
                Log.i(TAG, "google login get account info givenName:${credential.givenName}")
                Log.i(TAG, "google login get account info familyName:${credential.familyName}")
                Log.i(
                    TAG,
                    "google login get account info displayName:${credential.displayName}"
                )
                Log.i(
                    TAG,
                    "google login get account info profilePictureUri:${credential.profilePictureUri}"
                )
                updateUI(credential)
            } catch (exception: ApiException) {
                Log.e(TAG, "google login get account info error :${exception.message}")
                exception.printStackTrace()
            }
        }
    }

效果图:
image.png

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void signInNewApi(
        Activity activity,
        ActivityResultLauncher<IntentSenderRequest> launcher) {
//        Android client for me.hacket.assistant.samples (auto created by Google Service)
//        String clientId = "448384031016-24iv7e5c7ltl7t204urdin6n8l1f7d83.apps.googleusercontent.com";
    GetSignInIntentRequest request =
            GetSignInIntentRequest.builder()
//                        .setServerClientId(getString(R.string.server_client_id))
                    .setServerClientId(webClientId)
                    .build();
    Identity.getSignInClient(activity)
            .getSignInIntent(request)
            .addOnSuccessListener(
                    result -> {
                        launcher.launch(new IntentSenderRequest.Builder(result).build());
//                            try {
//                                activity.startIntentSenderForResult(
//                                        result.getIntentSender(),
//                                        REQUEST_CODE_GOOGLE_SIGN_IN,
//                                        /* fillInIntent= */ null,
//                                        /* flagsMask= */ 0,
//                                        /* flagsValue= */ 0,
//                                        /* extraFlags= */ 0,
//                                        /* options= */ null);
//                            } catch (IntentSender.SendIntentException e) {
//                                Log.e(TAG, "Google Sign-in failed");
//                            }
                    })
            .addOnFailureListener(
                    e -> {
                        Log.e(TAG, "Google Sign-in failed", e);
                    });
}
public static void signOutNewApi(Activity activity) {
    Identity.getSignInClient(activity)
            .signOut()
            .addOnSuccessListener(new OnSuccessListener<Void>() {
                @Override
                public void onSuccess(Void unused) {
                    Log.i(TAG, "google call logout success");
                }
            });
}

One tap sign-in/sign-out for Android

什么是 One tap?

一键登录,检索你之前登录过的账号,避免再次创建账号,减少登录的阻力。

Sign users in with their saved credentials 配置 SignInClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class YourActivity : AppCompatActivity() {
    // ...
    private lateinit var oneTapClient: SignInClient
    private lateinit var signInRequest: BeginSignInRequest
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        oneTapClient = Identity.getSignInClient(this)
        signInRequest = BeginSignInRequest.builder()
            .setPasswordRequestOptions(BeginSignInRequest.PasswordRequestOptions.builder()
                    .setSupported(true)
                    .build())
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                // Your server's client ID, not your Android client ID.
                    .setServerClientId(getString(R.string.your_web_client_id))
                // Only show accounts previously used to sign in.
                    .setFilterByAuthorizedAccounts(true)
                    .build())
        // Automatically sign in when exactly one credential is retrieved.
            .setAutoSelectEnabled(true)
            .build()
        // ...
    }
    // ...
}

Display the One Tap sign-in UI 展示 One Tap UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
oneTapClient.beginSignIn(signInRequest)
    .addOnSuccessListener(this) { result ->
        try {
            startIntentSenderForResult(
                result.pendingIntent.intentSender, REQ_ONE_TAP,
                null, 0, 0, 0, null)
        } catch (e: IntentSender.SendIntentException) {
            Log.e(TAG, "Couldn't start One Tap UI: ${e.localizedMessage}")
        }
    }
    .addOnFailureListener(this) { e ->
        // No saved credentials found. Launch the One Tap sign-up flow, or
        // do nothing and continue presenting the signed-out UI.
        Log.d(TAG, e.localizedMessage)
    }

Handle the user’s response 处理 One Tap 请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class YourActivity : AppCompatActivity() {

    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...

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

        when (requestCode) {
             REQ_ONE_TAP -> {
                try {
                    val credential = oneTapClient.getSignInCredentialFromIntent(data)
                    val idToken = credential.googleIdToken
                    val username = credential.id
                    val password = credential.password
                    when {
                        idToken != null -> {
                            // Got an ID token from Google. Use it to authenticate
                            // with your backend.
                            Log.d(TAG, "Got ID token.")
                        }
                        password != null -> {
                            // Got a saved username and password. Use them to authenticate
                            // with your backend.
                            Log.d(TAG, "Got password.")
                        }
                        else -> {
                            // Shouldn't happen.
                            Log.d(TAG, "No ID token or password!")
                        }
                    }
                } catch (e: ApiException) {
                    // ...
                }
            }
        }
    }
    // ...
}

Stop displaying the One Tap UI 停止展示 One Tap UI 框

  • 用户取消了的话,临时不要展示该弹窗
  • 展示频次的控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class YourActivity : AppCompatActivity() {
    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            REQ_ONE_TAP -> {
                try {
                    // ...
                } catch (e: ApiException) {
                    when (e.statusCode) {
                        CommonStatusCodes.CANCELED -> { // 用户取消
                            Log.d(TAG, "One-tap dialog was closed.")
                            // Don't re-prompt the user.
                            showOneTapUI = false
                        }
                        CommonStatusCodes.NETWORK_ERROR -> {
                            Log.d(TAG, "One-tap encountered a network error.")
                            // Try again or just ignore.
                        }
                        else -> {
                            Log.d(TAG, "Couldn't get credential from result." +
                                " (${e.localizedMessage})")
                        }
                    }
                }
            }
        }
    }
    // ...
}

Handle sign-out

用户登出了 App,调用 One Tap client’s signOut()

image.png
注意:One Tap 需要在之前已经登录过了账号才会弹出 UI;未登录时不会弹出 UI 框,且报错:16: Cannot find a matching credential.

Save passwords with Credential Saving

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun savePassword(activity: Activity, username: String, password: String) {
    val signInPassword = SignInPassword(username, password)
    val savePasswordRequest =
        SavePasswordRequest.builder().setSignInPassword(signInPassword).build()

    Identity.getCredentialSavingClient(activity)
        .savePassword(savePasswordRequest)
        .addOnSuccessListener { result ->
            activity.startIntentSenderForResult(
                result.pendingIntent.intentSender,
                REQUEST_CODE_GIS_SAVE_PASSWORD,
                /* fillInIntent= */ null,
                /* flagsMask= */ 0,
                /* flagsValue= */ 0,
                /* extraFlags= */ 0,
                /* options= */ null
            )
        }
}

UI 效果:
image.png
image.png

Credential management 凭据管理

Blockstore encrypted credential storage

Block Store API 可以让您的应用存储用户凭据,从而可在未来的新设备中取回凭据,并用于重新验证用户。当用户使用一台设备引导另一台设备时,凭据数据就会在设备间传输。

Smart Lock for Passwords

什么是 Smart Lock for Passwords?

程序化的保存和检索凭据,跨端和网站自动登录。
All Smart Lock for Passwords functionality has been migrated to One Tap, and Smart Lock for Passwords is deprecated. Use One Tap instead.

Store a user’s credentials

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
fun save(
    activity: Activity,
    name: String,
    password: String,
    email: String,
    requestCode: Int
) {
    val credential: Credential = Credential.Builder(email)
        .setName(name)
        .setPassword(password) // Important: only store passwords in this field.
        // Android autofill uses this value to complete
        // sign-in forms, so repurposing this field will
        // likely cause errors.
        .build()
    val mCredentialsClient = Credentials.getClient(activity)
    mCredentialsClient.save(credential).addOnCompleteListener { task ->
        if (task.isSuccessful) {
            Log.d(TAG, "SAVE: OK")
            Toast.makeText(activity, "Credentials saved", Toast.LENGTH_SHORT).show()
            return@addOnCompleteListener
        }

        val e = task.exception
        if (e is ResolvableApiException) {
            // Try to resolve the save request. This will prompt the user if
            // the credential is new.
            try {
                e.startResolutionForResult(activity, requestCode)
            } catch (exception: SendIntentException) {
                // Could not resolve the request
                Log.e(TAG, "Failed to send resolution.", exception)
                Toast.makeText(activity, "Save failed", Toast.LENGTH_SHORT).show()
            }
        } else {
            // Request has no resolution
            Toast.makeText(activity, "Save failed", Toast.LENGTH_SHORT).show()
        }
    }
}

如果没有立即 save 成功,就会抛出一个 ResolvableApiException� 异常,调用 startResolutionForResult�() 让用户来确认,效果图如下:
image.png

Retrieve a user’s stored credentials

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
fun retrive(activity: Activity) {
    val mCredentialsClient = Credentials.getClient(activity)
    val mCredentialRequest = CredentialRequest.Builder()
        .setPasswordLoginSupported(true)
        .setAccountTypes(IdentityProviders.GOOGLE, IdentityProviders.TWITTER)
        .build()

    mCredentialsClient?.request(mCredentialRequest)
        ?.addOnCompleteListener(object :
            OnCompleteListener<CredentialRequestResponse> {
            override fun onComplete(task: Task<CredentialRequestResponse>) {
                if (task.isSuccessful) {
                    // See "Handle successful credential requests"
                    Log.i(TAG, "onComplete Successful onCredentialRetrieved.")
                    onCredentialRetrieved(activity, task.result.credential)
                    return
                }
                // See "Handle unsuccessful and incomplete credential requests"
                val e = task.exception
                e?.printStackTrace()
                Log.w(TAG, "onComplete Unsuccessful credential request. ${e?.message}")
                if (e is ResolvableApiException) {
                    // This is most likely the case where the user has multiple saved
                    // credentials and needs to pick one. This requires showing UI to
                    // resolve the read request.
                    resolveResult(activity, e, 0x11)
                } else if (e is com.google.android.gms.common.api.ApiException) {
                    // The user must create an account or sign in manually.
                    Log.e(TAG, "Unsuccessful credential request.", e)
                    val ae = e as com.google.android.gms.common.api.ApiException?
                    val code: Int = ae!!.statusCode
                    // ...
                }
            }
        })
}

private fun resolveResult(activity: Activity, rae: ResolvableApiException, requestCode: Int) {
    try {
        rae.startResolutionForResult(activity, requestCode)
//            mIsResolving = true
    } catch (e: IntentSender.SendIntentException) {
        Log.e(TAG, "Failed to send resolution.", e)
        e.printStackTrace()
//            hideProgress()
    }
}

fun onCredentialRetrieved(activity: Activity, credential: Credential?) {
    val accountType: String? = credential?.accountType
    val name = credential?.name
    val id = credential?.id
    val password = credential?.password
    val idTokens = credential?.idTokens
    Log.w(
        TAG,
        "onCredentialRetrieved accountType: $accountType name: $name id: $id password: $password idTokens: $idTokens"
    )
    if (accountType == null) {
        // Sign the user in with information from the Credential.
        Log.i(TAG, "[accountType=null] signInWithPassword id: $id password: $password")
    } else if (accountType == IdentityProviders.GOOGLE) {
        // The user has previously signed in with Google Sign-In. Silently
        // sign in the user with the same ID.
        // See https://developers.google.com/identity/sign-in/android/
        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestEmail()
            .build()
        val signInClient = GoogleSignIn.getClient(activity, gso)
        val task = signInClient.silentSignIn()
        // There's no immediate result ready, displays some progress indicator and waits for the
        // async callback.
        Log.i(TAG, "[accountType=GOOGLE] silentSignIn.")
        task.addOnCompleteListener { t ->
            if (t.isSuccessful) {
                // There's immediate result available.
                val signInAccount = t.result
                // ...
            } else {
                // Unsuccessful sign-in, show the user an error dialog.
                // ...
            }
        }
        // ...
    }
}

Delete stored credentials

1
2
3
4
5
6
7
8
9
10
mCredentialsClient.delete(credential).addOnCompleteListener(
    new OnCompleteListener<Void>() {
        @Override
        public void onComplete(@NonNull Task<Void> task) {
            if (task.isSuccessful()) {
                // Credential deletion succeeded.
                // ...
            }
        }
    });

Autofill on the device

本文由作者按照 CC BY 4.0 进行授权