Trình quản lý thông tin xác thực là một API Jetpack hỗ trợ nhiều phương thức đăng nhập, chẳng hạn như đăng nhập bằng tên người dùng và mật khẩu, khoá truy cập và các giải pháp đăng nhập được liên kết (chẳng hạn như Đăng nhập bằng Google) trong một API duy nhất, từ đó đơn giản hoá quá trình tích hợp cho nhà phát triển.
Hơn nữa, đối với người dùng, Trình quản lý thông tin xác thực sẽ hợp nhất giao diện đăng nhập trên các phương thức xác thực, giúp người dùng đăng nhập vào ứng dụng một cách rõ ràng và dễ dàng hơn, bất kể họ chọn phương thức nào.
Giới thiệu về khoá truy cập
Khoá truy cập là phương thức thay thế an toàn và dễ dàng hơn cho mật khẩu. Với khoá truy cập, người dùng có thể đăng nhập vào các ứng dụng và trang web nhờ cảm biến sinh trắc học (chẳng hạn như dùng vân tay hoặc công nghệ nhận dạng khuôn mặt), mã PIN hoặc hình mở khoá. Khoá truy cập này mang lại trải nghiệm đăng nhập liền mạch, giúp người dùng không phải nhớ tên người dùng hoặc mật khẩu.
Khoá truy cập dựa vào WebAuthn (Ứng dụng xác thực người dùng truy cập Internet), một tiêu chuẩn do Liên minh FIDO và tổ chức the World Wide Web Consortium (W3C) cùng phát triển. WebAuthn sử dụng tiêu chuẩn mã hoá khoá công khai để xác thực người dùng. Trang web hoặc ứng dụng mà người dùng đăng nhập vào có thể xem và lưu trữ khoá công khai, nhưng tuyệt đối không nhận được khoá riêng tư. Khoá riêng tư này được giữ bí mật và an toàn. Chính vì khoá này là duy nhất cũng như gắn liền với trang web hoặc ứng dụng, nên khoá truy cập sẽ không thể đánh cắp, được tăng cường bảo mật.
Trình quản lý thông tin xác thực cho phép người dùng tạo khoá truy cập và lưu trữ các khoá đó trong Trình quản lý mật khẩu của Google.
Điều kiện tiên quyết
Để sử dụng Trình quản lý thông tin xác thực, hãy hoàn thành các bước trong phần này.
Sử dụng phiên bản nền tảng gần đây
Hỗ trợ Trình quản lý thông tin xác thực trên Android 4.4 (API cấp 19) trở lên.
Thêm phần phụ thuộc vào ứng dụng
Thêm các phần phụ thuộc sau vào tệp build.gradle
của mô-đun ứng dụng:
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") }
Thêm tính năng hỗ trợ cho Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số)
Để bật tính năng hỗ trợ khoá truy cập cho ứng dụng Android, hãy liên kết ứng dụng với một trang web mà ứng dụng của bạn sở hữu. Bạn có thể khai báo liên kết này bằng cách hoàn thành các bước sau:
Tạo tệp JSON chứa Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số). Ví dụ: để khai báo rằng trang web
https://signin.example.com
và ứng dụng Android có tên góicom.example
có thể dùng chung thông tin đăng nhập, hãy tạo một tệp có tênassetlinks.json
với nội dung sau:[{ "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 ] } }]
Trường
relation
là một mảng gồm một hoặc nhiều chuỗi mô tả mối quan hệ đang được khai báo. Để khai báo rằng các ứng dụng và trang web dùng chung thông tin đăng nhập, hãy chỉ định chuỗidelegate_permission/common.get_login_creds
.Trường
target
là đối tượng chỉ định tài sản mà nội dung khai báo sẽ áp dụng. Sau đây là các trường xác định một trang web:namespace
web
site
URL của trang web, có định dạng
https://domain[:optional_port]
; ví dụ:https://www.example.com
.domain phải đủ điều kiện và phải bỏ qua optional_port khi sử dụng cổng 443 cho HTTPS.
Đích
site
chỉ được là miền gốc: bạn không thể giới hạn liên kết ứng dụng với một thư mục con cụ thể. Không thêm một đường dẫn vào URL, chẳng hạn như dấu gạch chéo ở cuối.Miền con không được cân nhắc để khớp: nghĩa là nếu bạn chỉ định domain là
www.example.com
, thì miềnwww.counter.example.com
sẽ không được liên kết với ứng dụng của bạn.Sau đây là các trường xác định một ứng dụng Android:
namespace
android_app
package_name
Tên gói được khai báo trong tệp kê khai của ứng dụng. Ví dụ: com.example.android
sha256_cert_fingerprints
Các vân tay số SHA256 trong chứng chỉ ký của ứng dụng. Lưu trữ tệp JSON chứa Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số) tại vị trí sau trên miền đăng nhập:
https://domain[:optional_port]/.well-known/assetlinks.json
Ví dụ: nếu miền đăng nhập của bạn là
signin.example.com
, hãy lưu trữ tệp JSON tạihttps://signin.example.com/.well-known/assetlinks.json
.Loại MIME cho tệp Digital Asset Link (Đường liên kết đến tài sản kỹ thuật số) phải là JSON. Hãy đảm bảo máy chủ gửi một tiêu đề
Content-Type: application/json
trong phản hồi.Đảm bảo rằng máy chủ lưu trữ cho phép Google truy xuất tệp Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số) của bạn. Nếu bạn có tệp
robots.txt
, tệp đó phải cho phép tác nhân Googlebot truy xuất/.well-known/assetlinks.json
. Hầu hết các trang web chỉ cần cho phép mọi tác nhân tự động truy xuất tệp trong đường dẫn/.well-known/
để các dịch vụ khác có thể truy cập vào siêu dữ liệu trong những tệp đó:User-agent: * Allow: /.well-known/
Khai báo mối liên kết trong ứng dụng Android:
Thêm một tài nguyên chuỗi
asset_statements
vào tệpstrings.xml
. Chuỗiasset_statements
là một đối tượng JSON chỉ định các tệpassetlinks.json
cần tải. Bạn phải thoát mọi dấu nháy đơn và dấu ngoặc kép mà bạn dùng trong chuỗi. Ví dụ:<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
Định cấu hình Trình quản lý thông tin xác thực
Để định cấu hình và khởi tạo đối tượng CredentialManager
, hãy thêm logic tương tự như sau:
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)
Đăng nhập người dùng
Để truy xuất mọi tuỳ chọn mật khẩu và khoá truy cập được liên kết với tài khoản của người dùng, hãy hoàn tất các bước sau:
Khởi tạo các tuỳ chọn xác thực bằng mật khẩu và khoá truy cập:
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);
Tạo yêu cầu đăng nhập, sử dụng các tuỳ chọn được truy xuất từ bước trước:
Kotlin
val getCredRequest = GetCredentialRequest( listOf(getPasswordOption, getPublicKeyCredentialOption) )
Java
GetCredentialRequest getCredRequest = new GetCredentialRequest.Builder() .addCredentialOption(getPasswordOption) .addCredentialOption(getPublicKeyCredentialOption) .build();
Khởi chạy quy trình đăng nhập:
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); } } );
Đoạn mã sau đây trình bày một ví dụ về cách định dạng yêu cầu JSON khi bạn nhận được một khoá truy cập:
{
"challenge": "T1xCsnxM2DNL2KdK5CLa6fMhD7OBqho6syzInk_n-Uo",
"allowCredentials": [],
"timeout": 1800000,
"userVerification": "required",
"rpId": "credential-manager-app-test.glitch.me"
}
Đoạn mã sau đây minh hoạ phản hồi JSON mẫu sau khi bạn nhận được thông tin xác thực về khoá công khai:
{
"id": "KEDetxZcUfinhVi6Za5nZQ",
"type": "public-key",
"rawId": "KEDetxZcUfinhVi6Za5nZQ",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVDF4Q3NueE0yRE5MMktkSzVDTGE2Zk1oRDdPQnFobzZzeXpJbmtfbi1VbyIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
"authenticatorData": "j5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGQdAAAAAA",
"signature": "MEUCIQCO1Cm4SA2xiG5FdKDHCJorueiS04wCsqHhiRDbbgITYAIgMKMFirgC2SSFmxrh7z9PzUqr0bK1HZ6Zn8vZVhETnyQ",
"userHandle": "2HzoHm_hY0CjuEESY9tY6-3SdjmNHOoNqaPDcZGzsr0"
}
}
Quy trình đăng ký
Bạn có thể đăng ký phương thức xác thực cho người dùng bằng cách dùng khoá truy cập hoặc mật khẩu.
Tạo khoá truy cập
Để giúp người dùng có lựa chọn đăng ký khoá truy cập và dùng khoá truy cập đó cho quá trình xác thực lại, hãy đăng ký thông tin xác thực người dùng bằng cách sử dụng đối tượng 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()); } } } ); }
Định dạng yêu cầu JSON
Sau khi tạo mã xác thực, bạn phải liên kết mã xác thực đó với tài khoản của người dùng và lưu trữ khoá công khai của mã xác thực trên máy chủ của bạn. Đoạn mã sau đây trình bày ví dụ về cách định dạng yêu cầu JSON khi bạn tạo khoá truy cập.
Bài đăng trên blog này về xác thực liền mạch trong ứng dụng sẽ hướng dẫn bạn cách định dạng yêu cầu JSON khi tạo mã xác thực và khi xác thực bằng khoá truy cập. Bài đăng này cũng giải thích lý do mật khẩu không phải là giải pháp xác thực hiệu quả, cùng với cách tận dụng thông tin xác thực sinh trắc học có sẵn, cách liên kết ứng dụng với trang web mà bạn sở hữu, cách tạo mã xác thực và cách xác thực bằng cách sử dụng mã xác thực.
{
"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"
}
}
Xử lý phản hồi JSON
Đoạn mã sau đây minh hoạ phản hồi JSON mẫu để tạo thông tin xác thực khoá công khai. Tìm hiểu thêm về cách xử lý thông tin xác thực của khoá công khai trả về.
{
"id": "KEDetxZcUfinhVi6Za5nZQ",
"type": "public-key",
"rawId": "KEDetxZcUfinhVi6Za5nZQ",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibmhrUVhmRTU5SmI5N1Z5eU5Ka3ZEaVh1Y01Fdmx0ZHV2Y3JEbUdyT0RIWSIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOk1MTHpEdll4UTRFS1R3QzZVNlpWVnJGUXRIOEdjVi0xZDQ0NEZLOUh2YUkiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZ29vZ2xlLmNyZWRlbnRpYWxtYW5hZ2VyLnNhbXBsZSJ9",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUj5r_fLFhV-qdmGEwiukwD5E_5ama9g0hzXgN8thcFGRdAAAAAAAAAAAAAAAAAAAAAAAAAAAAEChA3rcWXFH4p4VYumWuZ2WlAQIDJiABIVgg4RqZaJyaC24Pf4tT-8ONIZ5_Elddf3dNotGOx81jj3siWCAWXS6Lz70hvC2g8hwoLllOwlsbYatNkO2uYFO-eJID6A"
}
}
Lưu mật khẩu của người dùng
Nếu người dùng cung cấp tên người dùng và mật khẩu cho luồng xác thực trong ứng dụng, thì bạn có thể đăng ký thông tin xác thực người dùng để dùng cho mục đích xác thực người dùng. Để thực hiện việc này, hãy tạo đối tượng 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) } } ); }
Khắc phục các lỗi thường gặp
Bảng sau đây trình bày mã và nội dung mô tả của một vài lỗi thường gặp, đồng thời cung cấp một số thông tin về nguyên nhân gây ra các lỗi đó:
Mã lỗi và nội dung mô tả | Nguyên nhân |
---|---|
Lỗi khi bắt đầu đăng nhập: 16: Phương thức gọi tạm thời bị chặn do có quá nhiều lời nhắc đăng nhập bị huỷ. | Nếu trong quá trình phát triển, bạn gặp phải lỗi thời gian chờ 24 giờ, bạn có thể đặt lại khoảng thời gian đó bằng cách xoá bộ nhớ ứng dụng của Dịch vụ Google Play. Ngoài ra, để bật/tắt thời gian chờ này trên trình mô phỏng hoặc thiết bị kiểm thử, hãy chuyển đến ứng dụng Trình quay số rồi nhập mã sau: |
Lỗi khi bắt đầu đăng nhập: 8: Lỗi nội bộ không xác định. |
|
CreatePublicKeyCredentialDomException: Không xác thực được yêu cầu đến | Mã gói của ứng dụng chưa được đăng ký với máy chủ. Hãy xác thực mã này trong quá trình tích hợp phía máy chủ. |