概要
OkHttpのAuthenticatorを利用することでステータスコードが401 Unauthorized
(認証の失敗)であった場合に任意の処理を実行できます。
Authenticatorの実装例
次のコードはAuthenticatorの実装例です。リクエストにAuthorization Headerでtokenを付与する必要があるAPIを想定しています。
class MyAuthenticator(private val repository: AuthenticationRepository) : Authenticator {
private val Response.retryCount: Int
get() {
var currentResponse = priorResponse()
var result = 1
while (currentResponse != null) {
result++
currentResponse = currentResponse.priorResponse()
}
return result
}
override fun authenticate(route: Route?, response: Response): Request? = when {
response.retryCount >= 3 -> null
else -> {
val token = runBlocking { repository.refreshToken() }
response.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${token}")
.build()
}
}
}
response.retryCount
が3未満の場合にtokenの更新と再リクエストを行います。この実装ではリトライ回数は3回です。
override fun authenticate(route: Route?, response: Response): Request? = when {
response.retryCount >= 3 -> null
else -> {
// tokenの更新と再リクエスト
}
}
retryCount
はResponseクラスの拡張関数として実装していますが、これはこちらの記事の実装を参考にしています(綺麗な実装で好きです)。
tokenを更新する処理は次のinterfaceで定義していて、repository.refreshToken()
のようにしてtokenの更新処理を行います。
interface AuthenticationRepository {
suspend fun refreshToken(): String
}
suspend functionなのでAuthenticatorではrunBlocking
で実行します。
val token = runBlocking { repository.refreshToken() }
使用方法
OkHttp Clientを作成する際にauthenticator
に実装したAuthenticatorをセットすることで使用できます。
val client: OkHttpClient = OkHttpClient().newBuilder()
.authenticator(MyAuthenticator(authenticationRepository))
.build()
AuthenticatorのUnitTest
Authenticatorのテストもinterceptorと同様に書けます。次のテストコードは1度目のリトライで認証が成功した場合を想定します。モックライブラリにはMockk、アサーションライブラリにはTruthを使用しています。
@RunWith(AndroidJUnit4::class)
class AuthenticatorTest {
@get:Rule
val server: MockWebServer = MockWebServer()
@Test
fun authenticatorTest() {
// MockResponseの用意
server.enqueue(MockResponse().apply {
status = "HTTP/1.1 401 Unauthorized"
})
server.enqueue(MockResponse().apply {
status = "HTTP/1.1 200 OK"
})
// refreshTokenが成功し新しいtokenが返るよう指定
val repository = mockk<AuthenticationRepository>()
coEvery { repository.refreshToken() } returns "NEW_TOKEN"
val client: OkHttpClient = OkHttpClient().newBuilder()
.authenticator(MyAuthenticator(repository))
.build()
val response = client.newCall(Request.Builder().url(server.url("/")).build()).execute()
// refreshToken関数が実行されていることを検証
coVerify { repository.refreshToken() }
// 想定通りHeaderにtokenが指定されていることを検証
Truth.assertThat(response.request().header("Authorization")).isEqualTo("Bearer NEW_TOKEN")
}
}
このテストコードでは、ステータスコードHTTP/1.1 401 Unauthorized
と、HTTP/1.1 200 OK
をセットした2つのMockResponseのそれぞれMockWebServerにenqueueします。そして、AuthenticationRepository
をモックし、新しいtokenを返すよう指定します。その後テスト対象のAuthenticatorをセットしたclientを作成しリクエストを実行、最後にrequestを検証します。
また、3回以上リトライを行った場合のテストは次のように書けます。
@RunWith(AndroidJUnit4::class)
class AuthenticatorTest {
@get:Rule
val server: MockWebServer = MockWebServer()
@Test
fun authenticatorTest() {
repeat(4) {
server.enqueue(MockResponse().apply {
status = "HTTP/1.1 401 Unauthorized"
})
}
val repository = mockk<AuthenticationRepository>()
coEvery { repository.refreshToken() } returns ""
val client: OkHttpClient = OkHttpClient().newBuilder()
.authenticator(MyAuthenticator(repository))
.build()
val response = client.newCall(Request.Builder().url(server.url("/")).build()).execute()
coVerify { repository.refreshToken() }
Truth.assertThat(responseCount(response)).isEqualTo(3)
Truth.assertThat(response.isSuccessful).isFalse()
}
private fun responseCount(response: Response): Int {
var currentResponse = response.priorResponse()
var result = 1
while (currentResponse != null) {
result++
currentResponse = currentResponse.priorResponse()
}
return result
}
}
参考
- https://square.github.io/okhttp/recipes/#handling-authentication-kt-java
-
https://github.com/yschimke/okhttp/blob/f9171936cac4cfc607b622951e7e65a085781a6a/okhttp/src/main/kotlin/okhttp3/Authenticator.kt
- コメントにリトライ回数をカウントするサンプルコードが書かれています。
- https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit