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

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

此外,对于用户而言,Credential Manager 可以在各种身份验证方法之间统一登录界面,这样无论用户选择何种方法,都可以更清晰、更轻松地登录应用。

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

关于通行密钥

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

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

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

如需有关如何使用 Credential Manager 实现顺畅的通行密钥身份验证流程的指导,请参阅使用通行密钥对用户进行身份验证

前提条件

若要使用 Credential Manager,请完成本部分中的步骤。

使用新近的平台版本

搭载 Android 4.4(API 级别 19)及更高版本的设备支持 Credential Manager。

为应用添加依赖项

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

implementation(libs.androidx.credentials)

// optional - needed for credentials support from play services, for devices running
// Android 13 and below.
implementation(libs.androidx.credentials.play.services.auth)

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

添加对 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/handle_all_urlsdelegate_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. 将以下代码行添加到清单文件中的 <application> 下:

    <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
    
  5. 如果您通过 Credential Manager 使用密码登录方式,请按照此步骤在清单中配置数字资产关联。如果您仅使用通行密钥,则无需执行此步骤。

    在 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
    

配置 Credential Manager

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

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

指示凭据字段

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

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

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

让用户登录

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

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

    // 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
    )
          
  2. 使用从上一步中检索到的选项构建登录请求。

    val credentialRequest = GetCredentialRequest(
      listOf(getPasswordOption, getPublicKeyCredentialOption),
    )
          
  3. 启动登录流程:

    coroutineScope {
      try {
        result = credentialManager.getCredential(
          // Use an activity-based context to avoid undefined system UI
          // launching behavior.
          context = activityContext,
          request = credentialRequest
        )
        handleSignIn(result)
      } catch (e: GetCredentialException) {
        // Handle failure
      }
    }
        
    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")
        }
      }
    }
        

以下示例展示了如何在获取通行密钥时设置 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 实例。

coroutineScope {
  try {
    result = credentialManager.getCredential(
      context = activityContext,
      request = credentialRequest
    )
  } catch (e: GetCredentialException) {
    Log.e("CredentialManager", "No credential available", e)
  }
}

在 Android 14 或更高版本中,您可以在调用 getCredential() 之前使用 prepareGetCredential() 方法缩短显示账号选择器时的延迟时间。

coroutineScope {
  val response = credentialManager.prepareGetCredential(
    GetCredentialRequest(
      listOf(
        getPublicKeyCredentialOption,
        getPasswordOption
      )
    )
  )
}

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

缓存的数据在 PrepareGetCredentialResponse 对象中返回。如果存在现有凭据,则系统会缓存结果,然后您可以稍后启动剩余的 getCredential() API 来调出包含缓存数据的账号选择器操作。

注册流程

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

创建通行密钥

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

suspend 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 {
    try {
      val result = credentialManager.createCredential(
        // Use an activity-based context to avoid undefined system
        // UI launching behavior
        context = activityContext,
        request = createPublicKeyCredentialRequest,
      )
      //  Handle passkey creation 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.
    }
    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 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}")
  }
}

设置 JSON 请求的格式

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

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

{
  "challenge": "abc123",
  "rp": {
    "name": "Credential Manager example",
    "id": "credential-manager-test.example.com"
  },
  "user": {
    "id": "def456",
    "name": "helloandroid@gmail.com",
    "displayName": "helloandroid@gmail.com"
  },
  "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 表示发出请求的应用或网站,通行密钥使用它来防范钓鱼式攻击。应用服务器需要对照已批准的应用和网站许可名单检查客户端数据来源。如果服务器从无法识别的来源收到来自应用或网站的请求,则应拒绝该请求。

对于网络情形,origin 反映了在凭据已登录的情况下的同网站来源。例如,如果网址为 https://www.example.com:8443/store?category=shoes#athletic,则 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 对象:

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

  // Create credential and handle result.
  coroutineScope {
    try {
      val result =
        credentialManager.createCredential(
          // Use an activity based context to avoid undefined
          // system UI launching behavior.
          activityContext,
          createPasswordRequest
        )
      // Handle register password result
    } catch (e: CreateCredentialException) {
      handleFailure(e)
    }
  }
}

支持凭据恢复

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

为用户自动创建通行密钥

如果用户没有通行密钥,您可以在用户下次使用密码管理工具中保存的密码登录时,为其自动创建通行密钥。为此,请在请求公共凭据时设置 isConditionalCreateRequest 字段:

CreatePublicKeyCredentialRequest(
    // other parameters
    isConditionalCreateRequest: Boolean = true
)

当用户登录时,系统会自动创建通行密钥并将其存储在用户选择的密码管理工具中。如果使用 Google 密码管理工具,用户必须已使用(通过 Credential Manager 或自动填充)密码管理工具中保存的密码。用户在创建此通行密钥时会收到通知,并且可以前往密码管理工具进行管理。

此功能需要 1.6.0-alpha01 或更高版本。

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

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

  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 上的通行密钥端点知名网址说明。

通过显示通行密钥的创建者,帮助用户管理通行密钥

在管理与给定应用关联的多个通行密钥时,用户面临的一个挑战是确定要修改或删除的正确通行密钥。为帮助解决此问题,建议应用和网站在应用的设置屏幕上的通行密钥列表中添加其他信息,例如创建凭据的提供程序、创建日期和上次使用日期。提供程序信息是通过检查与相应通行密钥关联的 AAGUID 获得的。AAGUID 可在通行密钥的身份验证器数据中找到。

例如,如果用户使用 Google 密码管理工具在 Android 设备上创建通行密钥,则 RP 会收到一个 AAGUID,其格式如下所示:“ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4”。依赖方可以在通行密钥列表中为通行密钥添加注释,以指明该通行密钥是使用 Google 密码管理工具创建的。

如需将 AAGUID 映射到通行密钥提供程序,RP 可以使用AAGUID 的社区来源代码库。在列表中查找 AAGUID,找到通行密钥提供程序名称和图标。

详细了解 AAGUID 集成

排查常见错误

如需了解常见错误代码、说明以及其原因,请参阅 Credential Manager 问题排查指南

其他资源

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