使用凭据管理器让用户登录

凭据管理器是一种 Jetpack API,它在单个 API 中支持多种登录方法,例如用户名和密码、通行密钥和联合登录解决方案(如“使用 Google 帐号登录”),从而为开发者简化了集成工作。

此外,无论用户选择何种身份验证方法,凭据管理器都能提供统一的登录界面,让用户可以更简单便捷地登录应用。

本页面介绍了通行密钥的概念以及使用 Credential Manager API 实现客户端对身份验证解决方案(包括通行密钥)支持的步骤。此外,如需了解针对更具体问题的更详细解答,请参阅单独的常见问题解答页面

关于通行密钥

通行密钥是一种更安全、更便捷的替代密码的方法。借助通行密钥,用户可以使用生物识别传感器(如指纹或人脸识别)、PIN 码或图案登录应用和网站。这样可以提供无缝的登录体验,让用户不必记住用户名或密码。

通行密钥依赖于 WebAuthn(网络身份验证),这是一个由 FIDO 联盟和万维网联盟 (W3C) 联合开发的标准。WebAuthn 使用公钥加密来验证用户身份。用户登录的网站或应用可以查看并存储公钥,但绝不会看到和存储私钥。私钥会保密,安全无虞。由于密钥是唯一的,并且与该网站或应用相关联,因此通行密钥不可仿冒,这可以进一步提高安全性。

借助凭据管理器,用户可以创建通行密钥并将其存储在 Google 密码管理工具中。

前提条件

若要使用凭据管理器,请完成本部分中的步骤。

使用较新版本的平台

搭载 Android 4.4(API 级别 19)及更高版本的设备支持凭据管理器。

为应用添加依赖项

将以下依赖项添加到应用模块的 build.gradle 文件中:

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.0.0-alpha02"

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation "androidx.credentials:credentials-play-services-auth:1.0.0-alpha02"
}

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.0.0-alpha02")

    // optional - needed for credentials support from play services, for devices running
    // Android 13 and below.
    implementation("androidx.credentials:credentials-play-services-auth:1.0.0-alpha02")
}

添加对 Digital Asset Links 的支持

若要为您的 Android 应用启用对通行密钥的支持,请将您的应用与其拥有的网站相关联。您可以通过完成以下步骤来声明此关联:

  1. 创建一个 Digital Asset Links JSON 文件。例如,如需声明网站 https://signin.example.com 和软件包名称为 com.example 的 Android 应用可以共享登录凭据,请使用以下内容创建名为 assetlinks.json 的文件:

    [{
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "web",
        "site": "https://signin.example.com"
      }
     },
     {
      "relation": ["delegate_permission/common.get_login_creds"],
      "target": {
        "namespace": "android_app",
        "package_name": "com.example",
        "sha256_cert_fingerprints": [
          SHA_HEX_VALUE
        ]
      }
     }]
    

    relation 字段是包含一个或多个字符串的数组,用于描述要声明的关系。如需声明应用和网站共享登录凭据,请指定字符串 delegate_permission/common.get_login_creds

    target 字段是一个对象,用于指定声明所适用的资源。以下字段用于标识一个网站:

    namespace web
    site

    网站的网址,格式为 https://domain[:optional_port];例如,https://www.example.com

    domain 必须是完全限定的,在为 HTTPS 使用端口 443 时必须省略 optional_port

    site 目标只能是根网域:您不能将应用限制为仅与特定子目录相关联。请勿在网址中包含路径符号,例如尾部斜杠。

    子网域不会被视为匹配:也就是说,如果您将 domain 指定为 www.example.com,则网域 www.counter.example.com 不会与您的应用相关联。

    以下字段用于标识一款 Android 应用:

    namespace android_app
    package_name 该应用的清单文件中声明的软件包名称。例如 com.example.android
    sha256_cert_fingerprints 应用的签名证书的 SHA256 指纹。
  2. 将这个 Digital Asset Links JSON 文件托管在登录网域中的以下位置:

    https://domain[:optional_port]/.well-known/assetlinks.json
    

    例如,如果您的登录网域是 signin.example.com,请将 JSON 文件托管在 https://signin.example.com/.well-known/assetlinks.json 上。

    Digital Asset Links 文件的 MIME 类型需为 JSON。确保服务器在响应中发送 Content-Type: application/json 标头。

  3. 确保您的主机允许 Google 检索您的 Digital Asset Links 文件。如果您有 robots.txt 文件,它必须允许 Googlebot 代理检索 /.well-known/assetlinks.json。大多数网站可以直接允许任何自动化代理检索 /.well-known/ 路径下的文件,以便其他服务可以访问这些文件中的元数据:

    User-agent: *
    Allow: /.well-known/
    

  4. 在 Android 应用中声明关联:

    1. asset_statements 字符串资源添加到 strings.xml 文件中。asset_statements 字符串是一个 JSON 对象,用于指定要加载的 assetlinks.json 文件。您必须对字符串中的所有撇号和引号进行转义。例如:

        <string name="asset_statements" translatable="false">
        [{
          \"include\": \"https://signin.example.com/.well-known/assetlinks.json\"
        }]
        </string>
      
        > GET /.well-known/assetlinks.json HTTP/1.1
        > User-Agent: curl/7.35.0
        > Host: signin.example.com
      
        < HTTP/1.1 200 OK
        < Content-Type: application/json
      

