了解如何在 Android 应用中使用 Credential Manager API 简化身份验证流程

1. 准备工作

传统的身份验证解决方案带来了许多安全性和易用性方面的挑战。

密码被广泛使用,但是...

  • 容易忘记
  • 用户需要具备相关知识来创建安全系数高的密码。
  • 攻击者可轻松钓鱼、获取和重现密码。

Android 一直致力于打造 Credential Manager API,以便简化登录体验并应对安全风险,具体方法是支持通行密钥这一新一代的无密码身份验证业界标准。

Credential Manager 整合了对通行密钥的支持,并将其与密码、“使用 Google 账号登录”等传统身份验证方法相结合。

用户将能够创建通行密钥,将它们存储在 Google 密码管理工具中,然后 Google 密码管理工具会在用户已登录账号的 Android 设备上同步这些通行密钥。必须先创建通行密钥、将其与用户账号关联,并将其公钥存储在服务器上,之后用户才能使用该通行密钥进行登录。

在此 Codelab 中,您将学习如何通过 Credential Manager API 使用通行密钥和密码进行注册,以及如何将其用于日后的身份验证。流程分为 2 步,包括:

  • 注册:使用通行密钥和密码。
  • 登录:使用通行密钥和已保存的密码。

前提条件

  • 对如何在 Android Studio 中运行应用有基本的了解。
  • 对 Android 应用中的身份验证流程有基本的了解。
  • 通行密钥有基本的了解。。

学习内容

  • 如何创建通行密钥。
  • 如何在密码管理工具中保存密码。
  • 如何使用通行密钥或已保存的密码对用户进行身份验证。

所需条件

以下任一设备组合:

  • 搭载 Android 9 或更高版本(适用于通行密钥)和 Android 4.4 或更高版本(用于通过 Credential Manager API 进行密码身份验证)的 Android 设备。
  • 设备最好带有生物识别传感器。
  • 请务必注册生物识别信息(或屏锁)。
  • Kotlin 插件版本:1.8.10

2. 进行设置

  1. 在您的笔记本电脑上,从 credman_codelab 分支克隆此代码库:https://github.com/android/identity-samples/tree/credman_codelab
  2. 前往 CredentialManager 模块,然后在 Android Studio 中打开项目。

我们来看看应用的初始状态

如需查看应用的初始状态的运作方式,请按以下步骤操作:

  1. 启动应用。
  2. 您将看到一个显示注册和登录按钮的主屏幕。
  3. 您可点击“注册”,使用通行密钥或密码进行注册。
  4. 您可点击“登录”,使用通行密钥和已保存的密码登录。

8c0019ff9011950a.jpeg

如需了解什么是通行密钥及其工作原理,请参阅通行密钥的工作原理

3. 添加使用通行密钥注册的功能

在使用 Credential Manager API 的 Android 应用中注册新账号时,用户可以为其账号创建通行密钥。该通行密钥将安全地存储在用户选择的凭据提供程序中,供日后登录使用,无需用户每次都输入密码。

现在,您将使用生物识别信息/屏锁创建通行密钥并注册用户凭据。

使用通行密钥注册

在 Credential Manager -> app -> main -> java -> SignUpFragment.kt 中,您可以看到一个文本字段“username”,以及一个使用通行密钥注册的按钮。

dcc5c529b310f2fb.jpeg

向 createPasskey() 调用传递质询码和其他 JSON 响应

创建通行密钥之前,您需要请求服务器获取必要的信息,以便在 createCredential() 调用期间将这些信息传递给 Credential Manager API。

幸运的是,您的资产 (RegFromServer.txt) 中已有一个模拟响应,它会在此 Codelab 中返回此类参数。

  • 在应用中,前往 SignUpFragment.kt 找到 signUpWithPasskeys 方法,在其中编写用于创建通行密钥并让用户进入的逻辑。您可以在同一个类中找到该方法。
  • 查看 else 块,其中包含调用 createPasskey() 的注释,并替换为以下代码:

SignUpFragment.kt

//TODO : Call createPasskey() to signup with passkey

val data = createPasskey()

当您在屏幕上填写了有效的用户名后,系统将调用此方法。

  • 在 createPasskey() 方法中,您需要创建一个 CreatePublicKeyCredentialRequest(),其中包含返回的必要参数。

SignUpFragment.kt

//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server

val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())

此 fetchRegistrationJsonFromServer() 方法会从资产中读取注册 JSON 响应,并返回创建通行密钥时要传递的注册 JSON。

  • 找到 fetchRegistrationJsonFromServer() 方法,将 TODO 替换为以下代码以返回 json,并移除空字符串 return 语句:

