OAuth2访问令牌与Swift p2/OAuth2的并行刷新请求

km0tfn4u  于 2022-10-31  发布在  Swift
关注(0)|答案(3)|浏览(252)

我正在使用https://github.com/p2/OAuth2通过OAuth2连接到我的应用程序的后端,它运行得相当好。
我遇到的问题是,当访问令牌过期时,多个请求同时发生,其中一些请求会失败。
可以从应用程序的不同部分触发并行请求。例如,当启动应用程序时,当前位置将发送到服务器,并下载事件列表。
要确保在第一个令牌仍在运行时不会发出第二个刷新令牌请求,最好的方法是什么?

rbl8hiat

rbl8hiat1#

查找令牌生存期并设置缓冲区,例如1-2分钟,如果令牌需要刷新,则在令牌刷新时保存所有请求之后,执行所有保存请求您可以使用DispatchQueue和DispatchWorkItem执行此操作
示例代码如下。

final class Network: NSObject {

    static let shared = Network()

    private enum Constants {
        static let tokenRefreshDiffrenceMinute = 1
        static let tokenExpireDateKey = "tokenExpireDate"
    }

    private(set) var tokenExpireDate: Date! {
        didSet {
            UserDefaults.standard.set(tokenExpireDate, forKey: Constants.tokenExpireDateKey)
        }
    }

    public override init() {
        super.init()

        if let date = UserDefaults.standard.object(forKey: Constants.tokenExpireDateKey) as? Date {
            tokenExpireDate = date
            print("Token found!")
        }
        else {
            print("Token not found!")
            isTokenRefreshing = true
            getToken {
                self.isTokenRefreshing = false
                self.executeAllSavedRequests()
            }
        }
    }

    private var isTokenRefreshing = false
    private var savedRequests: [DispatchWorkItem] = []

    func request(url: String, params: [String: Any], result: @escaping (String?, Error?) -> Void) {

        // isTokenRefreshing save all requests
        if isTokenRefreshing {

            saveRequest {
                self.request(url: url, params: params, result: result)
            }

            return
        }

        // if token expire
        if getMinutesFrom2Dates(Date(), tokenExpireDate) < Constants.tokenRefreshDiffrenceMinute {

            // open this flag for we need wait refresh token
            isTokenRefreshing = true

            // save current request too
            saveRequest {
                self.request(url: url, params: params, result: result)
            }

            // get token
            self.getToken { [unowned self] in
                self.isTokenRefreshing = false
                self.executeAllSavedRequests()
            }
        } else {
            //Alamofire.request ...

            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                DispatchQueue.main.async(execute: {
                    result(url, nil)
                })
            }
        }
    }

    private func saveRequest(_ block: @escaping () -> Void) {
        // Save request to DispatchWorkItem array
        savedRequests.append( DispatchWorkItem {
            block()
        })
    }

    private func executeAllSavedRequests() {
        savedRequests.forEach({ DispatchQueue.global().async(execute: $0) })
        savedRequests.removeAll()
    }

    private func getToken(completion: @escaping () -> Void) {
        print("Token needs a be refresh")
        // Goto server and update token
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [unowned self] in
            DispatchQueue.main.async(execute: { [unowned self] in
                self.tokenExpireDate = Date().addingTimeInterval(120)
                print("Token refreshed!")
                completion()
            })
        }
    }

    private func getMinutesFrom2Dates(_ date1: Date, _ date2: Date) -> Int {
        return Calendar.current.dateComponents([.minute], from: date1, to: date2).minute!
    }
}
gkl3eglg

gkl3eglg2#

您应该将失败请求排队401.由于您没有提供用于刷新令牌的代码,我向您解释了应该如何做,并让您自己实现它:

  • 为身份验证请求创建共享检索器
  • 它应该有一个重试请求的共享队列。
  • 当一个请求由于accessToken而失败时,它将被附加到requestQueue并等待访问令牌被刷新。
  • 因此,观察队列,如果它是空的,现在它有一个新项目,意味着令牌最近过期,应该执行刷新逻辑
  • 同时,另一个请求由于401错误而失败。
  • 将其添加到requestQueue
  • 观察者会注意到这个但是它不会尝试刷新令牌!因为在这个请求添加到它之前它不是空的。所以它只是在那里附加和等待。
  • 所以过了一段时间,新的访问令牌到达
  • 然后,您可以使用新的访问令牌重试队列中的所有请求。(从0开始以保持顺序,或同时异步以获得更快的响应)

-如果有一个请求已经执行,但未命中服务器,除非队列为空,该怎么办?

好吧,这是非常罕见的情况,但它可能会发生(已经发生了我).如果你实现它正确像我说的,不要搞砸whit的东西,如正在重试标志,它将只是刷新两次!不是真的想要,但它的罚款,它会像一个魅力.

-如果我们不想在这种罕见情况下刷新两次或更多次,该怎么办?

虽然它完全符合OAuth 2规则,但你可以这样做来防止错误:- 一旦你得到401错误(或任何你标记为auth错误),立即删除访问令牌。-任何进一步的请求将注意到没有访问令牌请求,他们可以自动和直接发送到requestQueue。-所以没有更多的请求竞争条件在所有。

-最后有任何备注吗?

如果刷新逻辑失败,请不要忘记清除队列。您也可以保留它们,并在用户再次登录时重试,但您必须将新登录用户的身份与填充队列的前一个用户的身份进行核对。
希望能有所帮助

smdncfj3

smdncfj33#

我们使用串行队列和信号量修复了类似的问题

import Alamofire

class AccessTokenInterceptor: RequestInterceptor {

    let auth = OAuthService()

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
            auth.refreshToken { [weak self] result in
                switch result {
                case .success(_):
                    urlRequest.setValue(accessTokenType + (self?.auth.oauth2?.accessToken ?? ""), forHTTPHeaderField: "Authorization")
                    urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
                    urlRequest.setValue(token, forHTTPHeaderField: "Token")
                    completion(.success(urlRequest))
                case .failure(let error):
                    switch error {
                    case .noRefreshToken:
                        self?.forceLogoutUser(error: error)
                    case .invalidGrant(_):
                        self?.forceLogoutUser(error: error)
                    default: ()
                    }
                }
            }
        }

        func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {

        }
    }

这里,每当调用getToken时,我们将其添加到串行队列中,并使用信号量确保一次只发送一个调用。

import Foundation
import OAuth2

class OAuthService: NSObject {

    private let serialQueue : DispatchQueue = DispatchQueue(label: "refreshToken.serial.queue")
    private let semaphore : DispatchSemaphore = DispatchSemaphore(value: 0)

    func getToken(completion: @escaping (Result<Bool, OAuth2Error>) -> ()) {
            if let validAccessToken = oauth2?.hasUnexpiredAccessToken(), validAccessToken {
                completion(.success(true))
                semaphore.signal()
            }else {
                serialQueue.async {
                    self.refreshOAuth2Token(completion)
                }
                refreshOAuth2Token(completion)
            }
            semaphore.wait()
        }

        func refreshOAuth2Token(_ completion: @escaping(Result<Bool, OAuth2Error>) -> ()) {
            oauth2?.clientConfig.secretInBody = true
            oauth2?.doRefreshToken(callback: { [weak self] authParamters, error in
                guard let self = self else { return }
                self.semaphore.signal()
                if let error = error {
                    completion(.failure(error))
                } else {
                    completion(.success(true))
                }
            })
        }

    }

相关问题