配置凭据管理器

如需配置和初始化 CredentialManager 对象,请添加类似于以下内容的逻辑:

Kotlin

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
val credentialManager = CredentialManager.create(context)

Java

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
CredentialManager credentialManager = CredentialManager.create(context)

让用户登录

如需检索与用户帐号关联的所有通行密钥和密码选项,请完成以下步骤:

  1. 初始化密码和通行密钥身份验证选项:

    Kotlin

    // Retrieves the user's saved password for your app from their
    // password provider.
    val getPasswordOption = GetPasswordOption()
    
    // Get passkeys from the user's public key credential provider.
    val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
        requestJson = requestJson,
        preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
    )

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkeys from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson, preferImmediatelyAvailableCredentials);
  2. 使用上一步中检索到的选项构建登录请求:

    Kotlin

    val getCredRequest = GetCredentialRequest(
        listOf(getPasswordOption, getPublicKeyCredentialOption)
    )

    Java

    GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder()
        .addCredentialOption(getPasswordOption)
        .addCredentialOption(getPublicKeyCredentialOption)
        .build();
  3. 启动登录流程:

    Kotlin

    coroutineScope.launch {
        try {
            val result = credentialManager.getCredential(
                request = getCredRequest,
                activity = activity,
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                responseJson = credential.authenticationResponseJson
                fidoAuthenticateWithServer(responseJson)
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                passwordAuthenticateWithServer(username, password)
            }
            else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        getCredRequest,
        activity,
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onResult(GetCredentialResponse result) {
                // Handle the successfully returned credential.
                Credential credential = result.getCredential();
                if (credential instanceof PublicKeyCredential) {
                    String responseJson = ((PublicKeyCredential) credential)
                            .getAuthenticationResponseJson();
                    fidoAuthenticateToServer(responseJson);
                } else if (credential instanceof PasswordCredential) {
                    Log.d(TAG, "Got PasswordCredential");
                    String id = ((PasswordCredential) credential).getId();
                    String password = ((PasswordCredential) credential)
                            .getPassword();
                    firebaseSignInWithPassword(id, password);
                } else {
                  Log.e(
                      TAG,
                      "Unexpected type of credential: " +
                      credential.getClass().getName());
                }
            }
    
            @Override
            public void onError(GetCredentialException e) {
                Log.e(TAG, "Sign in failed with exception", e);
            }
        }
    );

以下代码段举例说明了如何在获得通行密钥时设置 JSON 请求的格式:

