Alamofire 自动刷新令牌并在 iOS Swift 4 中重试之前的 API 调用
Alamofire auto refresh token and retry previous API call in iOS Swift 4
现在我正在 Swift 4 中开发一个 iOS 应用程序。这里我使用 Alamofire 来集成 API 调用。我需要将正确的方法集成到 auto-refresh 身份验证令牌并重试之前的 API 调用。成功登录后,我将存储身份验证令牌。因此,登录后,在每个 API 中,我将令牌附加到 header 部分。如果令牌过期,我将收到 401。那时我需要 auto-refresh 身份验证令牌并再次调用相同的 API。我怎样才能做到这一点?我查看了 Whosebug,但没有得到任何解决方案。
这是我的 API 电话,
import Foundation
import Alamofire
import SwiftyJSON
class LoveltyAPI {
let loveltyURL = Bundle.main.object(forInfoDictionaryKey: "APIUrlString") as! String // Main URL
let buildVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String //infoDictionary?["CFBundleShortVersionString"] as AnyObject
weak var delegate:LoveltyProtocol?
func get_profile(app_user_id:String, token:String) {
let urlString = "\(loveltyURL)\(get_profile_string)?app_user_id=\(app_user_id)"
let headers = ["Content-Type":"application/json","X-Requested-With":"XMLHttpRequest", "Authentication":"Token \(token)"]
Alamofire.request(urlString, method: .get, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success:
let swiftyJsonVar = JSON(response.result.value!)
switch response.response?.statusCode {
case 200, 201:
self.delegate?.getUserProfile!(response: swiftyJsonVar["data"].dictionaryObject as AnyObject)
case 401:
self.delegate?.tokenExpired(response: tokenExpired as AnyObject)
case 404:
self.delegate?.serviceError!(response: swiftyJsonVar["message"] as AnyObject)
case 422:
self.delegate?.serviceError!(response: swiftyJsonVar["error"] as AnyObject)
case 503:
self.delegate?.appDisabled(response: swiftyJsonVar.dictionaryObject as AnyObject)
default:
self.delegate?.serviceError!(response: self.serverError as AnyObject)
}
case .failure(let error):
self.delegate?.serviceError!(response: self.serverError as AnyObject)
}
}
}
}
请帮助我。如果你能用我的代码解释一下,那就太好了。
您需要检查 Alamofire RequestRetrier 和 RequestAdapter here
这是我的一些例子:
import UIKit
import Alamofire
class MyRequestAdapter: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let lock = NSLock()
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
var accessToken:String? = nil
var refreshToken:String? = nil
static let shared = MyRequestAdapter()
private init(){
let sessionManager = Alamofire.SessionManager.default
sessionManager.adapter = self
sessionManager.retrier = self
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(BASE_URL), !urlString.hasSuffix("/renew") {
if let token = accessToken {
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
return urlRequest
}
// 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 in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken {
strongSelf.accessToken = accessToken
}
strongSelf.requestsToRetry.forEach { [=10=](succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: @escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(BASE_URL)token/renew"
Alamofire.request(urlString, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: ["Authorization":"Bearer \(refreshToken!)"]).responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["accessToken"] as? String
{
completion(true, accessToken)
} else {
completion(false, nil)
}
strongSelf.isRefreshing = false
}
}
}
我的例子有点复杂,但是是的,一般来说我们有两个重要的方法第一个是 adapt(_ urlRequest: URLRequest) throws -> URLRequest
我们附加令牌的地方,这里我有自定义逻辑其中一个服务不应该附加此标记为 header。第二种方法是 func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
,我在其中检查错误代码是什么(在我的示例中为 401)。然后我用
刷新我的令牌
private func refreshTokens(completion: @escaping RefreshCompletion)
在我的例子中,我有刷新令牌和访问令牌,当我使用刷新令牌调用服务时,我不应该在 header 中附加我的旧访问令牌。我认为这不是最佳实践,但它是由我不认识的人实施的。
@m1sh0 的回答对我帮助很大。我只是添加了 OP 在评论中要求的缺失细节:How do you make Alamofire request so it uses Retrier and Adapter?
我基本上使用了@m1sh0 的例子并这样称呼它:
var request_url = Constants.API_URL + "/path/to/resource"
let sessionManager = Alamofire.SessionManager.default
sessionManager.adapter = MyRequestAdapter.shared
sessionManager.request(request_url).validate().responseJSON { (response: DataResponse<Any>) in
switch(response.result) {
case .success(_):
print(response.result.value!)
completion(response.result.value!)
case .failure(_):
print(response.result.error!)
completion(response.result.error!)
break
}
}
请注意,您需要在请求中添加 validate()
才能在失败时重试。没有它,响应只是返回完成。另请注意,所有非 401 错误的响应块中都有一个失败案例,因为它们被认为是不可恢复的。
您可以轻松刷新令牌并使用
重试您之前的 API 调用
Alamofire RequestInterceptor
NetworkManager.swift
import Alamofire
class NetworkManager {
static let shared: NetworkManager = {
return NetworkManager()
}()
typealias completionHandler = ((Result<Data, CustomError>) -> Void)
var request: Alamofire.Request?
let retryLimit = 3
func request(_ url: String, method: HTTPMethod = .get, parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.queryString, headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil, completion: @escaping completionHandler) {
AF.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers, interceptor: interceptor ?? self).validate().responseJSON { (response) in
if let data = response.data {
completion(.success(data))
} else {
completion(.failure())
}
}
}
}
RequestInterceptor.swift
import Alamofire
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
guard let token = UserDefaultsManager.shared.getToken() else {
completion(.success(urlRequest))
return
}
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
print("\nadapted; token added to the header field is: \(bearerToken)\n")
completion(.success(request))
}
func retry(_ request: Request, for session: Session, dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else {
completion(.doNotRetry)
return
}
guard request.retryCount < retryLimit else {
completion(.doNotRetry)
return
}
print("retry statusCode....\(statusCode)")
switch statusCode {
case 200...299:
completion(.doNotRetry)
case 401:
refreshToken { isSuccess in isSuccess ? completion(.retry) : completion(.doNotRetry) }
break
default:
completion(.retry)
}
}
func refreshToken(completion: @escaping (_ isSuccess: Bool) -> Void) {
let params = [
"refresh_token": Helpers.getStringValueForKey(Constants.REFRESH_TOKEN)
]
AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default).responseJSON { response in
if let data = response.data, let token = (try? JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any])?["access_token"] as? String {
UserDefaultsManager.shared.setToken(token: token)
print("\nRefresh token completed successfully. New token is: \(token)\n")
completion(true)
} else {
completion(false)
}
}
}
}
Alamofire v5 has a property named RequestInterceptor.
RequestInterceptor has two method, one is Adapt which assign
access_token to any Network call header, second one is Retry method.
In Retry method we can check response status code and call
refresh_token block to get new token and retry previous API again.
现在我正在 Swift 4 中开发一个 iOS 应用程序。这里我使用 Alamofire 来集成 API 调用。我需要将正确的方法集成到 auto-refresh 身份验证令牌并重试之前的 API 调用。成功登录后,我将存储身份验证令牌。因此,登录后,在每个 API 中,我将令牌附加到 header 部分。如果令牌过期,我将收到 401。那时我需要 auto-refresh 身份验证令牌并再次调用相同的 API。我怎样才能做到这一点?我查看了 Whosebug,但没有得到任何解决方案。
这是我的 API 电话,
import Foundation
import Alamofire
import SwiftyJSON
class LoveltyAPI {
let loveltyURL = Bundle.main.object(forInfoDictionaryKey: "APIUrlString") as! String // Main URL
let buildVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String //infoDictionary?["CFBundleShortVersionString"] as AnyObject
weak var delegate:LoveltyProtocol?
func get_profile(app_user_id:String, token:String) {
let urlString = "\(loveltyURL)\(get_profile_string)?app_user_id=\(app_user_id)"
let headers = ["Content-Type":"application/json","X-Requested-With":"XMLHttpRequest", "Authentication":"Token \(token)"]
Alamofire.request(urlString, method: .get, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success:
let swiftyJsonVar = JSON(response.result.value!)
switch response.response?.statusCode {
case 200, 201:
self.delegate?.getUserProfile!(response: swiftyJsonVar["data"].dictionaryObject as AnyObject)
case 401:
self.delegate?.tokenExpired(response: tokenExpired as AnyObject)
case 404:
self.delegate?.serviceError!(response: swiftyJsonVar["message"] as AnyObject)
case 422:
self.delegate?.serviceError!(response: swiftyJsonVar["error"] as AnyObject)
case 503:
self.delegate?.appDisabled(response: swiftyJsonVar.dictionaryObject as AnyObject)
default:
self.delegate?.serviceError!(response: self.serverError as AnyObject)
}
case .failure(let error):
self.delegate?.serviceError!(response: self.serverError as AnyObject)
}
}
}
}
请帮助我。如果你能用我的代码解释一下,那就太好了。
您需要检查 Alamofire RequestRetrier 和 RequestAdapter here
这是我的一些例子:
import UIKit
import Alamofire
class MyRequestAdapter: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let lock = NSLock()
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
var accessToken:String? = nil
var refreshToken:String? = nil
static let shared = MyRequestAdapter()
private init(){
let sessionManager = Alamofire.SessionManager.default
sessionManager.adapter = self
sessionManager.retrier = self
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(BASE_URL), !urlString.hasSuffix("/renew") {
if let token = accessToken {
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
return urlRequest
}
// 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 in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken {
strongSelf.accessToken = accessToken
}
strongSelf.requestsToRetry.forEach { [=10=](succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: @escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(BASE_URL)token/renew"
Alamofire.request(urlString, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: ["Authorization":"Bearer \(refreshToken!)"]).responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["accessToken"] as? String
{
completion(true, accessToken)
} else {
completion(false, nil)
}
strongSelf.isRefreshing = false
}
}
}
我的例子有点复杂,但是是的,一般来说我们有两个重要的方法第一个是 adapt(_ urlRequest: URLRequest) throws -> URLRequest
我们附加令牌的地方,这里我有自定义逻辑其中一个服务不应该附加此标记为 header。第二种方法是 func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
,我在其中检查错误代码是什么(在我的示例中为 401)。然后我用
private func refreshTokens(completion: @escaping RefreshCompletion)
在我的例子中,我有刷新令牌和访问令牌,当我使用刷新令牌调用服务时,我不应该在 header 中附加我的旧访问令牌。我认为这不是最佳实践,但它是由我不认识的人实施的。
@m1sh0 的回答对我帮助很大。我只是添加了 OP 在评论中要求的缺失细节:How do you make Alamofire request so it uses Retrier and Adapter?
我基本上使用了@m1sh0 的例子并这样称呼它:
var request_url = Constants.API_URL + "/path/to/resource"
let sessionManager = Alamofire.SessionManager.default
sessionManager.adapter = MyRequestAdapter.shared
sessionManager.request(request_url).validate().responseJSON { (response: DataResponse<Any>) in
switch(response.result) {
case .success(_):
print(response.result.value!)
completion(response.result.value!)
case .failure(_):
print(response.result.error!)
completion(response.result.error!)
break
}
}
请注意,您需要在请求中添加 validate()
才能在失败时重试。没有它,响应只是返回完成。另请注意,所有非 401 错误的响应块中都有一个失败案例,因为它们被认为是不可恢复的。
您可以轻松刷新令牌并使用
重试您之前的 API 调用Alamofire RequestInterceptor
NetworkManager.swift
import Alamofire
class NetworkManager {
static let shared: NetworkManager = {
return NetworkManager()
}()
typealias completionHandler = ((Result<Data, CustomError>) -> Void)
var request: Alamofire.Request?
let retryLimit = 3
func request(_ url: String, method: HTTPMethod = .get, parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.queryString, headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil, completion: @escaping completionHandler) {
AF.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers, interceptor: interceptor ?? self).validate().responseJSON { (response) in
if let data = response.data {
completion(.success(data))
} else {
completion(.failure())
}
}
}
}
RequestInterceptor.swift
import Alamofire
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
guard let token = UserDefaultsManager.shared.getToken() else {
completion(.success(urlRequest))
return
}
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
print("\nadapted; token added to the header field is: \(bearerToken)\n")
completion(.success(request))
}
func retry(_ request: Request, for session: Session, dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else {
completion(.doNotRetry)
return
}
guard request.retryCount < retryLimit else {
completion(.doNotRetry)
return
}
print("retry statusCode....\(statusCode)")
switch statusCode {
case 200...299:
completion(.doNotRetry)
case 401:
refreshToken { isSuccess in isSuccess ? completion(.retry) : completion(.doNotRetry) }
break
default:
completion(.retry)
}
}
func refreshToken(completion: @escaping (_ isSuccess: Bool) -> Void) {
let params = [
"refresh_token": Helpers.getStringValueForKey(Constants.REFRESH_TOKEN)
]
AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default).responseJSON { response in
if let data = response.data, let token = (try? JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any])?["access_token"] as? String {
UserDefaultsManager.shared.setToken(token: token)
print("\nRefresh token completed successfully. New token is: \(token)\n")
completion(true)
} else {
completion(false)
}
}
}
}
Alamofire v5 has a property named RequestInterceptor. RequestInterceptor has two method, one is Adapt which assign access_token to any Network call header, second one is Retry method. In Retry method we can check response status code and call refresh_token block to get new token and retry previous API again.