1. ก่อนเริ่มต้น
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ใน Android โดยใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบ
ข้อกำหนดเบื้องต้น
- ความเข้าใจพื้นฐานเกี่ยวกับการใช้ Kotlin ในการพัฒนาแอป Android
- มีความเข้าใจพื้นฐานเกี่ยวกับ Jetpack Compose (ดูข้อมูลเพิ่มเติมได้ที่นี่)
สิ่งที่คุณจะได้เรียนรู้
- วิธีสร้างโปรเจ็กต์ Google Cloud
- วิธีสร้างไคลเอ็นต์ OAuth ใน Google Cloud Console
- วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google โดยใช้โฟลว์ของชีตด้านล่าง
- วิธีติดตั้งใช้งานฟีเจอร์ลงชื่อเข้าใช้ด้วย Google โดยใช้ขั้นตอนปุ่ม
สิ่งที่ต้องมี
- Android Studio (ดาวน์โหลดที่นี่)
- คอมพิวเตอร์ที่มีคุณสมบัติตรงตามข้อกำหนดของระบบสำหรับ Android Studio
- คอมพิวเตอร์ที่มีคุณสมบัติตรงตามข้อกำหนดของระบบสำหรับ Android Emulator
- การติดตั้ง Java และ Java Development Kit (JDK)
2. สร้างโปรเจ็กต์ Android Studio
ระยะเวลา 3:00 - 5:00
หากต้องการเริ่มต้นใช้งาน เราต้องสร้างโปรเจ็กต์ใหม่ใน Android Studio โดยทำดังนี้
- เปิด Android Studio
- คลิกโปรเจ็กต์ใหม่
- เลือกโทรศัพท์และแท็บเล็ตและกิจกรรมเปล่า
- คลิกถัดไป
- ตอนนี้ถึงเวลาตั้งค่าโปรเจ็กต์บางส่วนแล้ว
- ชื่อ: นี่คือชื่อโปรเจ็กต์ของคุณ
- ชื่อแพ็กเกจ: ระบบจะเติมข้อมูลนี้โดยอัตโนมัติตามชื่อโปรเจ็กต์
- บันทึกตำแหน่ง: ค่าเริ่มต้นควรเป็นโฟลเดอร์ที่ Android Studio บันทึกโปรเจ็กต์ของคุณ คุณเปลี่ยนการตั้งค่านี้เป็นที่ใดก็ได้ตามต้องการ
- SDK ขั้นต่ำ: นี่คือ Android SDK เวอร์ชันต่ำสุดที่แอปของคุณสร้างขึ้นเพื่อเรียกใช้ ใน Codelab นี้ เราจะใช้ API 36 (Baklava)
- คลิกเสร็จสิ้น
- Android Studio จะสร้างโปรเจ็กต์และดาวน์โหลดการอ้างอิงที่จำเป็นสำหรับแอปพลิเคชันพื้นฐาน ซึ่งอาจใช้เวลาหลายนาที หากต้องการดูการดำเนินการนี้ ให้คลิกไอคอนสร้าง
- เมื่อดำเนินการเสร็จแล้ว Android Studio ควรมีลักษณะดังนี้
3. ตั้งค่าโปรเจ็กต์ Google Cloud
สร้างโปรเจ็กต์ Google Cloud
- ไปที่ Google Cloud Console
- เปิดโปรเจ็กต์หรือสร้างโปรเจ็กต์ใหม่
- คลิก API และบริการ
- ไปที่หน้าจอขอความยินยอม OAuth
- คุณจะต้องกรอกข้อมูลในช่องในภาพรวมเพื่อดำเนินการต่อ คลิกเริ่มต้นใช้งานเพื่อเริ่มกรอกข้อมูลต่อไปนี้
- ชื่อแอป: ชื่อของแอปนี้ ซึ่งควรเป็นชื่อเดียวกับที่คุณใช้เมื่อสร้างโปรเจ็กต์ใน Android Studio
- อีเมลฝ่ายสนับสนุนผู้ใช้: อีเมลนี้จะแสดงบัญชี Google ที่คุณใช้เข้าสู่ระบบและกลุ่ม Google ที่คุณจัดการ
- ผู้ชม:
- ภายในสำหรับแอปที่ใช้ภายในองค์กรเท่านั้น หากไม่มีองค์กรที่เชื่อมโยงกับโปรเจ็กต์ Google Cloud คุณจะเลือกตัวเลือกนี้ไม่ได้
- เราจะใช้ "ภายนอก"
- ข้อมูลติดต่อ: อีเมลนี้จะเป็นอีเมลใดก็ได้ที่คุณต้องการให้เป็นอีเมลติดต่อสำหรับแอปพลิเคชัน
- อ่านบริการ Google API: นโยบายข้อมูลผู้ใช้
- เมื่ออ่านและยอมรับนโยบายข้อมูลผู้ใช้แล้ว ให้คลิกสร้าง
ตั้งค่าไคลเอ็นต์ OAuth
เมื่อตั้งค่าโปรเจ็กต์ Google Cloud แล้ว เราต้องเพิ่มไคลเอ็นต์ของเว็บและไคลเอ็นต์ Android เพื่อให้เรียก API ไปยังเซิร์ฟเวอร์แบ็กเอนด์ OAuth ด้วยรหัสไคลเอ็นต์ได้
สำหรับเว็บไคลเอ็นต์ Android คุณจะต้องมีสิ่งต่อไปนี้
- ชื่อแพ็กเกจของแอป (เช่น com.example.example)
- ลายเซ็น SHA-1 ของแอป
- ลายเซ็น SHA-1 คืออะไร
- ลายนิ้วมือ SHA-1 คือแฮชการเข้ารหัสที่สร้างจากคีย์การลงนามของแอป โดยทำหน้าที่เป็นตัวระบุที่ไม่ซ้ำกันสำหรับใบรับรองการลงนามของแอปที่เฉพาะเจาะจง ซึ่งเปรียบเสมือน "ลายเซ็น" ดิจิทัลสำหรับแอป
- ทำไมเราจึงต้องใช้ลายเซ็น SHA-1
- ลายนิ้วมือ SHA-1 ช่วยให้มั่นใจได้ว่ามีเพียงแอปของคุณที่ลงนามด้วยคีย์การลงนามที่เฉพาะเจาะจงเท่านั้นที่ขอโทเค็นเพื่อการเข้าถึงได้โดยใช้รหัสไคลเอ็นต์ OAuth 2.0 ซึ่งจะป้องกันไม่ให้แอปอื่นๆ (แม้แต่แอปที่มีชื่อแพ็กเกจเดียวกัน) เข้าถึงทรัพยากรและข้อมูลผู้ใช้ของโปรเจ็กต์
- ลองคิดแบบนี้
- คีย์ Signing ของแอปก็เหมือนกุญแจจริงที่ใช้เปิด "ประตู" ของแอป ซึ่งเป็นสิ่งที่อนุญาตให้เข้าถึงการทำงานภายในของแอป
- ลายนิ้วมือ SHA-1 เปรียบเสมือนรหัสคีย์การ์ดที่ไม่ซ้ำกันซึ่งลิงก์กับกุญแจจริง โดยเป็นรหัสเฉพาะที่ระบุคีย์นั้นๆ
- รหัสไคลเอ็นต์ OAuth 2.0 เปรียบเสมือนรหัสเข้าถึงทรัพยากรหรือบริการของ Google ที่เฉพาะเจาะจง (เช่น Google Sign-In)
- เมื่อระบลายนิ้วมือ SHA-1 ระหว่างการตั้งค่าไคลเอ็นต์ OAuth คุณกำลังบอก Google ว่า "เฉพาะคีย์การ์ดที่มีรหัสเฉพาะนี้ (SHA-1) เท่านั้นที่เปิดรหัสการเข้าถึงนี้ (รหัสไคลเอ็นต์) ได้" ซึ่งจะช่วยให้มั่นใจได้ว่ามีเพียงแอปของคุณเท่านั้นที่เข้าถึงบริการของ Google ที่ลิงก์กับรหัสรายการนั้นได้"
- ลายเซ็น SHA-1 คืออะไร
สำหรับไคลเอ็นต์บนเว็บ สิ่งที่เราต้องการคือชื่อที่คุณต้องการใช้เพื่อระบุไคลเอ็นต์ในคอนโซล
สร้างไคลเอ็นต์ OAuth 2.0 สำหรับ Android
- ไปที่หน้าไคลเอ็นต์
- คลิกสร้างไคลเอ็นต์
- เลือก Android สำหรับประเภทแอปพลิเคชัน
- คุณจะต้องระบุชื่อแพ็กเกจของแอป
- จาก Android Studio เราจะต้องรับลายเซ็น SHA-1 ของแอปและคัดลอก/วางลงในที่นี้
- ไปที่ Android Studio แล้วเปิดเทอร์มินัล
- เรียกใช้คำสั่งนี้: Mac/Linux:
Windows:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
คำสั่งนี้ออกแบบมาเพื่อแสดงรายละเอียดของรายการ (นามแฝง) ที่เฉพาะเจาะจงภายในที่เก็บคีย์keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
-list
: ตัวเลือกนี้จะบอกให้ Keytool แสดงเนื้อหาของ Keystore-v
: ตัวเลือกนี้จะเปิดใช้เอาต์พุตแบบละเอียด ซึ่งจะให้ข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับรายการ-keystore ~/.android/debug.keystore
: ระบุเส้นทางไปยังไฟล์ที่เก็บคีย์-alias androiddebugkey
: ระบุชื่อแทน (ชื่อรายการ) ของคีย์ที่ต้องการตรวจสอบ-storepass android
: ระบุรหัสผ่านสำหรับไฟล์ที่เก็บคีย์-keypass android
: ระบุรหัสผ่านสำหรับคีย์ส่วนตัวของนามแฝงที่ระบุ
- คัดลอกค่าของลายเซ็น SHA-1
- กลับไปที่หน้าต่าง Google Cloud แล้ววางค่าลายเซ็น SHA-1
- ตอนนี้หน้าจอของคุณควรมีลักษณะคล้ายกับภาพนี้ และคุณสามารถคลิกสร้างได้
สร้างไคลเอ็นต์ OAuth 2.0 สำหรับเว็บ
- หากต้องการสร้างรหัสไคลเอ็นต์ของเว็บแอปพลิเคชัน ให้ทำซ้ำขั้นตอนที่ 1-2 จากส่วนสร้างไคลเอ็นต์ Android แล้วเลือกเว็บแอปพลิเคชันสำหรับประเภทแอปพลิเคชัน
- ตั้งชื่อไคลเอ็นต์ (ซึ่งจะเป็นไคลเอ็นต์ OAuth)
- คลิกสร้าง
- คัดลอกรหัสไคลเอ็นต์จากหน้าต่างป๊อปอัป คุณจะต้องใช้รหัสนี้ในภายหลัง
ตอนนี้เราตั้งค่าไคลเอ็นต์ OAuth เรียบร้อยแล้ว เราสามารถกลับไปที่ Android Studio เพื่อสร้างแอป Android ที่ลงชื่อเข้าใช้ด้วย Google ได้
4. ตั้งค่าอุปกรณ์เสมือน Android
หากต้องการทดสอบแอปพลิเคชันอย่างรวดเร็วโดยไม่ต้องใช้อุปกรณ์ Android จริง คุณจะต้องสร้างอุปกรณ์ Android เสมือนที่สามารถสร้างและเรียกใช้แอปได้ทันทีจาก Android Studio หากต้องการทดสอบด้วยอุปกรณ์ Android จริง คุณสามารถทำตามวิธีการจากเอกสารสำหรับนักพัฒนาแอป Android
สร้างอุปกรณ์ Android เสมือน
- เปิด Device Manager
ใน Android Studio
- คลิกปุ่ม + > สร้างอุปกรณ์เสมือน
- คุณเพิ่มอุปกรณ์ที่จำเป็นสำหรับโปรเจ็กต์ได้จากตรงนี้ สำหรับ Codelab นี้ ให้เลือกโทรศัพท์ขนาดกลาง แล้วคลิกถัดไป
- ตอนนี้คุณสามารถกำหนดค่าอุปกรณ์สำหรับโปรเจ็กต์ได้โดยตั้งชื่อที่ไม่ซ้ำกัน เลือกเวอร์ชันของ Android ที่อุปกรณ์จะใช้ และอื่นๆ ตรวจสอบว่าได้ตั้งค่า API เป็น API 36 "Baklava"; Android 16 แล้วคลิกเสร็จสิ้น
- คุณควรเห็นอุปกรณ์ใหม่ปรากฏในตัวจัดการอุปกรณ์ หากต้องการยืนยันว่าอุปกรณ์ทำงาน ให้คลิก
ข้างอุปกรณ์ที่เพิ่งสร้าง
- ตอนนี้อุปกรณ์ควรจะทำงานแล้ว
ลงชื่อเข้าใช้ Android Virtual Device
อุปกรณ์ที่คุณเพิ่งสร้างใช้งานได้แล้ว ตอนนี้เราจะต้องลงชื่อเข้าใช้อุปกรณ์ด้วยบัญชี Google เพื่อป้องกันข้อผิดพลาดเมื่อทดสอบการลงชื่อเข้าใช้ด้วย Google
- ไปที่การตั้งค่า
- คลิกตรงกลางหน้าจอในอุปกรณ์เสมือน แล้วปัดขึ้น
- มองหาแอปการตั้งค่าแล้วคลิก
- คลิก Google ในการตั้งค่า
- คลิกลงชื่อเข้าใช้ แล้วทำตามข้อความแจ้งเพื่อลงชื่อเข้าใช้บัญชี Google
- ตอนนี้คุณควรลงชื่อเข้าใช้ในอุปกรณ์แล้ว
ตอนนี้อุปกรณ์ Android เสมือนพร้อมสำหรับการทดสอบแล้ว
5. เพิ่มทรัพยากร Dependency
ระยะเวลา 5:00
หากต้องการเรียกใช้ OAuth API เราต้องผสานรวมไลบรารีที่จำเป็นก่อน ซึ่งจะช่วยให้เราส่งคำขอการตรวจสอบสิทธิ์และใช้รหัส Google เพื่อส่งคำขอเหล่านั้นได้
- libs.googleid
- libs.play.services.auth
- ไปที่ File > Project Structure:
- จากนั้นไปที่การขึ้นต่อกัน > app > '+' > การขึ้นต่อกันของไลบรารี
- ตอนนี้เราต้องเพิ่มไลบรารี
- ในกล่องโต้ตอบการค้นหา ให้พิมพ์ googleid แล้วคลิกค้นหา
- ควรมีเพียง 1 รายการ ให้เลือกรายการนั้นและเวอร์ชันสูงสุดที่พร้อมใช้งาน (ณ เวลาที่ Codelab นี้คือ 1.1.1)
- คลิกตกลง
- ทำขั้นตอนที่ 1-3 ซ้ำ แต่ค้นหา "play-services-auth" แทน แล้วเลือกบรรทัดที่มี "com.google.android.gms" เป็นรหัสกลุ่ม และ "play-services-auth" เป็นชื่ออาร์ติแฟกต์
- คลิกตกลง
6. ขั้นตอนการทำงานของ Bottom Sheet
โฟลว์ชีตด้านล่างใช้ Credential Manager API เพื่อให้ผู้ใช้ลงชื่อเข้าใช้แอปโดยใช้บัญชี Google ใน Android ได้อย่างราบรื่น โดยออกแบบมาให้รวดเร็วและสะดวกสบาย โดยเฉพาะสำหรับผู้ใช้ที่กลับมา ควรทริกเกอร์โฟลว์นี้เมื่อเปิดแอป
สร้างคำขอลงชื่อเข้าใช้
- หากต้องการเริ่มต้นใช้งาน ให้นำฟังก์ชัน
Greeting()
และGreetingPreview()
ออกจากMainActivity.kt
เราไม่จำเป็นต้องใช้ฟังก์ชันดังกล่าว - ตอนนี้เราต้องตรวจสอบว่าได้นำเข้าแพ็กเกจที่ต้องการสำหรับโปรเจ็กต์นี้แล้ว โปรดเพิ่มคำสั่ง
import
ต่อไปนี้หลังคำสั่งที่มีอยู่โดยเริ่มที่บรรทัดที่ 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
- จากนั้นเราต้องสร้างฟังก์ชันเพื่อสร้างคำขอ Bottom Sheet วางโค้ดนี้ไว้ใต้คลาส 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)
}
มาดูรายละเอียดการทำงานของโค้ดนี้กัน
fun BottomSheet(webClientId: String) {...}
: สร้างฟังก์ชันชื่อ BottomSheet ที่รับอาร์กิวเมนต์สตริง 1 รายการชื่อ webClientid
val context = LocalContext.current
: เรียกข้อมูลบริบท Android ปัจจุบัน ซึ่งจำเป็นสำหรับการดำเนินการต่างๆ รวมถึงการเปิดใช้คอมโพเนนต์ UILaunchedEffect(Unit) { ... }
:LaunchedEffect
เป็นฟังก์ชันที่ประกอบกันได้ของ Jetpack Compose ซึ่งช่วยให้คุณเรียกใช้ฟังก์ชันระงับ (ฟังก์ชันที่หยุดชั่วคราวและดำเนินการต่อได้) ภายในวงจรของฟังก์ชันที่ประกอบกันได้ Unit เป็นคีย์หมายความว่าเอฟเฟ็กต์นี้จะทํางานเพียงครั้งเดียวเมื่อเปิดใช้ Composable เป็นครั้งแรกval googleIdOption: GetGoogleIdOption = ...
: สร้างออบเจ็กต์GetGoogleIdOption
ออบเจ็กต์นี้กำหนดค่าประเภทของข้อมูลเข้าสู่ระบบที่ขอจาก Google.Builder()
: ใช้รูปแบบ Builder เพื่อกำหนดค่าตัวเลือก.setFilterByAuthorizedAccounts(true)
: ระบุว่าจะอนุญาตให้ผู้ใช้เลือกจากบัญชี Google ทั้งหมด หรือเฉพาะบัญชีที่ให้สิทธิ์แอปแล้ว ในกรณีนี้ ค่าเป็นจริง ซึ่งหมายความว่าระบบจะส่งคำขอโดยใช้ข้อมูลเข้าสู่ระบบที่ผู้ใช้เคยให้สิทธิ์เพื่อใช้กับแอปนี้ หากมี.setServerClientId(webClientId)
: ตั้งค่ารหัสไคลเอ็นต์ของเซิร์ฟเวอร์ ซึ่งเป็นตัวระบุที่ไม่ซ้ำกันสำหรับแบ็กเอนด์ของแอป คุณต้องระบุข้อมูลนี้เพื่อรับโทเค็นรหัส.setNonce(generateSecureRandomNonce())
: ตั้งค่า Nonce ซึ่งเป็นค่าแบบสุ่มเพื่อป้องกันการโจมตีแบบเล่นซ้ำและตรวจสอบว่าโทเค็นรหัสเชื่อมโยงกับคำขอที่เฉพาะเจาะจง.build()
: สร้างออบเจ็กต์GetGoogleIdOption
ด้วยการกำหนดค่าที่ระบุ
val request: GetCredentialRequest = ...
: สร้างออบเจ็กต์GetCredentialRequest
ออบเจ็กต์นี้ห่อหุ้มคำขอข้อมูลเข้าสู่ระบบทั้งหมด.Builder()
: เริ่มรูปแบบตัวสร้างเพื่อกำหนดค่าคำขอ.addCredentialOption(googleIdOption)
: เพิ่ม googleIdOption ลงในคำขอ โดยระบุว่าเราต้องการขอโทเค็นรหัส Google.build()
: สร้างออบเจ็กต์GetCredentialRequest
val e = signIn(request, context)
: การดำเนินการนี้จะพยายามลงชื่อเข้าใช้ของผู้ใช้ด้วยคำขอที่สร้างขึ้นและบริบทปัจจุบัน ระบบจะจัดเก็บผลลัพธ์ของฟังก์ชัน signIn ไว้ใน e ตัวแปรนี้จะมีผลลัพธ์ที่สำเร็จหรือข้อยกเว้นif (e is NoCredentialException) { ... }
: นี่เป็นการตรวจสอบแบบมีเงื่อนไข หากฟังก์ชัน signIn ล้มเหลวด้วย NoCredentialException แสดงว่าไม่มีบัญชีที่ได้รับอนุญาตก่อนหน้านี้val googleIdOptionFalse: GetGoogleIdOption = ...
: หากsignIn
ก่อนหน้าล้มเหลว ส่วนนี้จะสร้างGetGoogleIdOption
ใหม่.setFilterByAuthorizedAccounts(false)
: นี่คือความแตกต่างที่สำคัญจากตัวเลือกแรก ซึ่งจะปิดใช้การกรองบัญชีที่ได้รับอนุญาต ซึ่งหมายความว่าสามารถใช้บัญชี Google ใดก็ได้ในอุปกรณ์เพื่อลงชื่อเข้าใช้val requestFalse: GetCredentialRequest = ...
: ระบบจะสร้างGetCredentialRequest
ใหม่ด้วยgoogleIdOptionFalse
signIn(requestFalse, context)
: การดำเนินการนี้พยายามลงชื่อเข้าใช้ผู้ใช้ด้วยคำขอใหม่ที่อนุญาตให้ใช้บัญชีใดก็ได้
โดยพื้นฐานแล้ว โค้ดนี้จะเตรียมคำขอไปยัง Credential Manager API เพื่อดึงโทเค็นรหัส Google สำหรับผู้ใช้โดยใช้การกำหนดค่าที่ระบุ จากนั้นจะใช้ GetCredentialRequest เพื่อเปิด UI ของเครื่องมือจัดการข้อมูลเข้าสู่ระบบได้ ซึ่งผู้ใช้จะเลือกบัญชี Google และให้สิทธิ์ที่จำเป็นได้
fun generateSecureRandomNonce(byteLength: Int = 32): String
: ซึ่งจะกำหนดฟังก์ชันชื่อ generateSecureRandomNonce
โดยจะยอมรับอาร์กิวเมนต์จำนวนเต็ม byteLength (ค่าเริ่มต้นคือ 32) ที่ระบุความยาวที่ต้องการของ Nonce ในหน่วยไบต์ โดยจะแสดงผลสตริงซึ่งจะเป็นการแสดงไบต์แบบสุ่มที่เข้ารหัส Base64
val randomBytes = ByteArray(byteLength)
: สร้างอาร์เรย์ไบต์ที่มี byteLength ที่ระบุเพื่อเก็บไบต์แบบสุ่มSecureRandom.getInstanceStrong().nextBytes(randomBytes)
:SecureRandom.getInstanceStrong()
: ซึ่งจะได้รับเครื่องมือสร้างตัวเลขสุ่มที่เข้ารหัสอย่างแน่นหนา ซึ่งเป็นสิ่งสำคัญอย่างยิ่งต่อความปลอดภัย เนื่องจากช่วยให้มั่นใจได้ว่าตัวเลขที่สร้างขึ้นนั้นสุ่มอย่างแท้จริงและคาดเดาไม่ได้ โดยจะใช้แหล่งที่มาของเอนโทรปีที่แข็งแกร่งที่สุดในระบบ.nextBytes(randomBytes)
: This populates the randomBytes array with random bytes generated by the SecureRandom instance.
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
:Base64.getUrlEncoder()
: รับตัวเข้ารหัส Base64 ที่ใช้ตัวอักษรที่ปลอดภัยสำหรับ URL (ใช้ - และ _ แทน + และ /) ซึ่งมีความสำคัญเนื่องจากช่วยให้มั่นใจได้ว่าสตริงที่ได้สามารถใช้ใน URL ได้อย่างปลอดภัยโดยไม่ต้องเข้ารหัสเพิ่มเติม.withoutPadding()
: การดำเนินการนี้จะนำอักขระการเว้นวรรคออกจากสตริงที่เข้ารหัส Base64 ซึ่งมักเป็นที่ต้องการเพื่อทำให้ Nonce สั้นลงและกะทัดรัดขึ้นเล็กน้อย.encodeToString(randomBytes)
: ซึ่งจะเข้ารหัส randomBytes เป็นสตริง Base64 และส่งคืน
โดยสรุป ฟังก์ชันนี้จะสร้าง Nonce แบบสุ่มที่มีการเข้ารหัสที่รัดกุมซึ่งมีความยาวที่ระบุ เข้ารหัสโดยใช้ Base64 ที่ปลอดภัยสำหรับ URL และแสดงผลสตริงที่ได้ นี่เป็นแนวทางปฏิบัติมาตรฐานสำหรับการสร้าง Nonce ที่ปลอดภัยต่อการใช้งานในบริบทที่ละเอียดอ่อนด้านความปลอดภัย
ส่งคำขอลงชื่อเข้าใช้
ตอนนี้เราสร้างคำขอลงชื่อเข้าใช้ได้แล้ว จึงใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบเพื่อใช้ในการลงชื่อเข้าใช้ได้ ในการดำเนินการนี้ เราต้องสร้างฟังก์ชันที่จัดการการส่งคำขอลงชื่อเข้าใช้โดยใช้เครื่องมือจัดการข้อมูลเข้าสู่ระบบ ขณะเดียวกันก็จัดการข้อยกเว้นทั่วไปที่เราอาจพบ
คุณวางฟังก์ชันนี้ไว้ใต้ฟังก์ชัน BottomSheet()
เพื่อให้ดำเนินการนี้สำเร็จได้
//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
}
ต่อไปเราจะมาดูรายละเอียดว่าโค้ดนี้ทำอะไร
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?
: ซึ่งกำหนดฟังก์ชันระงับชื่อ signIn ซึ่งหมายความว่าสามารถหยุดชั่วคราวและกลับมาทำงานต่อได้โดยไม่บล็อกเทรดหลัก โดยจะแสดงผล Exception?
ซึ่งจะเป็นค่าว่างหากลงชื่อเข้าใช้สำเร็จ หรือข้อยกเว้นที่เฉพาะเจาะจงหากลงชื่อเข้าใช้ไม่สำเร็จ
โดยมีพารามิเตอร์ 2 ตัวดังนี้
request
: ออบเจ็กต์GetCredentialRequest
ซึ่งมีการกำหนดค่าสำหรับประเภทของข้อมูลเข้าสู่ระบบที่จะดึงข้อมูล (เช่น รหัส Google)context
: บริบท Android ที่จำเป็นต่อการโต้ตอบกับระบบ
สำหรับเนื้อหาของฟังก์ชัน ให้ทำดังนี้
val credentialManager = CredentialManager.create(context)
: สร้างอินสแตนซ์ของ CredentialManager ซึ่งเป็นอินเทอร์เฟซหลักสําหรับการโต้ตอบกับ Credential Manager API แอปจะเริ่มขั้นตอนการลงชื่อเข้าใช้ด้วยวิธีนี้val failureMessage = "Sign in failed!"
: กำหนดสตริง (failureMessage) ที่จะแสดงในข้อความป๊อปอัปเมื่อลงชื่อเข้าใช้ไม่สำเร็จvar e: Exception? = null
: บรรทัดนี้จะเริ่มต้นตัวแปร e เพื่อจัดเก็บข้อยกเว้นที่อาจเกิดขึ้นในระหว่างกระบวนการ โดยเริ่มต้นด้วยค่า nulldelay(250)
: เพิ่มความล่าช้า 250 มิลลิวินาที นี่คือวิธีแก้ปัญหาที่อาจเกิดขึ้นซึ่ง NoCredentialException อาจเกิดขึ้นทันทีเมื่อแอปเริ่มต้น โดยเฉพาะเมื่อใช้โฟลว์ BottomSheet ซึ่งจะช่วยให้ระบบมีเวลาเริ่มต้นเครื่องมือจัดการข้อมูลเข้าสู่ระบบtry { ... } catch (e: Exception) { ... }
:ใช้บล็อก try-catch เพื่อการจัดการข้อผิดพลาดที่มีประสิทธิภาพ ซึ่งจะช่วยให้มั่นใจได้ว่าหากเกิดข้อผิดพลาดในกระบวนการลงชื่อเข้าใช้ แอปจะไม่ขัดข้องและจะจัดการข้อยกเว้นได้อย่างราบรื่นval result = credentialManager.getCredential(request = request, context = context)
: ส่วนนี้คือที่ที่การเรียก Credential Manager API เกิดขึ้นจริงและเริ่มกระบวนการดึงข้อมูลเข้าสู่ระบบ โดยจะรับคำขอและบริบทเป็นข้อมูลนำเข้า และจะแสดง UI ให้ผู้ใช้เลือกข้อมูลเข้าสู่ระบบ หากสำเร็จ ระบบจะแสดงผลลัพธ์ที่มีข้อมูลเข้าสู่ระบบที่เลือก ผลลัพธ์ของการดำเนินการนี้GetCredentialResponse
จะจัดเก็บไว้ในตัวแปรresult
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
:แสดงข้อความโทสต์สั้นๆ ที่ระบุว่าการลงชื่อเข้าใช้สำเร็จLog.i(TAG, "Sign in Successful!")
: บันทึกข้อความที่สนุกและสำเร็จไปยัง Logcatcatch (e: GetCredentialException)
: จัดการข้อยกเว้นประเภทGetCredentialException
นี่คือคลาสหลักสำหรับข้อยกเว้นที่เฉพาะเจาะจงหลายรายการที่อาจเกิดขึ้นในระหว่างกระบวนการดึงข้อมูลข้อมูลเข้าสู่ระบบcatch (e: GoogleIdTokenParsingException)
: จัดการข้อยกเว้นที่เกิดขึ้นเมื่อมีข้อผิดพลาดในการแยกวิเคราะห์โทเค็นรหัส Googlecatch (e: NoCredentialException)
: จัดการNoCredentialException
ซึ่งจะเกิดขึ้นเมื่อไม่มีข้อมูลเข้าสู่ระบบสำหรับผู้ใช้ (เช่น ผู้ใช้ไม่ได้บันทึกข้อมูลเข้าสู่ระบบ หรือไม่มีบัญชี Google)- ที่สำคัญ ฟังก์ชันนี้จะแสดงผลข้อยกเว้นที่จัดเก็บไว้ใน
e
NoCredentialException
ซึ่งช่วยให้ผู้เรียกจัดการกรณีเฉพาะได้หากไม่มีข้อมูลเข้าสู่ระบบ
- ที่สำคัญ ฟังก์ชันนี้จะแสดงผลข้อยกเว้นที่จัดเก็บไว้ใน
catch (e: GetCredentialCustomException)
: จัดการข้อยกเว้นที่กำหนดเองซึ่งอาจเกิดจากผู้ให้บริการข้อมูลเข้าสู่ระบบcatch (e: GetCredentialCancellationException)
: จัดการGetCredentialCancellationException
ซึ่งจะแสดงเมื่อผู้ใช้ยกเลิกกระบวนการลงชื่อเข้าใช้Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
: แสดงข้อความโทสต์ที่ระบุว่าลงชื่อเข้าใช้ไม่สำเร็จโดยใช้ failureMessageLog.e(TAG, "", e)
: บันทึกข้อยกเว้นไปยัง logcat ของ Android โดยใช้ Log.e ซึ่งใช้สำหรับข้อผิดพลาด ซึ่งจะรวมถึง Stacktrace ของข้อยกเว้นเพื่อช่วยในการแก้ไขข้อบกพร่อง นอกจากนี้ ยังมีอีโมติคอนโกรธเพื่อความสนุกสนานด้วย
return e
: ฟังก์ชันจะแสดงผลข้อยกเว้นหากมีการจับข้อยกเว้น หรือแสดงผล Null หากลงชื่อเข้าใช้สำเร็จ
โดยสรุปแล้ว โค้ดนี้มีวิธีจัดการการลงชื่อเข้าใช้ของผู้ใช้โดยใช้ Credential Manager API, จัดการการดำเนินการแบบไม่พร้อมกัน, จัดการข้อผิดพลาดที่อาจเกิดขึ้น และให้ความคิดเห็นแก่ผู้ใช้ผ่านข้อความแจ้งและบันทึก พร้อมทั้งเพิ่มความขำขันเล็กน้อยในการจัดการข้อผิดพลาด
ใช้โฟลว์ Bottom Sheet ในแอป
ตอนนี้เราสามารถตั้งค่าการเรียกเพื่อทริกเกอร์โฟลว์ BottomSheet ในคลาส MainActivity
ได้โดยใช้โค้ดต่อไปนี้และรหัสไคลเอ็นต์ของเว็บแอปพลิเคชันที่เราคัดลอกจาก Google Cloud Console ก่อนหน้านี้
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)
}
}
}
}
}
ตอนนี้เราสามารถบันทึกโปรเจ็กต์ (ไฟล์ > บันทึก) และเรียกใช้ได้แล้ว
- กดปุ่มเรียกใช้:
- เมื่อแอปทำงานในโปรแกรมจำลองแล้ว คุณควรเห็น BottomSheet การลงชื่อเข้าใช้ปรากฏขึ้น คลิกต่อไปเพื่อทดสอบการลงชื่อเข้าใช้
- คุณควรเห็นข้อความ Toast ที่แสดงว่าการลงชื่อเข้าใช้สำเร็จแล้ว
7. ขั้นตอนการทำงานของปุ่ม
ขั้นตอนปุ่มสำหรับฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ช่วยให้ผู้ใช้ลงชื่อสมัครใช้หรือเข้าสู่ระบบแอป Android ได้ง่ายขึ้นโดยใช้บัญชี Google ที่มีอยู่ ผู้ใช้จะเห็นปุ่มนี้หากปิดชีตด้านล่างหรือต้องการใช้บัญชี Google ของตนเองอย่างชัดเจนเพื่อลงชื่อเข้าใช้หรือลงชื่อสมัครใช้ สำหรับนักพัฒนาแอป การเปลี่ยนแปลงนี้หมายถึงการเริ่มต้นใช้งานที่ราบรื่นยิ่งขึ้นและลดอุปสรรคระหว่างการลงชื่อสมัครใช้
แม้ว่าคุณจะใช้ปุ่ม Jetpack Compose ที่พร้อมใช้งานได้ แต่เราจะใช้ไอคอนแบรนด์ที่ได้รับอนุมัติล่วงหน้าจากหน้าหลักเกณฑ์การสร้างแบรนด์ของฟีเจอร์ลงชื่อเข้าใช้ด้วย Google
เพิ่มไอคอนแบรนด์ลงในโปรเจ็กต์
- ดาวน์โหลดไฟล์ ZIP ของไอคอนแบรนด์ที่ได้รับอนุมัติล่วงหน้าที่นี่
- คลายการบีบอัด signin-assest.zip จากโฟลเดอร์ดาวน์โหลด (ขั้นตอนนี้จะแตกต่างกันไปตามระบบปฏิบัติการของคอมพิวเตอร์) ตอนนี้คุณสามารถเปิดโฟลเดอร์ signin-assets และดูไอคอนที่มีได้แล้ว สำหรับ Codelab นี้ เราจะใช้
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
- คัดลอกไฟล์
- วางลงในโปรเจ็กต์ใน Android Studio ในส่วน res > drawable โดยคลิกขวาที่โฟลเดอร์ drawable แล้วคลิกวาง (คุณอาจต้องขยายโฟลเดอร์ res เพื่อดู)
- กล่องโต้ตอบจะปรากฏขึ้นเพื่อแจ้งให้คุณเปลี่ยนชื่อไฟล์และยืนยันไดเรกทอรีที่จะเพิ่มไฟล์ เปลี่ยนชื่อชิ้นงานเป็น siwg_button.png แล้วคลิกตกลง
โค้ดโฟลว์ของปุ่ม
โค้ดนี้จะใช้ฟังก์ชัน signIn()
เดียวกันกับที่ใช้สำหรับ BottomSheet()
แต่จะใช้ GetSignInWithGoogleOption
แทน GetGoogleIdOption
เนื่องจากโฟลว์นี้ไม่ได้ใช้ประโยชน์จากข้อมูลเข้าสู่ระบบและพาสคีย์ที่จัดเก็บไว้ในอุปกรณ์เพื่อแสดงตัวเลือกการลงชื่อเข้าใช้ ต่อไปนี้คือโค้ดที่คุณวางใต้ฟังก์ชัน 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)
)
}
โค้ดนี้ทำอะไรบ้าง
fun ButtonUI(webClientId: String)
: ประกาศฟังก์ชันชื่อ ButtonUI
ที่รับ webClientId
(รหัสไคลเอ็นต์ของโปรเจ็กต์ Google Cloud) เป็นอาร์กิวเมนต์
val context = LocalContext.current
: เรียกข้อมูลบริบท Android ปัจจุบัน ซึ่งจำเป็นสำหรับการดำเนินการต่างๆ รวมถึงการเปิดใช้คอมโพเนนต์ UI
val coroutineScope = rememberCoroutineScope()
: สร้างขอบเขตของโครูทีน ซึ่งใช้เพื่อจัดการงานแบบอะซิงโครนัส ทำให้โค้ดทำงานได้โดยไม่บล็อกเทรดหลัก rememberCoroutineScope
() เป็นฟังก์ชันที่ประกอบกันได้จาก Jetpack Compose ซึ่งจะให้ขอบเขตที่เชื่อมโยงกับวงจรของ Composable
val onClick: () -> Unit = { ... }
: ซึ่งจะสร้างฟังก์ชัน Lambda ที่จะดำเนินการเมื่อมีการคลิกปุ่ม ฟังก์ชัน Lambda จะทำสิ่งต่อไปนี้
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build()
: ส่วนนี้สร้างออบเจ็กต์GetSignInWithGoogleOption
ออบเจ็กต์นี้ใช้เพื่อระบุพารามิเตอร์สำหรับกระบวนการ "ลงชื่อเข้าใช้ด้วย Google" โดยต้องมีwebClientId
และ Nonce (สตริงแบบสุ่มที่ใช้เพื่อความปลอดภัย)val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
: ซึ่งจะสร้างออบเจ็กต์GetCredentialRequest
ระบบจะใช้คำขอนี้เพื่อรับข้อมูลเข้าสู่ระบบของผู้ใช้โดยใช้ตัวจัดการข้อมูลเข้าสู่ระบบGetCredentialRequest
จะเพิ่มGetSignInWithGoogleOption
ที่สร้างไว้ก่อนหน้านี้เป็นตัวเลือกเพื่อขอข้อมูลเข้าสู่ระบบ "ลงชื่อเข้าใช้ด้วย Google"
coroutineScope.launch { ... }
:CoroutineScope
สำหรับจัดการการดำเนินการแบบอะซิงโครนัส (ใช้โครูทีน)signIn(request, context)
: เรียกฟังก์ชันsignIn
() ที่เรากำหนดไว้ก่อนหน้านี้
Image(...)
: การดำเนินการนี้จะแสดงรูปภาพโดยใช้ painterResource
ที่โหลดรูปภาพ R.drawable.siwg_button
Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick)
:fillMaxSize()
: ทำให้รูปภาพเต็มพื้นที่ว่างclickable(enabled = true, onClick = onClick)
: ทำให้รูปภาพคลิกได้ และเมื่อคลิกแล้ว จะเรียกใช้ฟังก์ชัน Lambda onClick ที่กำหนดไว้ก่อนหน้านี้
โดยสรุปแล้ว โค้ดนี้จะตั้งค่าปุ่ม "ลงชื่อเข้าใช้ด้วย Google" ใน UI ของ Jetpack Compose เมื่อคลิกปุ่มนี้ ระบบจะเตรียมคำขอข้อมูลเข้าสู่ระบบเพื่อเปิด Credential Manager และอนุญาตให้ผู้ใช้ลงชื่อเข้าใช้ด้วยบัญชี Google
ตอนนี้เราต้องอัปเดตคลาส MainActivity เพื่อเรียกใช้ฟังก์ชัน 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)
}
}
}
}
}
}
ตอนนี้เราสามารถบันทึกโปรเจ็กต์ (ไฟล์ > บันทึก) และเรียกใช้ได้แล้ว
- กดปุ่มเรียกใช้:
- เมื่อแอปทำงานในโปรแกรมจำลองแล้ว BottomSheet ควรปรากฏขึ้น คลิกภายนอกเพื่อปิด
- ตอนนี้คุณควรเห็นปุ่มที่เราสร้างแสดงในแอปแล้ว ให้คลิกปุ่มดังกล่าวเพื่อดูกล่องโต้ตอบการลงชื่อเข้าใช้
- คลิกบัญชีเพื่อลงชื่อเข้าใช้
8. บทสรุป
คุณทำ Codelab นี้เสร็จแล้ว ดูข้อมูลเพิ่มเติมหรือความช่วยเหลือเกี่ยวกับการลงชื่อเข้าใช้ด้วย Google ใน Android ได้ที่ส่วนคำถามที่พบบ่อยด้านล่าง
คำถามที่พบบ่อย
- Stackoverflow
- คู่มือการแก้ปัญหาเครื่องมือจัดการข้อมูลเข้าสู่ระบบของ Android
- คำถามที่พบบ่อยเกี่ยวกับเครื่องมือจัดการข้อมูลเข้าสู่ระบบของ Android
- ศูนย์ช่วยเหลือการตรวจสอบแอปผ่าน OAuth
โค้ด MainActivity.kt แบบเต็ม
โค้ดแบบเต็มสำหรับ MainActivity.kt เพื่อใช้อ้างอิงมีดังนี้
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
}