{
  "challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
  "allowCredentials": [],
  "timeout": 1800000,
  "userVerification": "required",
  "rpId": "credential-manager-app-test.glitch.me"
}

以下代码段显示了获取公钥凭据后生成的 JSON 响应示例:

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
    "signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
    "userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
  }
}

注册流程

您可以使用通行密钥密码注册用户以进行身份验证。

创建通行密钥

若想让用户能够注册通行密钥并将其用于重新进行身份验证,请使用 CreatePublicKeyCredentialRequest 对象注册用户凭据:

Kotlin

fun createPasskey(requestJson: String, preferImmediatelyAvailableCredentials: Boolean) {
    val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
        // Contains the request in JSON format. Uses the standard WebAuthn
        // web JSON spec.
        requestJson = requestJson,
        // Defines whether you prefer to use only immediately available credentials,
        // not hybrid credentials, to fulfill this request. This value is false
        // by default.
        preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
    )

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    coroutineScope.launch {
        try {
            val result = credentialManager.createCredential(
                request = createPublicKeyCredentialRequest,
                activity = activity,
            )
            handlePasskeyRegistrationResult(result)
        } catch (e : CreateCredentialException){
            handleFailure(e)
        }
    }

    fun handleFailure(e: CreateCredentialException) {
        when (e) {
            is CreatePublicKeyCredentialDomException -> {
                // Handle the passkey DOM errors thrown according to the
                // WebAuthn spec.
                handlePasskeyError(e.domError)
            }
            is CreateCredentialCancellationException -> {
                // The user intentionally canceled the operation and chose not
                // to register the credential.
            }
            is CreateCredentialInterruptedException -> {
                // Retry-able error. Consider retrying the call.
            }
            is CreateCredentialProviderConfigurationException -> {
                // Your app is missing the provider configuration dependency.
                // Most likely, you're missing the
                // "credentials-play-services-auth" module.
            }
            is CreateCredentialUnknownException -> ...
            is CreateCustomCredentialException -> {
                // You have encountered an error from a 3rd-party SDK. If you
                // make the API call with a request object that's a subclass of
                // CreateCustomCredentialRequest using a 3rd-party SDK, then you
                // should check for any custom exception type constants within
                // that SDK to match with e.type. Otherwise, drop or log the
                // exception.
            }
            else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}")
        }
    }
}

Java

public void createPasskey(String requestJson, boolean preferImmediatelyAvailableCredentials) {
    CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
            // `requestJson` contains the request in JSON format. Uses the standard
            // WebAuthn web JSON spec.
            // `preferImmediatelyAvailableCredentials` defines whether you prefer
            // to only use immediately available credentials, not  hybrid credentials,
            // to fulfill this request. This value is false by default.
            new CreatePublicKeyCredentialRequest(
                requestJson, preferImmediatelyAvailableCredentials);

    // Execute CreateCredentialRequest asynchronously to register credentials
    // for a user account. Handle success and failure cases with the result and
    // exceptions, respectively.
    credentialManager.createCredentialAsync(
        createPublicKeyCredentialRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleSuccessfulCreatePasskeyResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                if (e instanceof CreatePublicKeyCredentialDomException) {
                    // Handle the passkey DOM errors thrown according to the
                    // WebAuthn spec.
                    handlePasskeyError(((CreatePublicKeyCredentialDomException)e).getDomError());
                } else if (e instanceof CreateCredentialCancellationException) {
                    // The user intentionally canceled the operation and chose not
                    // to register the credential.
                } else if (e instanceof CreateCredentialInterruptedException) {
                    // Retry-able error. Consider retrying the call.
                } else if (e instanceof CreateCredentialProviderConfigurationException) {
                    // Your app is missing the provider configuration dependency.
                    // Most likely, you're missing the
                    // "credentials-play-services-auth" module.
                } else if (e instanceof CreateCredentialUnknownException) {
                } else if (e instanceof CreateCustomCredentialException) {
                    // You have encountered an error from a 3rd-party SDK. If
                    // you make the API call with a request object that's a
                    // subclass of
                    // CreateCustomCredentialRequest using a 3rd-party SDK,
                    // then you should check for any custom exception type
                    // constants within that SDK to match with e.type.
                    // Otherwise, drop or log the exception.
                } else {
                  Log.w(TAG, "Unexpected exception type "
                          + e.getClass().getName());
                }
            }
        }
    );
}