SignUpFragment.kt

//TODO fetch registration mock response

val response = requireContext().readFromAsset("RegFromServer")

//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
   .replace("<userName>", binding.username.text.toString())
   .replace("<userDisplayName>", binding.username.text.toString())
   .replace("<challenge>", getEncodedChallenge())
  • 在这里,您将读取来自资产的注册 JSON。
  • 此 JSON 包含 4 个要替换的字段。
  • UserId 必须是唯一的,以便用户可以创建多个通行密钥(如果需要)。将 <userId> 替换为生成的 userId。
  • <challenge> 也必须是唯一的,这样您将生成一个随机的唯一质询码。此方法已存在于您的代码中。

以下代码段包含您从服务器收到的选项示例:

{
  "challenge": String,
  "rp": {
    "name": String,
    "id": String
  },
  "user": {
    "id": String,
    "name": String,
    "displayName": String
  },
  "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"
  }
}

下表列出了 PublicKeyCredentialCreationOptions 字典中的一些重要参数,但并非详尽无遗:

参数

广告内容描述

challenge

服务器生成的随机字符串,其中包含足够的熵,导致无法进行猜测。长度应至少为 16 个字节。必须提供该字符串,但在注册期间不使用,除非进行证明

user.id

用户的唯一 ID。此值不得包含个人身份信息,例如电子邮件地址或用户名。使用系统为每个账号生成的 16 字节随机值即可。

user.name

此字段应包含用户可以识别的账号唯一标识符,例如电子邮件地址或用户名。此 ID 将显示在账号选择器中。(如果使用用户名,请使用与密码身份验证相同的值。)

user.displayName

此字段是可选、更容易记住的账号名称。它是直观易懂的用户账号名称,仅用于显示给用户看。

rp.id

信赖方实体对应于您的应用详情,它需要:

  • 名称(必填):您的应用名称
  • ID(可选):对应于域名或子域名。如果没有,系统会使用当前域名。
  • 图标(可选)。

pubKeyCredParams

公钥凭据参数是允许的算法和密钥类型的列表。此列表必须包含至少一个元素。

excludeCredentials

尝试注册设备的用户可能已注册其他设备。若要限制在一个身份验证器中针对同一账号创建多个凭据,您可以忽略这些设备。Transports 成员(如果提供)应包含每个凭据注册期间调用 getTransports() 的结果。

authenticatorSelection.authenticatorAttachment

指明设备是否应连接到平台上,或者对设备是否没有任何这方面的要求。将其设置为“platform”。这表示我们想要在平台设备中嵌入身份验证器,并且系统不会提示用户插入 USB 安全密钥等。

residentKey

表示创建通行密钥“必需”的值。

创建凭据

  1. 创建 CreatePublicKeyCredentialRequest() 后,您需要使用创建的请求调用 createCredential()。

SignUpFragment.kt

//TODO call createCredential() with createPublicKeyCredentialRequest

try {
   response = credentialManager.createCredential(
       requireActivity(),
       request
   ) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
   configureProgress(View.INVISIBLE)
   handlePasskeyFailure(e)
}
  • 您将所需信息传递给 createCredential()。
  • 请求成功后,屏幕上会显示底部动作条,提示您创建通行密钥。
  • 现在,用户可以通过生物识别信息或屏锁等方式验证身份。
  • 您需要处理渲染视图的可见性,并处理请求因某种原因而失败的异常。在此示例中,系统将记录错误消息,并将其显示在应用的错误对话框中。您可以通过 Android Studio 或 adb 调试命令查看完整的错误日志

93022cb87c00f1fc.png

  1. 现在,最后您需要将公钥凭据发送到服务器并让用户进入,从而完成注册过程。应用会接收一个包含公钥的凭据对象,您可以将该公钥发送给服务器以注册通行密钥。

在此示例中,我们使用了模拟服务器,因此我们仅返回 true,表示服务器已保存已注册的公钥,以用于将来的身份验证和验证。

在 signUpWithPasskeys() 方法中,找到相关注释并替换为以下代码:

SignUpFragment.kt

//TODO : complete the registration process after sending public key credential to your server and let the user in

data?.let {
   registerResponse()
   DataProvider.setSignedInThroughPasskeys(true)
   listener.showHome()
}
  • registerResponse 返回 true,指示(模拟)服务器已保存公钥以供将来使用。
  • 将 setSignedInThroughPasskeys 标志设置为 true,表示您通过通行密钥登录。
  • 登录后,将用户重定向到主屏幕。

以下代码段包含您应该会收到的选项示例:

{
  "id": String,
  "rawId": String,
  "type": "public-key",
  "response": {
    "clientDataJSON": String,
    "attestationObject": String,
  }
}

