Recently, when I used Moya, I suddenly encountered the problem of token refresh, so I didn’t care about it until I had time. In the past, I didn’t care much about this problem of server Token. At most, I needed to re-log in after expiration. Even if I wanted to renew the Token, the server might just help us to do it directly. It took a long time for the problem to come to a satisfactory conclusion.

Problems encountered 🤨

The server defines the validity period of the token, which is approximately 2 hours. After the token expires, the front-end invokes the token refresh interface. In fact, I didn’t care about it at the beginning. I just thought that after the server reported an error, I would refresh the token. There was no problem. The result is still too young ah, a concurrent to be completely confused.

How to solve 🧐

The detours that preliminary practice takes 😩

This kind of large area will encounter the problem should have a lot of information. [Moya RxSwift extension after the platform no sense refresh token] roughly understand the next, feel the logic is workable, nothing more than when the concurrent processing on the line.

The process goes something like this:

Request -> Filter requests that do not require tokens -> Whether to refresh the token after receiving the response result -> resend the last request

The problem comes when the result is true and the concurrency is processed. This method is processed after the response result is received. When multiple interfaces are asynchronously called at the same time, the sequence is inconsistent, and the token refresh will be disordered, which will eventually cause the token to be invalid when the token is refreshed.

Final solution 🥰

With the cooperation of the server, the token expiration time is adjusted to a very short time, and the above scheme is finally abandoned after repeated attempts. After a period of data searching, we finally found relevant schemes for reference [Alamofire – use interceptor to gracefully authorize the interface], [Moya framework analysis], [Swift Moya to refresh expired tokens].

The ultimate core solution is to useAlamofireIn thesessionAdd an authorization interceptorAuthenticationInterceptor

The process looks like this:

Select the specified Provider to execute the request, verify the validity period set locally, handle 401 expiration, and process the request normally

Based on these data, the basic can be determined using the AuthenticationInterceptor Alamofire can achieve what I want effect. Since I use MultiTarget to manage all requests uniformly with a single Provider, the final implementation is a bit different

Start by defining an authorization certificate, OAuthCredential, that holds the token

struct OAuthCredential: AuthenticationCredential {
    var accessToken: String = Global.shared.token {
        didSet {
            Global.shared.token = accessToken
        }
    }
    var refreshToken: String = Global.shared.refreshToken {
        didSet {
            Global.shared.refreshToken = refreshToken
        }
    }
    var userID: String = ""
    var expiration: Date = Date(timeIntervalSinceNow: OAuthAuthenticator.expirationDuration)

    / / here we are in the period of validity is about to expire the ` OAuthAuthenticator. ExpirationDuration - OAuthAuthenticator. Return to need to refresh ` expirationDuration * 0.2 seconds
    var requiresRefresh: Bool { Date(timeIntervalSinceNow: OAuthAuthenticator.expirationDuration * 0.2) > expiration }
    
}

Copy the code

Implement authorized interceptors

class OAuthAuthenticator: Authenticator {
    /// Token expiration time
    static let expirationDuration: TimeInterval = 60 * 100
    
    / / / add the header
    func apply(_ credential: OAuthCredential.to urlRequest: inout URLRequest) {
        urlRequest.headers.add(.authorization(bearerToken: Global.shared.token))
    }
    /// implement the refresh process
    func refresh(_ credential: OAuthCredential.for session: Session.completion: @escaping (Result<OAuthCredential.Error- > >)Void) {
        logger.debug("Need to refresh token!!")
        
        MoyaProvider<AuthApi>(plugins: [ NetworkLoggerPlugin()]).request(.refreshToken) { result in
            switch result {
            case .success(let response):
                logger.debug("response = \(response.statusCode)")
                guard let dic = dataToDictionary(data: response.data) else{
                    completion(.failure(YHError.other(code: -999, msg: "Parsing error")))
                    return
                }
                guard let model = ResponseBaseModel<RefreshToken>(JSON: dic) else {
                    completion(.failure(YHError.other(code: -999, msg: "Parsing error")))
                    return
                }
                guard model.code = = 0 else {
                    let hud = HUD.show(message: "Please log in again")
                    hud?.pinned = true
                    Global.logout()
                    completion(.failure(YHError.other(code: -999, msg: "Please log in again")))
                    return
                }
                Global.shared.refreshToken = model.data?.refresh_token ?? ""
                Global.shared.token = model.data?.token ?? ""
                
                completion(.success(OAuthCredential(accessToken: model.data?.token ?? "",
                                                    refreshToken: model.data?.refresh_token ?? "",
                                                    userID: Global.shared.userInfo._id,
                                                    expiration: Date(timeIntervalSinceNow: OAuthAuthenticator.expirationDuration))))
            case .failure(let error ):
                logger.debug("error = \(error)")
                completion(.failure(error))
            }
        }
    }

    func didRequest(_ urlRequest: URLRequest.with response: HTTPURLResponse.failDueToAuthenticationError error: Error) -> Bool {
        logger.debug("Server token expired !!!!!!!!")
        return response.statusCode = = 401
    }

    func isRequest(_ urlRequest: URLRequest.authenticatedWith credential: OAuthCredential) -> Bool {
        let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
        return urlRequest.headers["Authorization"] = = bearerToken
    }
}

Copy the code

Here is a little need to pay attention to, if the first token expired at the service, back to 401 when Moya need extra processing configuration validationType, otherwise can’t trigger AuthenticationInterceptor Retry method!

extension TargetType {
    public var validationType: ValidationType {
        switch self {
        case .refreshToken:
            return .none
        default: return .successCodes
        }
    }
}
Copy the code

When dealing with whether authorization certificate is needed, I created two different providers, which are used to create session with authorization verification and session without verification

public var netProvider = CustomProvider.createAuthProvider()
public var noAuthProvider = CustomProvider.createNoAuthProvider()
Copy the code
    /// Create the provider to be authenticated
    static func createAuthProvider(a) -> CustomProvider {
        
        let networkLoggerPlugin = NetworkLoggerPlugin(configuration: .init(formatter: NetworkLoggerPlugin.Configuration.Formatter.init(entry: defaultEntryFormatter), output: reversedPrint, logOptions: .verbose))
        
        let configuration = URLSessionConfiguration.default
        configuration.headers = .default
        let interceptor: AuthenticationInterceptor? =  AuthenticationInterceptor(authenticator: OAuthAuthenticator(), credential: YHProvider.credential)
        let customSession = Session(configuration: configuration, startRequestsImmediately: false, interceptor: interceptor)
        return CustomProvider(endpointClosure: endpointClosure(),
                          requestClosure: requestClosure(),
                          stubClosure: MoyaProvider.neverStub,
                          callbackQueue: DispatchQueue.main,
                          session: customSession,
                          plugins: [networkLoggerPlugin, StorePlugin(), HeaderPlugin(), ErrorPlugin()],
                          trackInflights: false)}// create a provider that does not require authentication
    static func createNoAuthProvider(a) -> CustomProvider {

        let networkLoggerPlugin = NetworkLoggerPlugin(configuration: .init(formatter: NetworkLoggerPlugin.Configuration.Formatter.init(entry: defaultEntryFormatter), output: reversedPrint, logOptions: .verbose))
        
        return CustomProvider(endpointClosure: endpointClosure(),
                          requestClosure: requestClosure(),
                          stubClosure: MoyaProvider.neverStub,
                          callbackQueue: DispatchQueue.main,
                          plugins: [networkLoggerPlugin, StorePlugin(), HeaderPlugin(), ErrorPlugin()],
                          trackInflights: false)}Copy the code