设置 JSON 请求的格式

创建通行密钥后,您必须将其与用户帐号相关联,并将通行密钥的公钥存储在您的服务器上。以下代码段举例说明了如何在创建通行密钥时设置 JSON 请求的格式。

在这篇有关为应用引入无缝身份验证的博文中,介绍了在创建通行密钥以及使用通行密钥进行身份验证时,如何设置 JSON 请求的格式。该博文还说明了为何密码无法作为有效的身份验证解决方案、如何利用现有的生物识别凭据、如何将您的应用与您拥有的网站相关联、如何创建通行密钥,以及如何使用通行密钥进行身份验证。

{
  "challenge": "nhkQXfE59Jb97VyyNJkvDiXucMEvltduvcrDmGrODHY",
  "rp": {
    "name": "CredMan App Test",
    "id": "credential-manager-app-test.glitch.me"
  },
  "user": {
    "id": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

处理 JSON 响应

以下代码段展示了用于创建公钥凭据的 JSON 响应示例。详细了解如何处理返回的公钥凭据

{
  "id": "KEDetxZcUfinhVi6Za5nZQ",
  "type": "public-key",
  "rawId": "KEDetxZcUfinhVi6Za5nZQ",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"
  }
}

保存用户密码

如果用户在应用中提供了用于身份验证流程的用户名和密码,您可以注册一个可用于验证用户身份的用户凭据。为此,请创建一个 CreatePasswordRequest 对象:

Kotlin

fun registerPassword(username: String, password: String) {
    // Initialize a CreatePasswordRequest object.
    val createPasswordRequest =
            CreatePasswordRequest(id = username, password = password)

    // Create credentials and handle result.
    coroutineScope.launch {
        try {
            val result =
                    credentialManager.createCredential(createPasswordRequest)
            handleRegisterPasswordResult(result)
        } catch (e: CreateCredentialException) {
            handleFailure(e)
        }
    }
}

Java

void registerPassword(String username, String password) {
    // Initialize a CreatePasswordRequest object.
    CreatePasswordRequest createPasswordRequest =
            new CreatePasswordRequest(username, password);

    // Register the username and password.
    credentialManager.createCredentialAsync(
        createPasswordRequest,
        requireActivity(),
        cancellationSignal,
        requireContext().getMainExecutor(),
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

            @Override
            public void onError(CreateCredentialException e) {
                handleFailure(e)
            }
        }
    );
}

排查常见错误

下表列出了一些常见的错误代码及说明,并提供了一些相应的原因:

错误代码和说明 原因
On Begin Sign In Failure: 16:由于取消的登录提示过多,因此调用方已被暂时屏蔽。

如果您在开发期间遇到这段 24 小时冷却期,则可以通过清除 Google Play 服务的应用存储空间来重置该冷却期。

或者,如需在测试设备或模拟器上切换此冷却时间,请打开拨号器应用并输入以下代码:*#*#66382723#*#*。拨号器应用会清除所有输入并可能关闭,但系统不会显示确认消息。

On Begin Sign In Failure: 8:未知内部错误。
  1. 设备未正确设置 Google 帐号。
  2. 通行密钥 JSON 的创建方式不正确。
CreatePublicKeyCredentialDomException:无法验证传入请求 应用的软件包 ID 未在您的服务器上注册。请在服务器端集成中进行验证。

其他资源

如需详细了解 Credential Manager API 和通行密钥,请查看以下开发者指南: