1. Trước khi bắt đầu
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách triển khai tính năng Đăng nhập bằng Google trên Android bằng Credential Manager.
Điều kiện tiên quyết
- Hiểu biết cơ bản về cách sử dụng Kotlin để phát triển Android
- Hiểu biết cơ bản về Jetpack Compose (Bạn có thể xem thêm thông tin tại đây)
Kiến thức bạn sẽ học được
- Cách tạo một dự án trên Google Cloud
- Cách tạo ứng dụng OAuth trong Google Cloud Console
- Cách triển khai tính năng Đăng nhập bằng Google bằng quy trình Trang tính dưới cùng
- Cách triển khai tính năng Đăng nhập bằng Google bằng quy trình Nút
Bạn cần
- Android Studio (Tải xuống tại đây)
- Máy tính đáp ứng yêu cầu về hệ thống của Android Studio
- Một máy tính đáp ứng yêu cầu về hệ thống của Trình mô phỏng Android
- Cài đặt Java và Java Development Kit (JDK)
2. Tạo một dự án Android Studio
Thời lượng từ 3:00 đến 5:00
Để bắt đầu, chúng ta cần tạo một dự án mới trong Android Studio:
- Mở Android Studio
- Nhấp vào Dự án mới
- Chọn Điện thoại và máy tính bảng và Hoạt động trống
- Nhấp vào Tiếp theo
- Giờ là lúc thiết lập một số phần của dự án:
- Tên: đây là tên của dự án
- Tên gói: tên này sẽ được điền sẵn dựa trên tên dự án của bạn
- Save location (Vị trí lưu): theo mặc định, vị trí này sẽ là thư mục mà Android Studio lưu các dự án của bạn. Bạn có thể thay đổi chế độ này thành bất cứ chế độ nào bạn muốn.
- SDK tối thiểu: Đây là phiên bản thấp nhất của Android SDK mà ứng dụng của bạn được tạo để chạy trên đó. Trong Lớp học lập trình này, chúng ta sẽ sử dụng API 36 (Baklava)
- Nhấp vào Finish (Hoàn tất).
- Android Studio sẽ tạo dự án và tải mọi phần phụ thuộc cần thiết cho ứng dụng cơ bản xuống. Quá trình này có thể mất vài phút. Để xem điều này xảy ra, chỉ cần nhấp vào biểu tượng bản dựng:
- Sau khi hoàn tất, Android Studio sẽ có dạng như sau:
3. Thiết lập dự án trên Google Cloud
Tạo một dự án trên Google Cloud
- Chuyển đến Google Cloud Console
- Mở dự án của bạn hoặc tạo một dự án mới
- Nhấp vào API và Dịch vụ
- Chuyển đến Màn hình xin phép bằng OAuth
- Bạn phải điền thông tin vào các trường trong phần Tổng quan để tiếp tục. Nhấp vào Bắt đầu để bắt đầu điền thông tin này:
- Tên ứng dụng: Tên của ứng dụng này, phải giống với tên bạn đã dùng khi tạo dự án trong Android Studio
- Email hỗ trợ người dùng: Email này sẽ cho biết Tài khoản Google mà bạn đang đăng nhập và mọi Nhóm Google mà bạn quản lý
- Đối tượng:
- Nội bộ cho một ứng dụng chỉ được dùng trong tổ chức của bạn. Nếu không có tổ chức nào liên kết với Dự án trên Google Cloud, thì bạn sẽ không thể chọn dự án này.
- Chúng ta sẽ sử dụng External.
- Thông tin liên hệ: Bạn có thể dùng bất kỳ email nào mà bạn muốn làm thông tin liên hệ cho ứng dụng
- Xem Chính sách dữ liệu người dùng của dịch vụ API của Google.
- Sau khi bạn xem xét và đồng ý với Chính sách về dữ liệu người dùng, hãy nhấp vào Tạo
Thiết lập ứng dụng OAuth
Giờ đây, khi đã thiết lập một Dự án trên Google Cloud, chúng ta cần thêm một Ứng dụng web và Ứng dụng Android để có thể thực hiện các lệnh gọi API đến máy chủ phụ trợ OAuth bằng mã ứng dụng của chúng.
Đối với Ứng dụng web Android, bạn cần:
- Tên gói của ứng dụng (ví dụ: com.example.example)
- Chữ ký SHA-1 của ứng dụng
- Chữ ký SHA-1 là gì?
- Vân tay số SHA-1 là một hàm băm mật mã được tạo từ khoá ký của ứng dụng. Khoá này đóng vai trò là giá trị nhận dạng riêng biệt cho chứng chỉ ký của ứng dụng cụ thể. Hãy coi đây là "chữ ký" kỹ thuật số cho ứng dụng của bạn.
- Tại sao chúng ta cần chữ ký SHA-1?
- Dấu vân tay SHA-1 đảm bảo rằng chỉ ứng dụng của bạn (được ký bằng khoá ký cụ thể) mới có thể yêu cầu mã truy cập bằng mã ứng dụng khách OAuth 2.0, ngăn các ứng dụng khác (ngay cả những ứng dụng có cùng tên gói) truy cập vào tài nguyên và dữ liệu người dùng của dự án.
- Hãy nghĩ như thế này:
- Khoá ký của ứng dụng giống như chìa khoá thực cho "cửa" ứng dụng của bạn. Đây là thứ cho phép truy cập vào hoạt động bên trong của ứng dụng.
- Dấu vân tay SHA-1 giống như mã nhận dạng thẻ khoá riêng biệt được liên kết với khoá vật lý của bạn. Đây là một mã cụ thể xác định khoá đó.
- Mã ứng dụng OAuth 2.0 giống như mã truy cập vào một tài nguyên hoặc dịch vụ cụ thể của Google (ví dụ: Đăng nhập bằng Google).
- Khi cung cấp dấu vân tay SHA-1 trong quá trình thiết lập ứng dụng OAuth, về cơ bản, bạn đang nói với Google rằng: "Chỉ thẻ khoá có mã nhận dạng cụ thể này (SHA-1) mới có thể mở mã truy cập này (mã ứng dụng)." Điều này đảm bảo chỉ ứng dụng của bạn mới có thể truy cập vào các dịch vụ của Google được liên kết với mã truy cập đó."
- Chữ ký SHA-1 là gì?
Đối với Web Client, tất cả những gì chúng tôi cần là tên mà bạn muốn dùng để xác định ứng dụng khách trong bảng điều khiển.
Tạo ứng dụng OAuth 2.0 chạy trên Android
- Chuyển đến trang Ứng dụng
- Nhấp vào Tạo ứng dụng
- Chọn Android cho Loại ứng dụng
- Bạn sẽ cần chỉ định tên gói của ứng dụng
- Trong Android Studio, chúng ta cần lấy chữ ký SHA-1 của ứng dụng và sao chép/dán vào đây:
- Chuyển đến Android Studio rồi mở cửa sổ dòng lệnh
- Chạy lệnh này: Mac/Linux:
Windows:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Lệnh này được thiết kế để liệt kê thông tin chi tiết của một mục cụ thể (bí danh) trong một kho khoá.keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
-list
: Lựa chọn này yêu cầu keytool liệt kê nội dung của kho khoá.-v
: Lựa chọn này cho phép xuất ra thông tin chi tiết, cung cấp thông tin chi tiết hơn về mục nhập.-keystore ~/.android/debug.keystore
: Chỉ định đường dẫn đến tệp kho khoá.-alias androiddebugkey
: Tham số này chỉ định bí danh (tên mục) của khoá mà bạn muốn kiểm tra.-storepass android
: Tham số này cung cấp mật khẩu cho tệp kho khoá.-keypass android
: Lệnh này cung cấp mật khẩu cho khoá riêng tư của bí danh đã chỉ định.
- Sao chép giá trị của chữ ký SHA-1:
- Quay lại cửa sổ Google Cloud rồi dán giá trị chữ ký SHA-1 vào:
- Màn hình của bạn hiện sẽ trông tương tự như thế này và bạn có thể nhấp vào Tạo:
Tạo ứng dụng OAuth 2.0 cho web
- Để tạo mã ứng dụng khách của ứng dụng Web, hãy lặp lại các bước 1-2 trong phần Tạo ứng dụng khách Android và chọn Ứng dụng Web cho Loại ứng dụng
- Đặt tên cho ứng dụng khách (đây sẽ là Ứng dụng OAuth):
- Nhấp vào Tạo
- Hãy sao chép mã ứng dụng khách từ cửa sổ bật lên, bạn sẽ cần mã này sau này
Giờ đây, sau khi thiết lập xong các ứng dụng OAuth, chúng ta có thể quay lại Android Studio để tạo ứng dụng Android Đăng nhập bằng Google!
4. Thiết lập Thiết bị Android ảo
Để kiểm thử nhanh ứng dụng mà không cần thiết bị Android thực, bạn nên tạo một Thiết bị Android ảo để có thể tạo và chạy ngay ứng dụng trên Android Studio. Nếu muốn kiểm thử bằng thiết bị Android thực, bạn có thể làm theo hướng dẫn trong tài liệu dành cho nhà phát triển Android
Tạo Thiết bị Android ảo
- Trong Android Studio, hãy mở Trình quản lý thiết bị
- Nhấp vào nút + > Tạo thiết bị ảo
- Tại đây, bạn có thể thêm mọi thiết bị cần thiết cho dự án của mình. Để phục vụ mục đích của Lớp học lập trình này, hãy chọn Medium Phone (Điện thoại cỡ trung) rồi nhấp vào Next (Tiếp theo)
- Giờ đây, bạn có thể định cấu hình thiết bị cho dự án của mình bằng cách đặt cho thiết bị một tên riêng biệt, chọn phiên bản Android mà thiết bị sẽ chạy và nhiều thao tác khác. Đảm bảo rằng API được đặt thành API 36 "Baklava"; Android 16 rồi nhấp vào Finish (Hoàn tất)
- Bạn sẽ thấy thiết bị mới xuất hiện trong Trình quản lý thiết bị. Để xác minh rằng thiết bị đang chạy, hãy nhấp vào
bên cạnh thiết bị mà bạn vừa tạo
- Thiết bị hiện sẽ chạy!
Đăng nhập vào Thiết bị Android ảo
Thiết bị mà bạn vừa tạo hoạt động. Giờ đây, để ngăn chặn lỗi khi kiểm thử tính năng Đăng nhập bằng Google, bạn sẽ cần đăng nhập vào thiết bị bằng Tài khoản Google.
- Chuyển đến phần Cài đặt:
- Nhấp vào giữa màn hình trên thiết bị ảo rồi vuốt lên
- Tìm ứng dụng Cài đặt rồi nhấp vào ứng dụng đó
- Nhấp vào Google trong phần Cài đặt
- Nhấp vào Đăng nhập rồi làm theo lời nhắc để đăng nhập vào Tài khoản Google của bạn
- Giờ đây, bạn sẽ đăng nhập vào thiết bị
Thiết bị Android ảo của bạn hiện đã sẵn sàng để kiểm thử!
5. Thêm phần phụ thuộc
Thời lượng: 5:00
Để thực hiện các lệnh gọi API OAuth, trước tiên, chúng ta cần tích hợp các thư viện cần thiết cho phép chúng ta thực hiện các yêu cầu xác thực và sử dụng mã nhận dạng của Google để thực hiện các yêu cầu đó:
- libs.googleid
- libs.play.services.auth
- Chuyển đến File > Project Structure (Tệp > Cấu trúc dự án):
- Sau đó, chuyển đến Dependencies (Phần phụ thuộc) > app (ứng dụng) > '+' > Library Dependency (Phần phụ thuộc thư viện)
- Bây giờ, chúng ta cần thêm các thư viện:
- Trong hộp thoại tìm kiếm, hãy nhập googleid rồi nhấp vào Tìm kiếm
- Chỉ nên có một mục, hãy chọn mục đó và phiên bản cao nhất hiện có (Tại thời điểm diễn ra Lớp học lập trình này, phiên bản cao nhất là 1.1.1)
- Nhấp vào OK
- Lặp lại các bước 1-3 nhưng thay vào đó, hãy tìm "play-services-auth" rồi chọn dòng có "com.google.android.gms" làm Mã nhóm và "play-services-auth" làm Tên cấu phần phần mềm
- Nhấp vào OK
6. Luồng bảng dưới cùng
Quy trình bảng dưới cùng tận dụng Credential Manager API để người dùng có thể đăng nhập vào ứng dụng của bạn bằng Tài khoản Google trên Android một cách đơn giản. Tính năng này được thiết kế để hoạt động nhanh chóng và thuận tiện, đặc biệt là đối với người dùng cũ. Quy trình này sẽ được kích hoạt khi ứng dụng khởi chạy.
Tạo yêu cầu đăng nhập
- Để bắt đầu, hãy xoá các hàm
Greeting()
vàGreetingPreview()
khỏiMainActivity.kt
vì chúng ta sẽ không cần dùng đến. - Giờ đây, chúng ta cần đảm bảo rằng các gói cần thiết đã được nhập cho dự án này. Tiếp tục và thêm các câu lệnh
import
sau đây sau các câu lệnh hiện có bắt đầu từ dòng 3:import android.content.ContentValues.TAG import android.content.Context import android.credentials.GetCredentialException import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.NoCredentialException import androidx.credentials.GetCredentialRequest import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.CoroutineScope import androidx.compose.runtime.LaunchedEffect import kotlinx.coroutines.delay import kotlinx.coroutines.launch
- Tiếp theo, chúng ta phải tạo hàm để tạo yêu cầu cho Trang tính dưới cùng. Dán mã này vào bên dưới Lớp MainActivity
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
//We will build out this function in a moment
signIn(requestFalse, context)
}
}
}
//This function is used to generate a secure nonce to pass in with our request
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
Hãy phân tích những gì đoạn mã này đang làm:
fun BottomSheet(webClientId: String) {...}
: Tạo một hàm có tên là BottomSheet nhận một đối số chuỗi có tên là webClientid
val context = LocalContext.current
: Truy xuất ngữ cảnh Android hiện tại. Điều này là cần thiết cho nhiều thao tác, bao gồm cả việc khởi chạy các thành phần trên giao diện người dùng.LaunchedEffect(Unit) { ... }
:LaunchedEffect
là một thành phần kết hợp Jetpack Compose cho phép bạn chạy một hàm tạm ngưng (hàm có thể tạm dừng và tiếp tục thực thi) trong vòng đời của thành phần kết hợp. Unit làm khoá có nghĩa là hiệu ứng này sẽ chỉ chạy một lần khi thành phần kết hợp được khởi chạy lần đầu tiên.val googleIdOption: GetGoogleIdOption = ...
: Tạo một đối tượngGetGoogleIdOption
. Đối tượng này định cấu hình loại thông tin đăng nhập được yêu cầu từ Google..Builder()
: Mẫu trình tạo được dùng để định cấu hình các lựa chọn..setFilterByAuthorizedAccounts(true)
: Chỉ định xem có cho phép người dùng chọn trong số tất cả Tài khoản Google hay chỉ những tài khoản đã uỷ quyền cho ứng dụng. Trong trường hợp này, giá trị được đặt thành true, tức là yêu cầu sẽ được thực hiện bằng thông tin đăng nhập mà người dùng đã uỷ quyền sử dụng với ứng dụng này (nếu có)..setServerClientId(webClientId)
: Đặt mã ứng dụng khách máy chủ, đây là giá trị nhận dạng duy nhất cho phần phụ trợ của ứng dụng. Đây là yêu cầu bắt buộc để nhận mã nhận dạng..setNonce(generateSecureRandomNonce())
: Đặt một số chỉ dùng một lần (một giá trị ngẫu nhiên) để ngăn chặn các cuộc tấn công phát lại và đảm bảo mã thông báo nhận dạng được liên kết với yêu cầu cụ thể..build()
: Tạo đối tượngGetGoogleIdOption
bằng cấu hình đã chỉ định.
val request: GetCredentialRequest = ...
: Tạo một đối tượngGetCredentialRequest
. Đối tượng này đóng gói toàn bộ yêu cầu về thông tin xác thực..Builder()
: Bắt đầu mẫu trình tạo để định cấu hình yêu cầu..addCredentialOption(googleIdOption)
: Thêm googleIdOption vào yêu cầu, chỉ định rằng chúng ta muốn yêu cầu mã thông báo mã nhận dạng Google..build()
: Tạo đối tượngGetCredentialRequest
.
val e = signIn(request, context)
: Thao tác này cố gắng đăng nhập cho người dùng bằng yêu cầu đã tạo và bối cảnh hiện tại. Kết quả của hàm signIn được lưu trữ trong e. Biến này sẽ chứa kết quả thành công hoặc một ngoại lệ.if (e is NoCredentialException) { ... }
: Đây là một quy trình kiểm tra có điều kiện. Nếu hàm signIn không thành công với NoCredentialException, tức là không có tài khoản nào được uỷ quyền trước đó.val googleIdOptionFalse: GetGoogleIdOption = ...
: NếusignIn
trước đó không thành công, phần này sẽ tạo mộtGetGoogleIdOption
mới..setFilterByAuthorizedAccounts(false)
: Đây là điểm khác biệt quan trọng so với lựa chọn đầu tiên. Thao tác này sẽ tắt tính năng lọc tài khoản được uỷ quyền, tức là bạn có thể dùng mọi Tài khoản Google trên thiết bị để đăng nhập.val requestFalse: GetCredentialRequest = ...
: MộtGetCredentialRequest
mới được tạo bằnggoogleIdOptionFalse
.signIn(requestFalse, context)
: Thao tác này cố gắng đăng nhập người dùng bằng yêu cầu mới cho phép sử dụng mọi tài khoản.
Về cơ bản, đoạn mã này chuẩn bị một yêu cầu gửi đến Credential Manager API để truy xuất mã nhận dạng Google cho người dùng, bằng cách sử dụng các cấu hình được cung cấp. Sau đó, bạn có thể dùng GetCredentialRequest để chạy giao diện người dùng của trình quản lý thông tin đăng nhập. Tại đây, người dùng có thể chọn Tài khoản Google của họ và cấp các quyền cần thiết.
fun generateSecureRandomNonce(byteLength: Int = 32): String
: Thao tác này xác định một hàm có tên là generateSecureRandomNonce
. Hàm này chấp nhận một đối số số nguyên byteLength (với giá trị mặc định là 32) chỉ định độ dài mong muốn của số chỉ dùng một lần tính bằng byte. Phương thức này trả về một chuỗi là biểu thị được mã hoá Base64 của các byte ngẫu nhiên.
val randomBytes = ByteArray(byteLength)
: Tạo một mảng byte có byteLength được chỉ định để lưu trữ các byte ngẫu nhiên.SecureRandom.getInstanceStrong().nextBytes(randomBytes)
:SecureRandom.getInstanceStrong()
: Thao tác này sẽ lấy một trình tạo số ngẫu nhiên mạnh về mật mã. Điều này rất quan trọng đối với tính bảo mật, vì nó đảm bảo các số được tạo là hoàn toàn ngẫu nhiên và không thể đoán trước. Nó sử dụng nguồn entropy mạnh nhất có sẵn trên hệ thống..nextBytes(randomBytes)
: Thao tác này điền vào mảng randomBytes bằng các byte ngẫu nhiên do phiên bản SecureRandom tạo.
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
:Base64.getUrlEncoder()
: Thao tác này sẽ lấy một bộ mã hoá Base64 sử dụng bảng chữ cái an toàn với URL (sử dụng – và _ thay vì + và /). Điều này rất quan trọng vì đảm bảo chuỗi kết quả có thể được sử dụng an toàn trong URL mà không cần mã hoá thêm..withoutPadding()
: Thao tác này sẽ xoá mọi ký tự đệm khỏi chuỗi được mã hoá Base64. Điều này thường được mong muốn để làm cho số chỉ dùng một lần ngắn hơn và gọn hơn một chút..encodeToString(randomBytes)
: Hàm này mã hoá randomBytes thành một chuỗi Base64 và trả về chuỗi đó.
Tóm lại, hàm này tạo ra một số chỉ dùng một lần ngẫu nhiên có độ dài cụ thể và được mã hoá bằng Base64 an toàn cho URL, đồng thời trả về chuỗi kết quả. Đây là một phương pháp tiêu chuẩn để tạo số chỉ dùng một lần an toàn khi sử dụng trong các bối cảnh nhạy cảm về bảo mật.
Đưa ra yêu cầu đăng nhập
Giờ đây, khi đã có thể tạo yêu cầu đăng nhập, chúng ta có thể dùng Trình quản lý thông tin xác thực để sử dụng yêu cầu đó cho việc đăng nhập. Để làm được điều này, chúng ta cần tạo một hàm xử lý việc truyền các yêu cầu đăng nhập bằng Trình quản lý thông tin đăng nhập, đồng thời xử lý các trường hợp ngoại lệ thường gặp mà chúng ta có thể gặp phải.
Bạn có thể dán hàm này bên dưới hàm BottomSheet()
để thực hiện việc này.
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}
Sau đây là thông tin chi tiết về những gì đoạn mã này đang thực hiện:
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?
: Thao tác này xác định một hàm tạm ngưng có tên là signIn. Điều này có nghĩa là bạn có thể tạm dừng và tiếp tục mà không chặn luồng chính.Nó trả về một Exception?
sẽ có giá trị rỗng nếu quá trình đăng nhập thành công hoặc ngoại lệ cụ thể nếu quá trình đăng nhập không thành công.
Hàm này có 2 tham số:
request
: Một đối tượngGetCredentialRequest
chứa cấu hình cho loại thông tin đăng nhập cần truy xuất (ví dụ: Mã nhận dạng trên Google).context
: Android Context cần thiết để tương tác với hệ thống.
Đối với nội dung của hàm:
val credentialManager = CredentialManager.create(context)
: Tạo một phiên bản CredentialManager, đây là giao diện chính để tương tác với Credential Manager API. Đây là cách ứng dụng sẽ bắt đầu quy trình đăng nhập.val failureMessage = "Sign in failed!"
: Xác định một chuỗi (failureMessage) sẽ xuất hiện trong thông báo tạm thời khi đăng nhập không thành công.var e: Exception? = null
: Dòng này khởi tạo một biến e để lưu trữ mọi ngoại lệ có thể xảy ra trong quá trình này, bắt đầu bằng giá trị rỗng.delay(250)
: Giới thiệu độ trễ 250 mili giây. Đây là giải pháp cho một vấn đề tiềm ẩn có thể xảy ra khi NoCredentialException được truyền ngay khi ứng dụng khởi động, đặc biệt là khi sử dụng quy trình BottomSheet. Điều này giúp hệ thống có thời gian khởi động trình quản lý thông tin xác thực.try { ... } catch (e: Exception) { ... }
:Khối try-catch được dùng để xử lý lỗi một cách hiệu quả. Điều này đảm bảo rằng nếu có lỗi xảy ra trong quá trình đăng nhập, ứng dụng sẽ không gặp sự cố và có thể xử lý ngoại lệ một cách linh hoạt.val result = credentialManager.getCredential(request = request, context = context)
: Đây là nơi diễn ra lệnh gọi thực tế đến API Trình quản lý thông tin xác thực và bắt đầu quy trình truy xuất thông tin xác thực. Phương thức này lấy yêu cầu và bối cảnh làm dữ liệu đầu vào, đồng thời sẽ trình bày một giao diện người dùng để người dùng chọn thông tin đăng nhập. Nếu thành công, phương thức này sẽ trả về một kết quả chứa thông tin xác thực đã chọn. Kết quả của thao tác này,GetCredentialResponse
, được lưu trữ trong biếnresult
.Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
:Hiển thị một thông báo nhanh ngắn cho biết quá trình đăng nhập đã thành công.Log.i(TAG, "Sign in Successful!")
: Ghi một thông báo vui vẻ, thành công vào logcat.catch (e: GetCredentialException)
: Xử lý các ngoại lệ thuộc loạiGetCredentialException
. Đây là một lớp mẹ cho một số trường hợp ngoại lệ cụ thể có thể xảy ra trong quá trình tìm nạp thông tin đăng nhập.catch (e: GoogleIdTokenParsingException)
: Xử lý các trường hợp ngoại lệ xảy ra khi có lỗi phân tích cú pháp Mã thông báo cho mã nhận dạng trên Google.catch (e: NoCredentialException)
: Xử lýNoCredentialException
, được gửi khi người dùng không có thông tin đăng nhập (ví dụ: họ chưa lưu thông tin đăng nhập nào hoặc họ không có Tài khoản Google).- Điều quan trọng là hàm này trả về ngoại lệ được lưu trữ trong
e
,NoCredentialException
, cho phép phương thức gọi xử lý trường hợp cụ thể nếu không có thông tin đăng nhập.
- Điều quan trọng là hàm này trả về ngoại lệ được lưu trữ trong
catch (e: GetCredentialCustomException)
: Xử lý các ngoại lệ tuỳ chỉnh mà trình cung cấp thông tin xác thực có thể gửi.catch (e: GetCredentialCancellationException)
: Xử lýGetCredentialCancellationException
, được gửi khi người dùng huỷ quy trình đăng nhập.Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
: Hiển thị một thông báo ngắn cho biết rằng quá trình đăng nhập không thành công bằng failureMessage.Log.e(TAG, "", e)
: Ghi nhật ký ngoại lệ vào logcat của Android bằng Log.e (dùng cho lỗi). Thao tác này sẽ bao gồm stacktrace của ngoại lệ để giúp gỡ lỗi. Ngoài ra, ứng dụng này còn có biểu tượng cảm xúc giận dữ để bạn giải trí.
return e
: Hàm này trả về trường hợp ngoại lệ nếu có, hoặc giá trị rỗng nếu đăng nhập thành công.
Tóm lại, đoạn mã này cung cấp một cách xử lý hoạt động đăng nhập của người dùng bằng API Trình quản lý thông tin xác thực, quản lý hoạt động không đồng bộ, xử lý các lỗi có thể xảy ra và cung cấp thông tin phản hồi cho người dùng thông qua thông báo và nhật ký, đồng thời thêm một chút hài hước vào quá trình xử lý lỗi.
Triển khai quy trình bảng dưới cùng trên ứng dụng
Giờ đây, chúng ta có thể thiết lập một lệnh gọi để kích hoạt quy trình BottomSheet trong lớp MainActivity
bằng cách sử dụng mã sau và Mã ứng dụng khách của ứng dụng web mà chúng ta đã sao chép từ Google Cloud Console trước đó:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
//This will trigger on launch
BottomSheet(webClientId)
}
}
}
}
}
Giờ đây, chúng ta có thể lưu dự án (File > Save) và chạy dự án:
- Nhấn nút chạy:
- Sau khi ứng dụng chạy trên trình mô phỏng, bạn sẽ thấy BottomSheet đăng nhập bật lên. Hãy nhấp vào Tiếp tục để kiểm thử tính năng đăng nhập
- Bạn sẽ thấy một thông báo Toast cho biết bạn đã đăng nhập thành công!
7. Luồng nút
Luồng nút Đăng nhập bằng Google giúp người dùng dễ dàng đăng ký hoặc đăng nhập vào ứng dụng Android của bạn bằng Tài khoản Google hiện có. Họ sẽ nhấn vào nút này nếu đóng trang tính dưới cùng hoặc chỉ muốn sử dụng Tài khoản Google của mình một cách rõ ràng để đăng nhập hoặc đăng ký. Đối với nhà phát triển, điều này có nghĩa là quá trình tham gia suôn sẻ hơn và ít trở ngại hơn trong quá trình đăng ký.
Mặc dù có thể thực hiện việc này bằng nút Jetpack Compose có sẵn, nhưng chúng ta sẽ sử dụng một biểu tượng thương hiệu đã được phê duyệt trước trên trang Nguyên tắc sử dụng thương hiệu của tính năng Đăng nhập bằng Google.
Thêm biểu tượng thương hiệu vào dự án
- Tải tệp ZIP chứa các biểu tượng thương hiệu đã được phê duyệt trước tại đây
- Giải nén tệp signin-assest.zip trong thư mục tải xuống (thao tác này sẽ khác nhau tuỳ thuộc vào hệ điều hành của máy tính). Giờ đây, bạn có thể mở thư mục signin-assets và xem các biểu tượng có sẵn. Trong Lớp học lập trình này, chúng ta sẽ sử dụng
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
. - Sao chép tệp
- Dán vào dự án trong Android Studio trong phần res > drawable bằng cách nhấp chuột phải vào thư mục drawable rồi nhấp vào Paste (Dán) (bạn có thể phải mở rộng thư mục res để thấy thư mục này)
- Một hộp thoại sẽ xuất hiện, nhắc bạn đổi tên tệp và xác nhận thư mục mà tệp sẽ được thêm vào. Đổi tên thành phần thành siwg_button.png rồi nhấp vào OK
Mã luồng nút
Mã này sẽ sử dụng cùng hàm signIn()
được dùng cho BottomSheet()
nhưng sử dụng GetSignInWithGoogleOption
thay vì GetGoogleIdOption
vì quy trình này không tận dụng thông tin đăng nhập và khoá truy cập được lưu trữ trên thiết bị để hiện các lựa chọn đăng nhập. Sau đây là mã mà bạn có thể dán vào bên dưới hàm BottomSheet()
:
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
Để phân tích những gì mã này đang làm:
fun ButtonUI(webClientId: String)
: Thao tác này khai báo một hàm có tên là ButtonUI
, chấp nhận webClientId
(mã ứng dụng khách của dự án trên Google Cloud) làm đối số.
val context = LocalContext.current
: Truy xuất ngữ cảnh Android hiện tại. Điều này là cần thiết cho nhiều thao tác, bao gồm cả việc khởi chạy các thành phần trên giao diện người dùng.
val coroutineScope = rememberCoroutineScope()
: Tạo một phạm vi coroutine. Điều này được dùng để quản lý các tác vụ không đồng bộ, cho phép mã chạy mà không chặn luồng chính. rememberCoroutineScope
() là một hàm có khả năng kết hợp trong Jetpack Compose, sẽ cung cấp một phạm vi được liên kết với vòng đời của thành phần kết hợp.
val onClick: () -> Unit = { ... }
: Thao tác này sẽ tạo một hàm lambda được thực thi khi người dùng nhấp vào nút. Hàm lambda sẽ:
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build()
: Phần này tạo một đối tượngGetSignInWithGoogleOption
. Đối tượng này dùng để chỉ định các tham số cho quy trình "Đăng nhập bằng Google", yêu cầuwebClientId
và một số chỉ dùng một lần (một chuỗi ngẫu nhiên dùng cho mục đích bảo mật).val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
: Phương thức này tạo một đối tượngGetCredentialRequest
. Yêu cầu này sẽ được dùng để lấy thông tin xác thực của người dùng thông qua Trình quản lý thông tin xác thực.GetCredentialRequest
sẽ thêmGetSignInWithGoogleOption
đã tạo trước đó làm một lựa chọn để yêu cầu thông tin đăng nhập "Đăng nhập bằng Google".
coroutineScope.launch { ... }
: MộtCoroutineScope
để quản lý các thao tác không đồng bộ (bằng cách sử dụng coroutine).signIn(request, context)
: gọi hàmsignIn
() mà chúng ta đã xác định trước đó
Image(...)
: Thao tác này hiển thị một hình ảnh bằng cách sử dụng painterResource
tải hình ảnh R.drawable.siwg_button
Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick)
:fillMaxSize()
: Giúp hình ảnh lấp đầy khoảng trống có sẵn.clickable(enabled = true, onClick = onClick)
: Khiến hình ảnh có thể nhấp vào và khi được nhấp vào, hình ảnh sẽ thực thi hàm lambda onClick đã xác định trước đó.
Tóm lại, đoạn mã này thiết lập nút "Đăng nhập bằng Google" trong giao diện người dùng Jetpack Compose. Khi người dùng nhấp vào nút này, nút sẽ chuẩn bị một yêu cầu về thông tin đăng nhập để khởi chạy Credential Manager và cho phép người dùng đăng nhập bằng Tài khoản Google của họ.
Bây giờ, bạn cần cập nhật lớp MainActivity để chạy hàm ButtonUI()
:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
Giờ đây, chúng ta có thể lưu dự án (File > Save) và chạy dự án:
- Nhấn nút chạy:
- Sau khi ứng dụng chạy trên trình mô phỏng, BottomSheet sẽ xuất hiện. Nhấp vào bên ngoài để đóng.
- Bây giờ, bạn sẽ thấy nút mà chúng ta đã tạo xuất hiện trong ứng dụng. Hãy nhấp vào nút đó để xem hộp thoại đăng nhập
- Nhấp vào tài khoản của bạn để đăng nhập!
8. Kết luận
Bạn đã hoàn thành lớp học lập trình này! Để biết thêm thông tin hoặc được trợ giúp về tính năng Đăng nhập bằng Google trên Android, hãy xem phần Câu hỏi thường gặp bên dưới:
Câu hỏi thường gặp
- Stackoverflow
- Hướng dẫn khắc phục sự cố về Trình quản lý thông tin xác thực trên Android
- Câu hỏi thường gặp về Trình quản lý thông tin xác thực trên Android
- Trung tâm trợ giúp về việc xác minh ứng dụng OAuth
Toàn bộ mã MainActivity.kt
Sau đây là mã đầy đủ cho MainActivity.kt để bạn tham khảo:
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
signIn(coroutineScope, request, context)
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}