使用凭据管理器登录您的用户

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

此外,对于用户而言,凭据管理器统一了所有身份验证方法的登录界面,使用户能够更清晰、更轻松地登录应用程序,无论他们选择哪种方法。

本页面介绍了密钥通行证的概念以及使用凭据管理器 API 实现客户端身份验证解决方案(包括密钥通行证)支持的步骤。还有一个单独的 常见问题解答页面,其中提供了更详细、具体问题的解答。

您的反馈对于改进凭据管理器 API 至关重要。使用以下链接分享您遇到的任何问题或改进 API 的想法。

提供反馈

关于密钥通行证

密钥通行证 是密码更安全、更简单的替代方案。使用密钥通行证,用户可以使用生物识别传感器(例如指纹或面部识别)、PIN 或图案登录应用程序和网站。这提供了一种无缝的登录体验,使您的用户无需记住用户名或密码。

密钥通行证依赖于 WebAuthn(Web 身份验证),该标准由 FIDO 联盟和万维网联盟 (W3C) 共同开发。WebAuthn 使用公钥加密来验证用户。用户登录的网站或应用程序可以看到并存储公钥,但永远不会看到私钥。私钥保持秘密和安全。由于密钥是唯一的且与网站或应用程序绑定,因此密钥通行证无法被钓鱼,从而进一步提高了安全性。

凭据管理器允许用户创建密钥通行证并将它们存储在 Google 密码管理器 中。

阅读 使用密钥通行证进行用户身份验证,了解有关如何使用凭据管理器实现无缝密钥通行证身份验证流程的指南。

先决条件

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

使用最新的平台版本

凭据管理器在 Android 4.4(API 级别 19)及更高版本上受支持。

向您的应用程序添加依赖项

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

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-alpha05")

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

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.5.0-alpha05"

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

在 ProGuard 文件中保留类

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

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

了解有关如何 压缩、混淆和优化您的应用程序 的更多信息。

添加对数字资产链接的支持

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

  1. 创建数字资产链接 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/handle_all_urlsdelegate_permission/common.get_login_creds

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

    namespace web
    site

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

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

    一个 site 目标只能是根域:您不能将应用关联限制到特定子目录。 不要在 URL 中包含路径,例如尾部斜杠。

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

    以下字段标识 Android 应用

    namespace android_app
    package_name 应用清单中声明的包名称。 例如,com.example.android
    sha256_cert_fingerprints 您应用的 签名证书 的 SHA256 指纹。
  2. 将数字资产链接 JSON 文件托管在登录域上的以下位置

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

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

    数字资产链接文件的 MIME 类型需要是 JSON。 确保服务器在响应中发送 Content-Type: application/json 标头。

  3. 确保您的主机允许 Google 检索您的数字资产链接文件。 如果您有 robots.txt 文件,则它必须允许 Googlebot 代理检索 /.well-known/assetlinks.json。 大多数站点都可以允许任何自动代理检索 /.well-known/ 路径中的文件,以便其他服务可以访问这些文件中的元数据。

    User-agent: *
    Allow: /.well-known/
    
  4. <application> 下的清单文件中添加以下行

    <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
    
  5. 如果您通过凭据管理器使用密码登录,请按照以下步骤在清单中配置数字资产链接。 如果您只使用通行码,则不需要执行此步骤。

    在 Android 应用中声明关联。 添加一个指定要加载的 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)

指示凭据字段

在 Android 14 及更高版本上,isCredential 属性可用于指示凭据字段,例如用户名或密码字段。 此属性表明此视图是一个凭据字段,旨在与凭据管理器和第三方凭据提供商一起使用,同时帮助自动填充服务提供更好的自动填充建议。 当应用使用凭据管理器 API 时,将显示带有可用凭据的凭据管理器底部工作表,并且无需再显示自动填充的填充对话框以输入用户名或密码。 同样,无需显示自动填充的保存对话框以保存密码,因为应用将请求凭据管理器 API 保存凭据。

要使用 isCredential 属性,请将其添加到相关视图中

<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 onResult(GetCredentialResponse result) {
                handleSignIn(result);
            }
    
            @Override
            public void onError(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"
  }
}

处理没有可用凭据时的异常