下表列出了 PublicKeyCredential 中的一些重要参数,但并非详尽无遗:

参数

说明

id

所创建通行密钥的 Base64URL 编码 ID。此 ID 有助于浏览器在进行身份验证时确定设备中是否存在匹配的通行密钥。此值必须存储在后端的数据库中。

rawId

ArrayBuffer 对象版本的凭据 ID。

response.clientDataJSON

ArrayBuffer 对象编码的客户端数据。

response.attestationObject

ArrayBuffer 编码的证明对象;其中包含一些重要信息,例如 RP ID、标志和公钥。

运行应用,您可以点击“Sign up with passkeys”按钮并创建通行密钥了。

4. 将密码保存到 Credential Provider 中

在此应用中的“SignUp”界面内,已经有用用户名和密码进行了注册的用户(用于演示目的)。

为了通过密码提供程序保存用户密码凭据,您需要实现要传递给 createCredential() 的 CreatePasswordRequest 以保存密码。

  • 找到 signUpWithPassword() 方法,将 TODO 替换为 createPassword 调用:

SignUpFragment.kt

//TODO : Save the user credential password with their password provider

createPassword()
  • createPassword() 方法中,您需要创建这样的密码请求,并将 TODO 替换为以下代码:

SignUpFragment.kt

//TODO : CreatePasswordRequest with entered username and password

val request = CreatePasswordRequest(
   binding.username.text.toString(),
   binding.password.text.toString()
)
  • 接下来,在 createPassword() 方法中,您需要使用创建密码请求创建凭据,并通过密码提供程序保存用户密码凭据,然后将 TODO 替换为以下代码:

SignUpFragment.kt

//TODO : Create credential with created password request


try {
   credentialManager.createCredential(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
   Log.e("Auth", " Exception Message : " + e.message)
}
  • 现在,您已成功通过用户的密码提供程序保存了密码凭据,只需点按一下,即可通过密码进行身份验证。

5. 添加使用通行密钥或密码进行身份验证的功能

现在,您可以将其作为一种安全地向您的应用进行身份验证的方式了。

629001f4a778d4fb.png

获取质询码以及要传递给 getPasskey() 调用的其他选项

在要求用户进行身份验证之前,您需要从服务器获取要传入 WebAuthn CreatePasswordRequest JSON 的参数(包括质询码)。

您的资产 (AuthFromServer.txt) 中已有一个模拟响应,它会在此 Codelab 中返回此类参数。

  • 在应用中,前往 SignInFragment.kt,找到 signInWithSavedCredentials 方法,在其中编写通过已保存的通行密钥或密码进行身份验证并允许用户进入的逻辑:
  • 查看 else 块,其中包含调用 createPasskey() 的注释,并替换为以下代码:

SignInFragment.kt

//TODO : Call getSavedCredentials() method to signin using passkey/password

val data = getSavedCredentials()
  • 在 getSavedCredentials() 方法内,您需要创建一个 GetPublicKeyCredentialOption(),其中包含从凭据提供程序获取凭据所需的必要参数。

SigninFragment.kt

//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server

val getPublicKeyCredentialOption =
   GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)

此 fetchAuthJsonFromServer() 是一个方法,该方法从资产中读取身份验证 JSON 响应并返回身份验证 JSON,以检索与此用户账号关联的所有通行密钥。

第二个参数:clientDataHash - 用于验证依赖方身份的哈希值,仅在您已设置 GetCredentialRequest.origin 时设置该参数。对于示例应用,此参数为 null。

如果您希望操作在没有可用凭据时立即返回,而不是回退到发现远程凭据,则第 3 个参数为 true,否则为 false(默认值)。

  • 查找 fetchAuthJsonFromServer() 方法,并将 TODO 替换为以下代码以返回 JSON,同时移除空字符串 return 语句:

SignInFragment.kt

//TODO fetch authentication mock json

return requireContext().readFromAsset("AuthFromServer")

注意:此 Codelab 的服务器会返回一个 JSON,该 JSON 与传递给 API 的 getCredential() 调用的 PublicKeyCredentialRequestOptions 字典极其相似。以下代码段包含您应该会收到的几个选项示例:

{
  "challenge": String,
  "rpId": String,
  "userVerification": "",
  "timeout": 1800000
}

下表列出了 PublicKeyCredentialRequestOptions 字典中的一些重要参数,但并非详尽无遗:

参数

说明

challenge

服务器在 ArrayBuffer 对象中生成的质询。该质询对于防范重放攻击至关重要。切勿在同一响应中接受同一质询两次。可将质询视为 CSRF 令牌

rpId

