Ktor Client 中的持有者驗證
所需依賴項:io.ktor:ktor-client-auth
程式碼範例: client-auth-oauth-google
持有者驗證涉及稱為持有者權杖的安全權杖。舉例來說,這些權杖可用作 OAuth 流程的一部分,透過 Google、Facebook、Twitter 等外部供應商授權您的應用程式使用者。您可以從 Ktor 伺服器的OAuth 授權流程部分了解 OAuth 流程的樣貌。
在伺服器端,Ktor 提供 Authentication 外掛程式來處理持有者驗證。
設定持有者驗證
Ktor 客戶端允許您設定要使用 Bearer
方案在 Authorization
標頭中傳送的權杖。您還可以指定當舊權杖無效時重新整理權杖的邏輯。要設定 bearer
提供者,請遵循以下步驟:
在
install
區塊內呼叫bearer
函式。kotlinimport io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.auth.* //... val client = HttpClient(CIO) { install(Auth) { bearer { // Configure bearer authentication } } }
使用
loadTokens
回呼設定如何取得初始的存取和重新整理權杖。此回呼旨在從本地儲存載入快取的權杖,並將它們作為BearerTokens
實例返回。kotlininstall(Auth) { bearer { loadTokens { // Load tokens from a local storage and return them as the 'BearerTokens' instance BearerTokens("abc123", "xyz111") } } }
abc123
存取權杖會使用Bearer
方案隨每個 請求在Authorization
標頭中傳送:HTTPGET http://localhost:8080/ Authorization: Bearer abc123
指定當舊權杖無效時如何使用
refreshTokens
取得新權杖。kotlininstall(Auth) { bearer { // Load tokens ... refreshTokens { // this: RefreshTokensParams // Refresh tokens and return them as the 'BearerTokens' instance BearerTokens("def456", "xyz111") } } }
此回呼的工作方式如下:
a. 客戶端使用無效的存取權杖向受保護的資源發出請求,並獲得
401
(Unauthorized) 回應。如果安裝了多個提供者,回應應包含
WWW-Authenticate
標頭。b. 客戶端自動呼叫
refreshTokens
以取得新權杖。c. 客戶端再次自動使用新權杖向受保護的資源發出請求。
(可選)指定在不等待
401
(Unauthorized) 回應的情況下傳送憑證的條件。例如,您可以檢查請求是否發送到指定的主機。kotlininstall(Auth) { bearer { // Load and refresh tokens ... sendWithoutRequest { request -> request.url.host == "www.googleapis.com" } } }
範例:使用持有者驗證存取 Google API
讓我們看看如何使用持有者驗證來存取 Google API,Google API 使用 OAuth 2.0 協定 進行驗證和授權。我們將研究 client-auth-oauth-google 主控台應用程式,該應用程式取得 Google 的個人資料資訊。
取得客戶端憑證
作為第一步,我們需要取得存取 Google API 所需的客戶端憑證:
- 建立 Google 帳戶。
- 開啟 Google Cloud Console 並建立應用程式類型為
Android
的OAuth client ID
憑證。此客戶端 ID 將用於取得授權許可。
OAuth 授權流程
我們應用程式的 OAuth 授權流程如下:
(1) --> 授權請求 資源擁有者
(2) <-- 授權許可 (碼) 資源擁有者
(3) --> 授權許可 (碼) 授權伺服器
(4) <-- 存取和重新整理權杖 授權伺服器
(5) --> 攜帶有效權杖的請求 資源伺服器
(6) <-- 受保護的資源 資源伺服器
⌛⌛⌛ 權杖過期
(7) --> 攜帶過期權杖的請求 資源伺服器
(8) <-- 401 未經授權回應 資源伺服器
(9) --> 授權許可 (重新整理權杖) 授權伺服器
(10) <-- 存取和重新整理權杖 授權伺服器
(11) --> 攜帶新權杖的請求 資源伺服器
(12) <-- 受保護的資源 資源伺服器
讓我們調查每個步驟的實現方式,以及 Bearer
驗證提供者如何幫助我們存取 API。
(1) -> 授權請求
作為第一步,我們需要建立用於請求所需權限的授權連結。為此,我們需要將指定的查詢參數附加到 URL:
val authorizationUrlQuery = parameters {
append("client_id", System.getenv("GOOGLE_CLIENT_ID"))
append("scope", "https://www.googleapis.com/auth/userinfo.profile")
append("response_type", "code")
append("redirect_uri", "http://127.0.0.1:8080")
append("access_type", "offline")
}.formUrlEncode()
println("https://accounts.google.com/o/oauth2/auth?$authorizationUrlQuery")
println("Open a link above, get the authorization code, insert it below, and press Enter.")
client_id
:先前取得的客戶端 ID 用於存取 Google API。scope
:Ktor 應用程式所需的資源範圍。在我們的案例中,應用程式請求使用者的個人資料資訊。response_type
:用於取得存取權杖的授予類型。在我們的案例中,我們需要取得授權碼。redirect_uri
:http://127.0.0.1:8080
值表示使用 回環 IP 位址流程 取得授權碼。要使用此 URL 接收授權碼,您的應用程式必須在本地網頁伺服器上監聽。 例如,您可以使用 Ktor 伺服器將授權碼作為查詢參數取得。
access_type
:存取類型設定為offline
,因為我們的主控台應用程式需要在使用者不在瀏覽器時重新整理存取權杖。
(2) <- 授權許可 (碼)
在此步驟中,我們從瀏覽器複製授權碼,將其貼到主控台,並將其儲存在變數中:
val authorizationCode = readln()
(3) -> 授權許可 (碼)
現在我們準備好將授權碼換取權杖。為此,我們需要建立一個客戶端並安裝 ContentNegotiation 外掛程式,並使用 json
序列化器。此序列化器是反序列化從 Google OAuth 權杖端點接收的權杖所必需的。
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
}
使用建立的客戶端,我們可以安全地將授權碼和其他必要的選項作為表單參數傳遞到權杖端點:
val tokenInfo: TokenInfo = client.submitForm(
url = "https://accounts.google.com/o/oauth2/token",
formParameters = parameters {
append("grant_type", "authorization_code")
append("code", authorizationCode)
append("client_id", System.getenv("GOOGLE_CLIENT_ID"))
append("client_secret", System.getenv("GOOGLE_CLIENT_SECRET"))
append("redirect_uri", "http://127.0.0.1:8080")
}
).body()
結果,權杖端點以 JSON 物件的形式傳送權杖,該 JSON 物件使用已安裝的 json
序列化器反序列化為 TokenInfo
類別實例。TokenInfo
類別如下所示:
import kotlinx.serialization.*
@Serializable
data class TokenInfo(
@SerialName("access_token") val accessToken: String,
@SerialName("expires_in") val expiresIn: Int,
@SerialName("refresh_token") val refreshToken: String? = null,
val scope: String,
@SerialName("token_type") val tokenType: String,
@SerialName("id_token") val idToken: String,
)
(4) <- 存取和重新整理權杖
接收到權杖後,我們可以將它們儲存在儲存區中。在我們的範例中,儲存區是 BearerTokens
實例的可變列表。這表示我們可以將其元素傳遞給 loadTokens
和 refreshTokens
回呼。
val bearerTokenStorage = mutableListOf<BearerTokens>()
bearerTokenStorage.add(BearerTokens(tokenInfo.accessToken, tokenInfo.refreshToken!!))
請注意,
bearerTokenStorage
應該在初始化客戶端之前建立,因為它將在客戶端設定中使用。
(5) -> 攜帶有效權杖的請求
現在我們擁有有效的權杖,因此我們可以向受保護的 Google API 發出請求並取得使用者資訊。首先,我們需要調整客戶端設定:
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
install(Auth) {
bearer {
loadTokens {
bearerTokenStorage.last()
}
sendWithoutRequest { request ->
request.url.host == "www.googleapis.com"
}
}
}
}
指定了以下設定:
已安裝的 ContentNegotiation 外掛程式與
json
序列化器,用於反序列化從資源伺服器以 JSON 格式接收的使用者資訊。帶有
bearer
提供者的 Auth 外掛程式設定如下:loadTokens
回呼從儲存區載入權杖。sendWithoutRequest
回呼設定為只向提供受保護資源存取權限的主機傳送憑證,而不等待401
(Unauthorized) 回應。
此客戶端可用於向受保護資源發出請求:
while (true) {
(6) <- 受保護的資源
資源伺服器以 JSON 格式返回使用者資訊。我們可以將回應反序列化為 UserInfo
類別實例並顯示個人問候語:
val userInfo: UserInfo = response.body()
println("Hello, ${userInfo.name}!")
UserInfo
類別如下所示:
import kotlinx.serialization.*
@Serializable
data class UserInfo(
val id: String,
val name: String,
@SerialName("given_name") val givenName: String,
@SerialName("family_name") val familyName: String,
val picture: String,
val locale: String
)
(7) -> 攜帶過期權杖的請求
在某些時候,客戶端會像步驟 5 中那樣發出請求,但使用過期的存取權杖。
(8) <- 401 未經授權回應
資源伺服器返回 401
未經授權的回應,因此客戶端應呼叫 refreshTokens
回呼。
請注意,
401
回應返回包含錯誤詳細資訊的 JSON 資料,我們需要在接收回應時處理此情況。
(9) -> 授權許可 (重新整理權杖)
為了取得新的存取權杖,我們需要設定 refreshTokens
並向權杖端點發出另一個請求。這次,我們使用 refresh_token
授予類型而不是 authorization_code
:
install(Auth) {
bearer {
refreshTokens {
val refreshTokenInfo: TokenInfo = client.submitForm(
url = "https://accounts.google.com/o/oauth2/token",
formParameters = parameters {
append("grant_type", "refresh_token")
append("client_id", System.getenv("GOOGLE_CLIENT_ID"))
append("refresh_token", oldTokens?.refreshToken ?: "")
}
) { markAsRefreshTokenRequest() }.body()
}
}
}
請注意,refreshTokens
回呼使用 RefreshTokensParams
作為接收者,並允許您存取以下設定:
client
實例。在上面的程式碼片段中,我們使用它來提交表單參數。oldTokens
屬性用於存取重新整理權杖並將其傳送到權杖端點。
HttpRequestBuilder
暴露的markAsRefreshTokenRequest
函式啟用對用於取得重新整理權杖的請求的特殊處理。
(10) <- 存取和重新整理權杖
收到新權杖後,我們可以將它們儲存在儲存區中,因此 refreshTokens
如下所示:
refreshTokens {
val refreshTokenInfo: TokenInfo = client.submitForm(
url = "https://accounts.google.com/o/oauth2/token",
formParameters = parameters {
append("grant_type", "refresh_token")
append("client_id", System.getenv("GOOGLE_CLIENT_ID"))
append("refresh_token", oldTokens?.refreshToken ?: "")
}
) { markAsRefreshTokenRequest() }.body()
bearerTokenStorage.add(BearerTokens(refreshTokenInfo.accessToken, oldTokens?.refreshToken!!))
bearerTokenStorage.last()
}
(11) -> 攜帶新權杖的請求
在此步驟中,對受保護資源的請求包含新權杖,並且應該正常工作。
val response: HttpResponse = client.get("https://www.googleapis.com/oauth2/v2/userinfo")
(12) <-- 受保護的資源
鑑於 401 回應返回包含錯誤詳細資訊的 JSON 資料,我們需要更新範例以將錯誤資訊作為 ErrorInfo
物件接收:
val response: HttpResponse = client.get("https://www.googleapis.com/oauth2/v2/userinfo")
try {
val userInfo: UserInfo = response.body()
println("Hello, ${userInfo.name}!")
} catch (e: Exception) {
val errorInfo: ErrorInfo = response.body()
println(errorInfo.error.message)
}
ErrorInfo
類別如下所示:
import kotlinx.serialization.*
@Serializable
data class ErrorInfo(val error: ErrorDetails)
@Serializable
data class ErrorDetails(
val code: Int,
val message: String,
val status: String,
)
您可以在此處找到完整範例:client-auth-oauth-google。