在某些情况下,用户可能没有可用的凭据,或者用户可能不同意使用可用的凭据。 如果调用了 getCredential() 并且没有找到凭据,则将返回 NoCredentialException。 如果发生这种情况,您的代码应处理 NoCredentialException 实例。

Kotlin

try {
  val credential = credentialManager.getCredential(credentialRequest)
} catch (e: NoCredentialException) {
  Log.e("CredentialManager", "No credential available", e)
}

Java

try {
  Credential credential = credentialManager.getCredential(credentialRequest);
} catch (NoCredentialException e) {
  Log.e("CredentialManager", "No credential available", e);
}

在 Android 14 或更高版本上,您可以通过在调用 getCredential() 之前使用 prepareGetCredential() 方法来减少显示帐户选择器时的延迟。

Kotlin

val response = credentialManager.prepareGetCredential(
  GetCredentialRequest(
    listOf(
      <getPublicKeyCredentialOption>,
      <getPasswordOption>
    )
  )
}

Java

GetCredentialResponse response = credentialManager.prepareGetCredential(
  new GetCredentialRequest(
    Arrays.asList(
      new PublicKeyCredentialOption(),
      new PasswordOption()
    )
  )
);

prepareGetCredential() 方法不会调用 UI 元素。 它只帮助您执行准备工作,以便您以后可以通过 getCredential() API 启动其余的获取凭据操作(涉及 UI)。

缓存的数据将返回到 PrepareGetCredentialResponse 对象中。 如果存在现有凭据,结果将被缓存,然后您可以稍后启动剩余的 getCredential() API 以使用缓存的数据调出帐户选择器。

注册流程

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

创建通行码

为了让用户可以选择注册通行码并将其用于重新身份验证,请使用 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(
                // 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": "abc123",
  "rp": {
    "name": "Credential Manager example",
    "id": "credential-manager-test.example.com"
  },
  "user": {
    "id": "def456",
    "name": "[email protected]",
    "displayName": "[email protected]"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    },
    {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {"id": "ghi789", "type": "public-key"},
    {"id": "jkl012", "type": "public-key"}
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

为 authenticatorAttachment 设置值

authenticatorAttachment 参数只能在凭据创建时设置。 您可以指定 platformcross-platform 或无值。 在大多数情况下,建议不指定值。

  • platform:要注册用户的当前设备或提示密码用户在登录后升级到通行码,请将 authenticatorAttachment 设置为 platform
  • cross-platform:此值通常用于注册多因素凭据,在通行码上下文中不使用。
  • 无值:为了让用户能够在他们首选的设备上创建通行码(例如在帐户设置中),当用户选择添加通行码时,不应指定 authenticatorAttachment 参数。 在大多数情况下,不指定参数是最佳选择。

防止创建重复的通行码

在可选的 excludeCredentials 数组中列出凭据 ID 以防止创建新的通行码,如果已经存在具有相同通行码提供商的通行码。

处理 JSON 响应

以下代码段展示了创建公钥凭据的 JSON 响应示例。 了解有关如何处理 返回的公钥凭据 的更多信息。

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

验证来自客户端数据 JSON 的来源

origin 表示请求来自的应用或网站,通行码使用它来防止网络钓鱼攻击。 您的应用服务器需要将客户端数据来源与批准的应用和网站的允许列表进行比较。 如果服务器收到来自来自未知来源的应用或网站的请求,则应拒绝该请求。

在 Web 案例中,origin 反映了 创建凭据的同站点来源。 例如,对于 https://www.example.com:8443/store?category=shoes#athletic 的 URL,originhttps://www.example.com:8443

对于 Android 应用,用户代理会自动将 origin 设置为调用应用程序的签名。您应该在您的服务器上验证此签名是否匹配,以验证调用密钥 API 的应用程序。Android origin 是从 APK 签名证书的 SHA-256 哈希派生的 URI,例如

android:apk-key-hash:<sha256_hash-of-apk-signing-cert>

可以通过运行以下终端命令找到密钥库中签名证书的 SHA-256 哈希值

keytool -list -keystore <path-to-apk-signing-keystore>

SHA-256 哈希值采用冒号分隔的十六进制格式(91:F7:CB:F9:D6:81…),而 Android origin 值采用 base64url 编码。此 Python 示例演示了如何将哈希格式转换为兼容的冒号分隔十六进制格式

import binascii
import base64
fingerprint = '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5'
print("android:apk-key-hash:" + base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))

fingerprint 的值替换为您的值。以下是一个示例结果

android:apk-key-hash:kffL-daBUxvHpY-4M8yhTavt5QnFEI2LsexohxrGPYU

然后,您可以在服务器上将该字符串匹配为允许的来源。如果您有多个签名证书,例如用于调试和发布的证书,或者有多个应用程序,则重复此过程并在服务器上接受所有这些来源作为有效来源。

保存用户的密码

如果用户在您的应用中提供了用户名和密码以进行身份验证流程,您可以注册一个用户凭据,该凭据可用于验证用户。为此,请创建一个 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);
            }
        }
    );
}

