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

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

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

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

您的反馈对于改进 Credential Manager API 至关重要。您可以通过以下链接分享发现的任何问题或改进 API 的想法:

提供反馈

关于通行密钥

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

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

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

前提条件

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

使用新近的平台版本

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

为应用添加依赖项

将以下依赖项添加到应用模块的构建脚本中:

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

在 ProGuard 文件中保留类

在模块的 proguard-rules.pro 文件中,添加以下指令:

-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
  *;
}

详细了解如何缩减、混淆处理和优化应用

添加对 Digital Asset Links 的支持

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

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

    [
      {
        "relation" : [
          "delegate_permission/common.handle_all_urls",
          "delegate_permission/common.get_login_creds"
        ],
        "target" : {
          "namespace" : "android_app",
          "package_name" : "com.example.android",
          "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/
    

配置凭据管理器

如需配置和初始化 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)

指示凭据字段

在 Android 14 及更高版本中,isCredential 属性可用于指示凭据字段,例如用户名字段或密码字段。该属性表示此 View 是一个凭据字段,旨在与凭据管理器和第三方凭据提供程序搭配使用,同时帮助自动填充服务提供更好的自动填充建议。当应用使用 Credential Manager API 时,系统会显示包含可用凭据的凭据管理器底部动作条,而无需显示自动填充服务的用户名或密码填充对话框。

如需使用 isCredential 属性,请将其添加到相关 View 中:

<TextView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:isCredential="true"
...
 />

让用户登录

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

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

    Kotlin

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

    Java

    // Retrieves the user's saved password for your app from their
    // password provider.
    GetPasswordOption getPasswordOption = new GetPasswordOption();
    
    // Get passkey from the user's public key credential provider.
    GetPublicKeyCredentialOption getPublicKeyCredentialOption =
            new GetPublicKeyCredentialOption(requestJson);
  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(
                // Use an activity-based context to avoid undefined system UI
                // launching behavior.
                context = activityContext,
                request = getCredRequest
            )
            handleSignIn(result)
        } catch (e : GetCredentialException) {
            handleFailure(e)
        }
    }
    
    fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        val credential = result.credential
    
        when (credential) {
            is PublicKeyCredential -> {
                val responseJson = credential.authenticationResponseJson
                // Share responseJson i.e. a GetCredentialResponse on your server to
                // validate and  authenticate
            }
            is PasswordCredential -> {
                val username = credential.id
                val password = credential.password
                // Use id and password to send to your server to validate
                // and authenticate
            }
          is CustomCredential -> {
              // If you are also using any external sign-in libraries, parse them
              // here with the utility functions provided.
              if (credential.type == ExampleCustomCredential.TYPE)  {
              try {
                  val ExampleCustomCredential = ExampleCustomCredential.createFrom(credential.data)
                  // Extract the required credentials and complete the authentication as per
                  // the federated sign in or any external sign in library flow
                  } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) {
                      // Unlikely to happen. If it does, you likely need to update the dependency
                      // version of your external sign-in library.
                      Log.e(TAG, "Failed to parse an ExampleCustomCredential", e)
                  }
              } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential")
              }
            } else -> {
                // Catch any unrecognized credential type here.
                Log.e(TAG, "Unexpected type of credential")
            }
        }
    }

    Java

    credentialManager.getCredentialAsync(
        // Use activity based context to avoid undefined
        // system UI launching behavior
        activity,
        getCredRequest,
        cancellationSignal,
        <executor>,
        new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
            @Override
            public void onSuccess(GetCredentialResponse result) {
                handleSignIn(result);
            }
    
            @Override
            public void onFailure(GetCredentialException e) {
                handleFailure(e);
            }
        }
    );
    
    public void handleSignIn(GetCredentialResponse result) {
        // Handle the successfully returned credential.
        Credential credential = result.getCredential();
        if (credential instanceof PublicKeyCredential) {
            String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson();
            // Share responseJson i.e. a GetCredentialResponse on your server to validate and authenticate
        } else if (credential instanceof PasswordCredential) {
            String username = ((PasswordCredential) credential).getId();
            String password = ((PasswordCredential) credential).getPassword();
            // Use id and password to send to your server to validate and authenticate
        } else if (credential instanceof CustomCredential) {
            if (ExampleCustomCredential.TYPE.equals(credential.getType())) {
                try {
                    ExampleCustomCredential customCred = ExampleCustomCredential.createFrom(customCredential.getData());
                    // Extract the required credentials and complete the
                    // authentication as per the federated sign in or any external
                    // sign in library flow
                } catch (ExampleCustomCredential.ExampleCustomCredentialParsingException e) {
                    // Unlikely to happen. If it does, you likely need to update the
                    // dependency version of your external sign-in library.
                    Log.e(TAG, "Failed to parse an ExampleCustomCredential", e);
                }
            } else {
                // Catch any unrecognized custom credential type here.
                Log.e(TAG, "Unexpected type of credential");
            }
        } else {
            // Catch any unrecognized credential type here.
            Log.e(TAG, "Unexpected type of credential");
        }
    }

