最近使用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
,否则无法触发AuthenticationInterceptor
的Retry
方法!
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)
}