Как гарантировать, что одновременно выполняется только одна попытка при использовании класса RequestInterceptor от AlamoFire?

#swift #alamofire

#swift #alamofire

Вопрос:

Я реализую способ обновления токена сеанса с помощью OAuth2 с помощью AlamoFire5, и я пытаюсь выяснить, как решить этот сценарий:

1 — При сбое какого-либо запроса должен запускаться запрос refreshToken, который должен быть единственным выполняемым одновременно запросом refreshToken. т. е. Другие запросы, которые завершились неудачей, не должны повторяться до завершения этого запроса.

2 — Если refreshToken завершается с ошибкой, приложение должно перезапускаться, а все остальные ожидающие запросы должны быть отменены.

3 — Если запрос refreshToken выполнен успешно, токен должен быть обновлен, и все остальные ожидающие запросы должны теперь продолжаться.

Я использую класс RequestInterceptor от AlamoFire, чтобы попытаться решить эту проблему, и моя реализация пока такова:

 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?

Ответ №1:

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

В итоге вы получите что-то вроде этого:

 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 .

Итак, закрытие первого defer оператора будет выполнено до onSuccess onError закрытия or.

Вот почему нам нужно снова заблокировать источник внутри onSuccess и onError замыкания.