获得 401 响应代码并重试请求后,使用 URLSession 刷新访问令牌

tha*_*don 9 ios swift urlsession

我正在为使用 OAuth 2.0 授权技术 ( Access& Refresh Token) 的iOS 应用程序构建网络客户端。我一直在努力实现我的网络客户端的一项功能:

  • 当一个401错误出现时即表示Access Token已经过期,我需要发送Refresh Token到我的服务器,以获得新的Access Token
  • 获得新Access Token请求后,我需要重做之前出现 401 错误的请求。

到目前为止,我已经为我的网络客户端编写了这段代码:

typealias NetworkCompletion = Result<(Data, URLResponse), FRNetworkingError>

/// I am using a custom result type to support just an Error and not a Type object for success
enum NetworkResponseResult<Error> {
    case success
    case failure(Error)
}

class FRNetworking: FRNetworkingProtocol {
    fileprivate func handleNetworkResponse(_ response: HTTPURLResponse) -> NetworkResponseResult<Error> {
        switch response.statusCode {
        case 200...299: return .success
        case 401: return .failure(FRNetworkingError.invalidAuthToken)
        case 403: return .failure(FRNetworkingError.forbidden)
        case 404...500: return .failure(FRNetworkingError.authenticationError)
        case 501...599: return .failure(FRNetworkingError.badRequest)
        default: return .failure(FRNetworkingError.requestFailed)
        }
    }

    func request(using session: URLSession = URLSession.shared, _ endpoint: Endpoint, completion: @escaping(NetworkCompletion) -> Void) {
        do {
            try session.dataTask(with: endpoint.request(), completionHandler: { (data, response, error) in
                if let error = error {
                    print("Unable to request data \(error)")
                    // Invoke completion for error
                    completion(.failure(.unknownError))
                } else if let data = data, let response = response {
                    // Passing Data and Response into completion for parsing in ViewModels
                    completion(.success((data, response)))
                }
            }).resume()
        } catch {
            print("Failed to execute request", error)
            completion(.failure(.requestFailed))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Endpoint 只是一个构建 URLRequest 的结构:

struct Endpoint {
    let path: String
    let method: HTTPMethod
    let parameters: Parameters?
    let queryItems: [URLQueryItem]?
    let requiresAuthentication: Bool

    var url: URL? {
        var components = URLComponents()
        components.scheme = "http"
        components.host = "127.0.0.1"
        components.port = 8000
        components.path = "/api\(path)"
        components.queryItems = queryItems
        return components.url
    }

    func request() throws -> URLRequest {
        /// Creates a request based on the variables per struct
    }
}
Run Code Online (Sandbox Code Playgroud)

我在哪里放置允许FRNetworking.request()获取新令牌并重试请求的代码?

我在else if let data = data, let response = response声明中做了以下事情:

if let response = response as? HTTPURLResponse {
    let result = self.handleNetworkResponse(response)
    switch result {
    case .failure(FRNetworkingError.invalidAuthToken):
        break
    // TODO: Get new Access Token and refresh?
    default:
        break
    }
}
Run Code Online (Sandbox Code Playgroud)

这是刷新令牌和重做 API 调用的正确方法还是有更好的方法?

小智 0

您必须编写一个函数来更新令牌,并根据结果返回 true 或 false

 private func refreshAccessToken(completion: @escaping (Bool) -> Void {
     // Make a request to refresh the access token
     // Update the accessToken and refreshToken variables when the request is completed
     // Call completion(true) if the request was successful, completion(false) otherwise 
 }
Run Code Online (Sandbox Code Playgroud)

在类的开头声明2个变量

var session: URLSession
var endpoint: Endpoint
Run Code Online (Sandbox Code Playgroud)

在里面case .failure分配这些变量

 session = session
 endpoint = endpoint
Run Code Online (Sandbox Code Playgroud)

然后调用refreshAccessToken方法。最终代码如下所示

if let response = response as? HTTPURLResponse {
    let result = self.handleNetworkResponse(response)
    switch result {
    case .failure(FRNetworkingError.invalidAuthToken):
        session = session
        endpoint = endpoint

        self?.refreshAccessToken { success in
            if success {
                self?.request(using: session, endpoint, completion: completion)
            } else {
                completion(.failure(.unknownError))
            }
        }
        break
    default:
        break
    }
}
Run Code Online (Sandbox Code Playgroud)