RP ID 是一个网域,网站可以指定自己的网域,也可以指定一个可注册后缀。此值必须与创建通行密钥时使用的 rp.id 参数一致。

  • 接下来,您需要创建一个 PasswordOption() 对象,以便通过此用户账号的 Credential Manager API 检索在密码提供程序中保存的所有密码。在 getSavedCredentials() 方法内,找到 TODO 并替换为以下内容:

SigninFragment.kt

//TODO create a PasswordOption to retrieve all the associated user's password

val getPasswordOption = GetPasswordOption()

获取凭据

  • 接下来,您需要使用上述所有选项调用 getCredential() 请求以检索关联的凭据:

SignInFragment.kt

//TODO call getCredential() with required credential options

val result = try {
   credentialManager.getCredential(
       requireActivity(),
       GetCredentialRequest(
           listOf(
               getPublicKeyCredentialOption,
               getPasswordOption
           )  
     )
   )
} catch (e: Exception) {
   configureViews(View.INVISIBLE, true)
   Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
   activity?.showErrorAlert(
       "An error occurred while authenticating through saved credentials. Check logs for additional details"
   )
   return null
}

if (result.credential is PublicKeyCredential) {
   val cred = result.credential as PublicKeyCredential
   DataProvider.setSignedInThroughPasskeys(true)
   return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
   val cred = result.credential as PasswordCredential
   DataProvider.setSignedInThroughPasskeys(false)
   return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
   //If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}

  • 将所需信息传递给 getCredential()。这会获取凭据选项列表和 activity 上下文,从而在该上下文的底部动作条中渲染选项。
  • 请求成功后,您会在屏幕上看到一个底部动作条,其中列出了为关联账号创建的所有凭据。
  • 现在,用户可以通过生物识别信息或屏锁等方式验证身份,以对所选凭据进行身份验证。
  • 将 setSignedInThroughPasskeys 标志设置为 true,表示您通过通行密钥登录。否则,设为 False。
  • 您需要处理渲染视图的可见性,并处理请求因某种原因而失败的异常。在此示例中,系统将记录错误消息,并将其显示在应用的错误对话框中。您可以通过 Android Studio 或 adb 调试命令查看完整的错误日志
  • 现在,最后您需要将公钥凭据发送到服务器并让用户进入,从而完成注册过程。应用会收到一个包含公钥的凭据对象,您可以将该公钥发送给服务器以通过通行密钥进行身份验证。

在此示例中,我们使用了模拟服务器,因此我们仅返回 true 以表明服务器已验证公钥。

signInWithSavedCredentials() 方法中,找到相关注释并替换为以下代码:

SignInFragment.kt

//TODO : complete the authentication process after validating the public key credential to your server and let the user in.

data?.let {
   sendSignInResponseToServer()
   listener.showHome()
}
  • sendSigninResponseToServer() 返回 true,表示(模拟)服务器已验证公钥以供将来使用。
  • 登录后,将用户重定向到主屏幕。

以下代码段包含一个示例 PublicKeyCredential 对象:

{
  "id": String
  "rawId": String
  "type": "public-key",
  "response": {
    "clientDataJSON": String
    "authenticatorData": String
    "signature": String
    "userHandle": String
  }
}

下表列出了 PublicKeyCredential 对象中的一些重要参数,但并非详尽无遗:

参数

说明

id

经过身份验证的通行密钥凭据的 Base64URL 编码 ID。

rawId

ArrayBuffer 对象版本的凭据 ID。

response.clientDataJSON

客户端数据的 ArrayBuffer 对象。此字段包含质询以及 RP 服务器需要验证的来源等信息。

response.authenticatorData

身份验证器数据的 ArrayBuffer 对象。此字段包含 RP ID 等信息。

response.signature

签名的 ArrayBuffer 对象。此值是凭据的核心,必须在服务器上进行验证。

response.userHandle

ArrayBuffer 对象,该对象包含系统在创建用户时设置的用户 ID。如果服务器需要选择其使用的 ID 值,或者后端希望避免为凭据 ID 创建索引,则可以使用此值来代替凭据 ID。

运行应用,前往“登录”->“使用通行密钥/已保存的密码登录”,然后尝试使用已保存的凭据登录。

试用

您在 Android 应用中创建了通行密钥、在 Credential Manager 中保存了密码,以及使用 Credential Manager API 通过通行密钥或保存的密码进行了身份验证。

6. 恭喜!

您已完成此 Codelab!如果您想查看最终解决方案,可以访问 https://github.com/android/identity-samples/tree/main/CredentialManager

如果您有任何疑问,请在 StackOverflow 上使用 passkey 标签提问。

了解详情