以下示例展示了如何在获取通行密钥时设置 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 对象注册用户凭据。

当您首次调用 Credential Manager API 时,请将 preferImmediatelyAvailableCredentials 设置为 truepreferImmediatelyAvailableCredentials 方法定义您是否愿意仅使用即时可用的凭据(而不是混合凭据)来完成此请求。如果为 true,当凭据不可用时,Credential Manager API 不会显示远程条目。默认情况下,此值为 false

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(
                // Use an activity-based context to avoid undefined system
                // UI launching behavior
                context = activityContext,
                request = createPublicKeyCredentialRequest,
            )
            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 CreateCredentialCustomException -> {
            // 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(
        // Use an activity-based context to avoid undefined system
        // UI launching behavior
        requireActivity(),
        createPublicKeyCredentialRequest,
        cancellationSignal,
        executor,
        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 CreateCredentialCustomException) {
                    // 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 credential and handle result.
    coroutineScope.launch {
        try {
            val result =
                credentialManager.createCredential(
                    // Use an activity based context to avoid undefined
                    // system UI launching behavior.
                    activityContext,
                    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(
        // Use an activity-based context to avoid undefined
        // system UI launching behavior
        requireActivity(),
        createPasswordRequest,
        cancellationSignal,
        executor,
        new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
            @Override
            public void onResult(CreateCredentialResponse result) {
                handleResult(result);
            }

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

支持凭据恢复

如果用户无法再访问存储了凭据的设备,则可能需要通过安全的在线备份进行恢复。如需详细了解如何支持此凭据恢复过程,请阅读以下博文中标题为“Recovering access or adding new devices”(恢复访问权限或添加新设备)的部分:Google 密码管理工具中的通行密钥的安全性

添加对使用通行密钥端点知名网址的密码管理工具的支持

为了无缝集成并在未来兼容密码和凭据管理工具,我们建议您添加对通行密钥端点知名网址的支持。这是一个开放协议,可以让相关各方正式公布对通行密钥的支持,并提供用于注册和管理通行密钥的直接链接。

  1. 对于网址为 https://example.com 且既有网站又有 Android 和 iOS 应用的依赖方,知名网址为 https://example.com/.well-known/passkey-endpoints

  2. 在查询网址时,响应应使用以下架构

    {
      "enroll": "https://example.com/account/manage/passkeys/create"
      "manage": "https://example.com/account/manage/passkeys"
    }
    
  3. 如需直接在应用中(而不是在网页上)打开此链接,请使用 Android App Links

  4. 更多详情请参阅 GitHub 上的通行密钥端点知名网址说明。

排查常见错误

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

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

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

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

On Begin Sign In Failure: 8:未知内部错误。
  1. 设备未正确设置 Google 账号。
  2. 通行密钥 JSON 的创建方式不正确。
CreatePublicKeyCredentialDomException:无法验证传入请求 应用的软件包 ID 未在您的服务器上注册。请在服务器端集成中进行验证。
CreateCredentialUnknownException:保存密码期间,点按一下即可找到密码失败响应 16:Android 自动填入功能可能会提示用户,因此跳过密码保存 该错误仅在 Android 13 及更低版本中发生,且仅当 Google 是自动填充提供程序时才会发生。在这种情况下,用户会看到自动填充功能发出的保存提示,并且密码会保存到 Google 密码管理工具。请注意,使用 Google 自动填充功能保存的凭据会与 Credential Manager API 进行双向共享。因此,您可以放心地忽略这一错误。

其他资源

如需详细了解 Credential Manager API 和通行密钥,请查看以下资源: