跳至主要內容

為你的 Android (Kotlin/Java) 應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Android 應用程式中。

提示:

先決條件

  • 一個 Logto Cloud 帳戶或 自託管 Logto
  • 已建立的 Logto 原生應用程式。
  • 一個 Kotlin Android 應用程式專案。

安裝

備註:

Logto Android SDK 支援的最低 Android API 等級為 24。

在安裝 Logto Android SDK 之前,請確保在 Gradle 專案的建置檔案中將 mavenCentral() 添加到你的倉庫配置中:

settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}

將 Logto Android SDK 添加到你的相依項目中:

build.gradle.kts
dependencies {
implementation("io.logto.sdk:android:1.1.3")
}

由於 SDK 需要網路存取,你需要在 AndroidManifest.xml 檔案中添加以下權限:

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- 添加網路權限 -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- 其他配置... -->
</manifest>

整合

初始化 LogtoClient

建立一個 LogtoViewModel.kt 並在此 ViewModel 中初始化 LogtoClient

LogtoViewModel.kt
//...with other imports
import io.logto.sdk.android.LogtoClient
import io.logto.sdk.android.type.LogtoConfig

class LogtoViewModel(application: Application) : AndroidViewModel(application) {
private val logtoConfig = LogtoConfig(
endpoint = "<your-logto-endpoint>",
appId = "<your-app-id>",
scopes = null,
resources = null,
usingPersistStorage = true,
)

private val logtoClient = LogtoClient(logtoConfig, application)

companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// 從 extras 中獲取 Application 物件
val application = checkNotNull(extras[APPLICATION_KEY])
return LogtoViewModel(application) as T
}
}
}
}

接著,為你的 MainActivity.kt 建立一個 LogtoViewModel

MainActivity.kt
//...with other imports
class MainActivity : AppCompatActivity() {
private val logtoViewModel: LogtoViewModel by viewModels { LogtoViewModel.Factory }
//...其他程式碼
}

配置重定向 URI

在進入細節之前,這裡先快速說明一下終端使用者的體驗。登入流程可簡化如下:

Parse error on line 2: ...aph LR A(你的應用程式 (Your app)) -->|1\. ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重新導向至 Logto 登入頁面。對於原生應用程式,會開啟系統瀏覽器。
  3. 使用者登入後,會被重新導向回你的應用程式(設定為 redirect URI)。

關於基於重導的登入

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析


讓我們切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI io.logto.android://io.logto.sample/callback,然後點擊「儲存變更」。

Logto Console 中的重定向 URI

在 Android 中,重定向 URI 遵循以下模式:$(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback

  • LOGTO_REDIRECT_SCHEME 應為反向域格式的自定義方案。
  • YOUR_APP_PACKAGE 是你的應用程式包名。

假設你將 io.logto.android 作為自定義 LOGTO_REDIRECT_SCHEME,而 io.logto.sample 是你的應用程式包名,則重定向 URI 應為 io.logto.android://io.logto.sample/callback

實作登入與登出

備註:

在呼叫 logtoClient.signIn 之前,請確保已在管理控制台中正確配置了 Redirect URI。

你可以使用 logtoClient.signIn 來讓使用者登入,並使用 logtoClient.signOut 來讓使用者登出。

例如,在 Android 應用程式中:

LogtoModelView.kt
//...with other imports
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...other codes

// 新增一個 live data 來觀察驗證 (Authentication) 狀態
private val _authenticated = MutableLiveData(logtoClient.isAuthenticated)
val authenticated: LiveData<Boolean>
get() = _authenticated

fun signIn(context: Activity) {
logtoClient.signIn(context, "io.logto.android://io.logto.sample/callback") { logtoException ->
logtoException?.let { println(it) }
// 更新 live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}

fun signOut() {
logtoClient.signOut { logtoException ->
logtoException?.let { println(it) }
// 更新 live data
_authenticated.postValue(logtoClient.isAuthenticated)
}
}
}

然後在你的 activity 中呼叫 signInsignOut 方法:

MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...other codes

// 假設你的佈局中有一個 id 為 "sign_in_button" 的按鈕
val signInButton = findViewById<Button>(R.id.sign_in_button)
signInButton.setOnClickListener {
logtoViewModel.signIn(this)
}

// 假設你的佈局中有一個 id 為 "sign_out_button" 的按鈕
val signOutButton = findViewById<Button>(R.id.sign_out_button)
signOutButton.setOnClickListener {
if (logtoViewModel.authenticated) { // 檢查使用者是否已驗證 (Authenticated)
logtoViewModel.signOut()
}
}

// 觀察驗證 (Authentication) 狀態以更新 UI
logtoViewModel.authenticated.observe(this) { authenticated ->
if (authenticated) {
// 使用者已驗證 (Authenticated)
signInButton.visibility = View.GONE
signOutButton.visibility = View.VISIBLE
} else {
// 使用者未驗證 (Authenticated)
signInButton.visibility = View.VISIBLE
signOutButton.visibility = View.GONE
}
}

}
}

檢查點:測試你的應用程式

現在,你可以測試你的應用程式:

  1. 執行你的應用程式,你會看到登入按鈕。
  2. 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
  3. 登入後,你將被重定向回應用程式並看到登出按鈕。
  4. 點擊登出按鈕以清除權杖存儲並登出。

獲取使用者資訊

顯示使用者資訊

要顯示使用者的資訊,你可以使用 logtoClient.getIdTokenClaims() 方法。例如,你可以在 ViewModel 中獲取使用者資訊,然後在你的活動中顯示:

LogtoModelView.kt
class LogtoViewModel(application: Application) : AndroidViewModel(application) {
// ...其他代碼

// 添加一個 live data 來觀察 ID 權杖 (ID token) 宣告 (Claims)
private val _idTokenClaims = MutableLiveData<IdTokenClaims>()
val idTokenClaims: LiveData<IdTokenClaims>
get() = _idTokenClaims

fun getIdTokenClaims() {
logtoClient.getIdTokenClaims { logtoException, idTokenClaims ->
logtoException?.let { _logtoException.postValue(it) } ?: _idTokenClaims.postValue(idTokenClaims)
}
}
}
MainActivity.kt
//...與其他匯入
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...其他代碼

// 假設你的佈局中有一個 ID 為 `user_info_text_view` 的文字視圖
val userInfoResponseTextView: TextView = findViewById(R.id.user_info_text_view)
logtoViewModel.userInfoResponse.observe(this) { userInfoResponse ->
userInfoResponseTextView.text = if (userInfoResponse !== null) {
val json = Gson().toJson(userInfoResponse, UserInfoResponse::class.java)
JSONObject(json).toString(2)
} else {
""
}
}
}
}

請求額外的宣告 (Claims)

你可能會發現從 logtoClient.getIdTokenClaims() 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

openid

+sub

profile

+name

+username

+picture

+...

email

+email

+email_verified

phone

+phone_number

+phone_number_verified

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openidprofileoffline_access

要請求額外的權限範圍 (Scopes),你可以將權限範圍傳遞給 LogtoConfig 物件。例如:

LogtoViewModel.kt
private val logtoConfig = LogtoConfig(
// ...其他配置
scopes = listOf("email", "phone"), // 或 `listOf(UserScope.EMAIL, UserScope.PHONE)`
)

然後你可以在 logtoClient.getIdTokenClaims() 的返回值中訪問額外的宣告 (Claims):

logtoClient.getIdTokenClaims { logtoException, idTokenClaims ->
println("IdTokenClaims:$idTokenClaims")
}
// 現在你可以訪問額外的宣告 `claims.email`、`claims.phone` 等。

需要網路請求的宣告 (Claims)

為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 logtoClient.fetchUserInfo() 方法

LogtoViewModel.kt
logtoClient.fetchUserInfo {_, userInfoResponse ->
println("UserInfoResponse:$userInfoResponse")
}
// 現在你可以訪問宣告 `userInfo.custom_data`
此方法將透過請求 userinfo 端點來獲取使用者資訊。要了解更多可用的權限範圍 (Scopes) 和宣告 (Claims),請參閱 權限範圍 (Scopes) 和宣告 (Claims) 部分。

權限範圍 (Scopes) 與宣告 (Claims)

Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名
usernamestring使用者的用戶名
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。

其他 標準宣告 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null

備註:

與標準宣告不同,created_atupdated_at 宣告使用毫秒而非秒。

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址
email_verifiedboolean電子郵件地址是否已驗證

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼
phone_number_verifiedboolean電話號碼是否已驗證

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分
sso_identitiesarray使用者的連結 SSO 身分

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID
organization_dataobject[]使用者所屬的組織資料

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

API 資源與組織

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

配置 Logto 客戶端

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
//...other configs
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"), // 新增 API 資源 (API resources)
)

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:readshopping:write 權限,而 https://store.your-app.com/api 資源具有 store:readstore:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
// ..other configs
scopes = listOf("shopping:read", "shopping:write", "store:read", "store:write"),
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"),
)

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read write 權限範圍而不需要前綴。然後,在 Logto 配置中:

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf("read", "write"),
resources = listOf("https://shopping.your-app.com/api", "https://store.your-app.com/api"),
)

對於每個 API 資源,它將請求 readwrite 權限範圍。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

獲取 API 資源的存取權杖 (Access token)

要獲取特定 API 資源的存取權杖 (Access token),你可以使用 getAccessToken 方法:

LogtoViewModel.kt
logtoClient.getAccessToken("https://shopping.your-app.com/api") { logtoException, accessToken ->
logtoException?.let { println(it) }
accessToken?.let { println(it) }
}

此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。

獲取組織權杖 (Organization tokens)

如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。

在配置 Logto client 時,你需要新增 UserScope.Organizations 權限範圍 (scope):

LogtoViewModel.kt
val logtoConfig = LogtoConfig(
// ...other configs
scopes = listOf(UserScope.Organizations),
)

使用者登入後,你可以為使用者獲取組織權杖 (organization token):

LogtoViewModel.kt
// 將參數替換為有效的組織 ID。
// 使用者的有效組織 ID 可以在 ID 權杖 (ID token) 宣告 (claim) `organizations` 中找到。
logtoClient.getOrganizationToken("organization-id") { logtoException, organizationToken ->
logtoException?.let { println(it) }
organizationToken?.let { println(it) }
}

// 或
logtoClient.getOrganizationTokenClaims("organization-id") { logtoException, claims ->
logtoException?.let { println(it) }
claims?.let { println(it) }
}

組織 API 資源 (Organization API resources)

要為組織中的 API 資源取得存取權杖 (Access token),可以使用 getAccessToken 方法,並將 API 資源和組織 ID 作為參數:

LogtoViewModel.kt
logtoClient.getAccessToken(
'https://shopping.your-app.com/api',
organizationId
) { logtoException, accessToken ->
println("AccessToken:$accessToken")
}

進一步閱讀

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization)