AuthenticationInterceptor
先月Alamofire 5.2がリリースされました。
https://github.com/Alamofire/Alamofire/releases/tag/5.2.0
新機能の1つとして、「AuthenticationInterceptor」があります。
(Release)
https://github.com/Alamofire/Alamofire/releases/tag/5.2.0
(PR)
https://github.com/Alamofire/Alamofire/pull/3164
クレデンシャルを使用したリクエストのAdaptとRetryを容易にするために導入されたとされています。
AuthenticationInterceptorは、AdaptとRetryの処理におけるスレッドやキューイングの管理を意識せずに実装するための仕組みのようです。
経緯
導入の発端?となったのはこのissueのようです。
https://github.com/Alamofire/Alamofire/issues/3086
今までAdvanced Usageに記載されていたOAuthのトークンリフレッシュ処理のexampleが突然削除されたのはなぜか?という趣旨のissueです。
従来Alamofireで行われていたトークンリフレッシュの実装はこんな感じでした。(上記issueより抜粋)
// MARK: - RequestRetrier
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
RequestRetrierを用いる方法ですが、上記のようなコードが今まではAlamofireのAdvanced Usageには記載がありました。
それが最近突如削除されたので、既存のサンプルコードが何かまずい動作をしていたのではないかと心配になった開発者の方々がいたようですね。
私もRequestRetrierを実際に利用していましたが、「lock.lock()処理がサンプルに入っているけど、入れても他に影響ないかな?」みたいなことを考えた記憶があります。
Alamofireの開発サイドとしては、どうやら掲載していたサンプルコードは**本番で動く想定のものではなかったとのことです。しかしその意図と反して本番プロジェクトにも利用されることが増えてしまったため、Advanced Usageから削除したとコメントされています。**実際、ネット上で見るRequestRetrierの実装の紹介の多くが上記のような実装になっていますよね。
こういった状況を踏まえ、スレッドの管理やlock処理について意識した実装をする必要のない仕組みの導入に至ったようです。
使い方
クレデンシャル情報をプロパティに持つstructを定義します。(公式のサンプルから抜粋)
struct OAuthCredential: AuthenticationCredential {
let accessToken: String
let refreshToken: String
let userID: String
let expiration: Date
// Require refresh if within 5 minutes of expiration
var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}
次にAuthenticatorクラスを実装します。
applyメソッドでは、クレデンシャル情報の適用(headerにトークン付与)、refreshメソッドではリフレッシュ処理を行います。
class OAuthAuthenticator: Authenticator {
func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
}
func refresh(_ credential: OAuthCredential,
for session: Session,
completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
// トークンリフレッシュ処理
requestToRefreshToken()
// 新しいクレデンシャルの作成
completion(.success(newCredential))
}
func didRequest(_ urlRequest: URLRequest,
with response: HTTPURLResponse,
failDueToAuthenticationError error: Error) -> Bool {
return response.statusCode == 401
}
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
return urlRequest.headers["Authorization"] == bearerToken
}
}
上記のようにrefreshメソッド内でトークンリフレッシュ処理を行い、新しいクレデンシャル情報をcompletion(.success)に渡してあげると、それ以降の通信は新しいクレデンシャルをapplyメソッドで適用してくれるようになります。
使い方はこんな感じです。 (公式のAdvancedUsageより抜粋)
// Generally load from keychain if it exists
let credential = OAuthCredential(accessToken: "a0",
refreshToken: "r0",
userID: "u0",
expiration: Date(timeIntervalSinceNow: 60 * 60))
// Create the interceptor
let authenticator = OAuthAuthenticator()
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
credential: credential)
// Execute requests with the interceptor
let session = Session()
let urlRequest = URLRequest(url: URL(string: "https://api.example.com/example/user")!)
session.request(urlRequest, interceptor: interceptor)
手軽にトークンリフレッシュ機能を実装することが可能でした。
尚、Authenticatorクラスの「didRequest」「isRequest」メソッドですが、下記のような定義がコメントで書かれているものの、手元でこのメソッドに到達するケースを見つけることができませんでした。
func didRequest(_ urlRequest: URLRequest,
with response: HTTPURLResponse,
failDueToAuthenticationError error: Error) -> Bool {
// If authentication server CANNOT invalidate credentials, return `false`
return false
// If authentication server CAN invalidate credentials, then inspect the response matching against what the
// authentication server returns as an authentication failure. This is generally a 401 along with a custom
// header value.
// return response.statusCode == 401
}
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
// If authentication server CANNOT invalidate credentials, return `true`
return true
// If authentication server CAN invalidate credentials, then compare the "Authorization" header value in the
// `URLRequest` against the Bearer token generated with the access token of the `Credential`.
// let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
// return urlRequest.headers["Authorization"] == bearerToken
}
issueでも質問をしてみていますが、引き続き調査をしつつ判明したことがあったら追記をしていきます。
以上、AuthenticationInterceptorを使ってみた際のメモでした。