swift - 在使用 AlamoFire 的 RequestInterceptor 类时,如何保证一次只运行一次重试?
问题描述
我正在实现一种使用 OAuth2 和 AlamoFire5 刷新会话令牌的方法,并且我正在尝试弄清楚如何解决这种情况:
1 - 当某个请求失败时,必须启动一个 refreshToken 请求,这必须是一次运行的唯一一个 refreshToken 请求。即在该请求完成之前,不应重试其他失败的请求。
2 - 如果 refreshToken 以错误结束,则应用程序必须重新启动,并且必须取消所有其他正在等待的请求。
3 - 如果 refreshToken 请求成功,则必须更新令牌,并且所有其他等待的请求现在必须继续。
我正在使用 AlamoFire 的 RequestInterceptor 类来尝试解决这个问题,到目前为止我的实现是这样的:
final class RequestInterceptor: Alamofire.RequestInterceptor {
private let disposeBag = DisposeBag()
private let lock = NSRecursiveLock()
private var refreshTokenParameters: TokenParameters {
TokenParameters(clientId: "pdappclient",
grantType: "refresh_token",
refreshToken: KeychainManager.shared.refreshToken)
}
private let storage: AccessTokenStorage
init(storage: AccessTokenStorage) {
self.storage = storage
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
lock.lock()
defer { lock.unlock() }
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
return completion(.doNotRetryWithError(error))
}
let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
.fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))
refreshTokenRequest.subscribe(onSuccess: { token in
self.lock.unlock()
self.storage.accessToken = token.accessToken ?? ""
completion(.retry)
}, onError: { error in
self.lock.unlock()
completion(.doNotRetryWithError(error))
}).disposed(by: disposeBag)
}
}
如何使用 RequestInterceptor 解决这种情况?
解决方案
您可以使用一个数组来存储重试闭包,用于在令牌刷新完成之前可能发生的请求,并使用一个布尔值来知道刷新操作正在进行中。
你最终会得到这样的东西:
final class RequestInterceptor: Alamofire.RequestInterceptor {
private let disposeBag = DisposeBag()
private let lock = NSRecursiveLock()
private var refreshTokenParameters: TokenParameters {
TokenParameters(
clientId: "pdappclient",
grantType: "refresh_token",
refreshToken: KeychainManager.shared.refreshToken
)
}
private let storage: AccessTokenStorage
private var retryQueue = [(RetryResult) -> Void]()
private var isTokenRefreshing = false
init(storage: AccessTokenStorage) {
self.storage = storage
}
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
lock.lock()
defer { lock.unlock() }
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
retryQueue.append(completion)
if !isTokenRefreshing {
isTokenRefreshing = true
let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
.fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))
refreshTokenRequest.subscribe(onSuccess: { token in
self.lock.lock()
defer { self.lock.unlock() }
self.storage.accessToken = token.accessToken ?? ""
self.retryQueue.forEach { $0(.retry) }
self.retryQueue.removeAll()
self.isTokenRefreshing = false
}, onError: { error in
self.lock.lock()
defer { self.lock.unlock() }
self.retryQueue.forEach { $0(.doNotRetryWithError(error)) }
self.retryQueue.removeAll()
self.isTokenRefreshing = false
}).disposed(by: disposeBag)
}
}
}
请注意,正如延迟文档所述:
语句用于在将
defer
程序控制转移到 defer 语句出现的范围之外之前执行代码。
所以,第一条语句的闭包defer
会在闭包之前执行。onSuccess
onError
这就是为什么我们需要再次锁定源内部onSuccess
和onError
闭包。
推荐阅读
- discord.py - 通过 Discord Bot 启动 Minecraft 服务器
- sql - 如何在 presto 中捕获 NaN?
- r - 在R中的函数上循环
- duration - 计算python列表中的持续时间
- linux - 在没有 eval 的情况下从 bash 中的“key1='val1' key2='val2'”字符串中解析变量
- java - 将 Hibernate SessionFactory 与 JPA EntityManager 一起使用
- r - 在 R 中加入 html 标签
- javascript - 是否可以在单击特定按钮时显示一些标记?
- c# - 如何获得具有友好名称(字符串)的类型(类型对象)?
- php - MongoDB:SSL/TLS 握手失败并且没有找到合适的服务器