Alamofire автоматически обновляет токен и повторяет предыдущий вызов API в iOS Swift 4

Сейчас я работаю над приложением iOS в Swift 4. Здесь я использую Alamofire для интеграции вызовов API. Мне нужно интегрировать правильный способ для автоматического обновления токена аутентификации и повторения предыдущих вызовов API. Я сохраняю токен аутентификации после успешного входа в систему. Поэтому после входа в каждый API я добавляю токен в часть заголовка. И когда срок действия токена истечет, я получу 401. Тогда мне нужно автоматически обновить токен аутентификации и снова вызвать тот же API. Как я могу это сделать? Я зарегистрировался в Stackru, но не получил никакого решения.

Вот мой вызов 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)
            }
        }
    }
}

Пожалуйста, помогите мне. Если вы можете объяснить с моим кодом, было бы очень хорошо.

3 ответа

Решение

Вам нужно проверить Alamofire RequestRetrier и RequestAdapter здесь

Вот пример, который у меня есть:

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 { $0(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 где мы присоединяем токен, здесь у меня есть собственная логика, где одна из служб не должна прикреплять этот токен в качестве заголовка. Второй метод func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) где я проверяю какой код ошибки (в моем примере 401). А потом я обновляю свои токены

 private func refreshTokens(completion: @escaping RefreshCompletion)

В моем случае у меня есть токен обновления и токен доступа, и когда я вызываю службу с токеном обновления, я не должен добавлять свой старый токен доступа в заголовок. Я думаю, что это не лучшая практика, но она была реализована из людей, которых я не знаю.

Вы можете легко обновить токен и повторить предыдущий вызов 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 есть свойство RequestInterceptor.RequestInterceptor имеет два метода: один - Adapt, который назначает access_token любому заголовку сетевого вызова, второй - метод Retry . В методе Retry мы можем проверить код статуса ответа и вызвать блок refresh_token, чтобы получить новый токен и повторить попытку предыдущего API.

Ответ @m1sh0 был мне чрезвычайно полезен. Я просто добавляю недостающую деталь, которую OP просил в комментариях: как сделать запрос Alamofire, чтобы он использовал Retrier и 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, поскольку они считаются неисправимыми.

Другие вопросы по тегам