最近运用Moya突然碰到token改写的问题,一直没有放在心上,最近有时间就好好的弄了下。
曾经对于服务端Token的这个问题都没有怎样关心过,顶多处理一下过期后要求从头登录而已,就算要做续期服务端或许就直接帮咱们做掉了。这次暴露出来的这个问题整了好久才算有一个比较满意的效果。

遇到的问题

服务端界说了token的有效期,差不多在2个小时。token过期后需求前端自行调用改写token接口
一开始其实没有放在心上,想着服务端报错后就进行token的改写就行了,没什么问题。
效果还是太年轻啊,一个并发直接给整懵了。

怎样处理

开始实践走的弯路

这种大面积都会碰到的问题应该有许多资料的吧。
公然参照这个小哥哥的写法 [Moya 的 RxSwift 扩展之后台无感改写token] 大致理解了下,感觉上逻辑也是行得通,无非便是并发的时分多处理一下就行。

流程大概是这样:

央求 -> 过滤不需求token的央求 -> 拿到照应效果后处理是否需求改写token -> 重发上一次央求

效果然到处理并发的时分问题就来了,这个方法是在拿到照应效果后才会处理,在异步一同调用多个接口的时分会构成先后顺序不一致,且token改写时会紊乱,毕竟导致改写token时构成token不合法。

毕竟处理

在服务端的配合下将token过期时间调整到很短的情况下,多次测验后毕竟抛弃上面的计划。
通过一段时间的资料查找,毕竟找到相关可参阅的计划 [Alamofire – 运用拦截器高雅的对接口进行授权] 、 [Moya结构浅析]、[Swift Moya改写过期Token]

毕竟的核心处理方法是运用Alamofire中的session增加授权拦截器AuthenticationInterceptor

流程大致是这样:

选定指定的Provider实行央求->校验凭证本地设置的有效期->处理401过期情况->正常处理央求

根据这些资料,基本能够承认运用Alamofire中的AuthenticationInterceptor即可完结我想要的作用。
因为我运用了MultiTarget单个Provider统一管理一切央求,所以在毕竟的完结上还是有点不同

首要界说一个用于存放token的授权凭证 OAuthCredential

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)
    // 这儿咱们在有效期行将过期的  `OAuthAuthenticator.expirationDuration - OAuthAuthenticator.expirationDuration * 0.2` 秒回来需求改写
    var requiresRefresh: Bool { Date(timeIntervalSinceNow: OAuthAuthenticator.expirationDuration * 0.2) > expiration }
}

完结授权拦截器

class OAuthAuthenticator: Authenticator {
    /// token 过期时间
    static let expirationDuration: TimeInterval = 60 * 100
    /// 增加header
    func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
        urlRequest.headers.add(.authorization(bearerToken: Global.shared.token))
    }
    /// 完结改写流程
    func refresh(_ credential: OAuthCredential,
                 for session: Session,
                 completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
        logger.debug("需求改写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: "解析过错")))
                    return
                }
                guard let model = ResponseBaseModel<RefreshToken>(JSON: dic) else {
                    completion(.failure(YHError.other(code: -999, msg: "解析过错")))
                    return
                }
                guard model.code == 0 else {
                    let hud = HUD.show(message: "请从头登录")
                    hud?.pinned = true
                    Global.logout()
                    completion(.failure(YHError.other(code: -999, msg: "请从头登录")))
                    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("服务端token过期!!!!!!!!")
        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
    }
}

这儿有点需求注意,如果服务端的token先过期,回来401时Moya需求额定处理配置validationType,否则无法触发AuthenticationInterceptorRetry方法!

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

在处理是否需求授权凭证时,我这边是创立了2个不同的provider,用于创立有授权校验的session和无校验的session

public var netProvider = CustomProvider.createAuthProvider()
public var noAuthProvider = CustomProvider.createNoAuthProvider()
    /// 创立需认证的provider
    static func createAuthProvider() -> 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)
    }
    /// 创立无需认证的provider
    static func createNoAuthProvider() -> 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)
    }