支持凭据恢复

如果用户无法访问存储其凭据的设备,则可能需要从安全的在线备份中恢复。要详细了解如何支持此凭据恢复流程,请阅读本博文中的“恢复访问权限或添加新设备”部分:Google 密码管理器中密钥的安全.

添加对支持密钥端点知名 URL 的密码管理工具的支持

为了与密码和凭据管理工具无缝集成并实现未来兼容性,我们建议添加对密钥端点知名 URL 的支持。这是一种开放协议,允许协调方正式宣传他们对密钥的支持,并提供密钥注册和管理的直接链接。

  1. 对于位于 https://example.com 的信赖方,该信赖方拥有一个网站以及 Android 和 iOS 应用,知名 URL 将是 https://example.com/.well-known/passkey-endpoints
  2. 查询 URL 时,响应应使用以下模式

    {
      "enroll": "https://example.com/account/manage/passkeys/create"
      "manage": "https://example.com/account/manage/passkeys"
    }
    
  3. 要使此链接直接在您的应用中打开,而不是在网页上打开,请使用 Android 应用链接.

  4. 您可以在 GitHub 上的 密钥端点知名 URL 解释器中找到更多详细信息。

帮助用户管理其密钥,显示创建密钥的提供商

用户在管理与特定应用关联的多个密钥时面临的一个挑战是,识别要编辑或删除的正确密钥。为了帮助解决此问题,建议应用和网站在应用设置屏幕上的密钥列表中包含其他信息,例如创建凭据的提供商、创建日期和上次使用日期。提供商信息是通过检查与相应密钥关联的 AAGUID 获得的。AAGUID 可以作为密钥的验证器数据的一部分找到。

例如,如果用户使用 Google 密码管理器在 Android 设备上创建密钥,则 RP 会收到一个类似于“ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4”的 AAGUID。信赖方可以在密钥列表中对密钥进行注释,以表明它是使用 Google 密码管理器创建的。

要将 AAGUID 映射到密钥提供商,RP 可以使用 社区提供的 AAGUID 存储库。在 列表 中查找 AAGUID,以找到密钥提供商名称和图标。

详细了解 AAGUID 集成.

排查常见错误

下表显示了一些常见的错误代码和描述,并提供了一些关于其原因的信息

错误代码和描述 原因
在开始登录失败时:16:调用者由于登录提示取消次数过多而被暂时阻止。

如果您在开发过程中遇到此 24 小时的冷却时间,可以通过清除 Google Play 服务的应用存储空间来重置它。

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

在开始登录失败时:8:未知内部错误。
  1. 设备未正确设置 Google 帐户。
  2. 密钥 JSON 创建不正确。
CreatePublicKeyCredentialDomException:无法验证传入的请求 应用的包 ID 未在您的服务器上注册。在您的服务器端集成中验证这一点。
CreateCredentialUnknownException:在保存密码期间,从一键式 16 中发现密码失败响应:由于用户可能被提示使用 Android 自动填充,因此跳过密码保存 此错误仅在 Android 13 及更低版本上出现,并且仅当 Google 是自动填充提供程序时才会出现。在这种情况下,用户会看到来自自动填充的保存提示,并且密码会保存到 Google 密码管理器中。请注意,使用 Google 的自动填充保存的凭据将与 Credential Manager API 双向共享。因此,可以安全地忽略此错误。

其他资源

要详细了解 Credential Manager API 和密钥,请查看以下资源