咱们在之前分析拦截器的文章中提到,Alamofire
中实现了一些比较常用的拦截器。AuthenticationInterceptor
肯定是满分(我打的分)实现之一。今日一同来拜读一下。
和
AuthenticationInterceptor
相似的还有RetryPolicy
,也可谓精辟。详细内容放在下篇展开,敬请期待。
面临的问题
在实践的项目中咱们经常遇到的问题是:部分API是需求授权之后才能够拜访。例如:咱们获取用户信息的接口api.xx.com/users/id
,需求在恳求头中增加Authorization: Bearer accessToken
以完成授权,不然服务器会回来401
回绝咱们拜访。这个accessToken
会有过期时刻,过期后咱们需求从头获取,一般是经过登陆接口回来。后来为了削减用户登录频率,和accessToken
一同回来的还有refreshToken
,它的有用期会比accessToken
稍长,能够运用它来对accessToken
进行改写,就能够避免用户登录操作。
这儿触及
OAuth2.0
以及JWT
相关布景知识,不了解的同学自行解决哈。
那么关于上面的需求,咱们客户端需求做的有哪些呢?详细如下:
- 获取
accessToken
和refreshToken
- 在后续需求授权的接口中增加恳求头
-
accessToken
过期后,运用refreshToken
进行改写 - 改写
accessToken
失利时,需求用户登录从头授权。
那么Alamofire
为咱们做了哪些?继续看
怎么解决
首要,咱们能够界说一个自己的凭据(也便是后续需求用到的认证信息):
struct OAuthCredential: AuthenticationCredential {
let accessToken: String
let refreshToken: String
let userID: String
let expiration: Date
// 这儿咱们在有用期行将过期的5分钟回来需求改写
var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}
其次,咱们再实现一个自己的授权中心:
class OAuthAuthenticator: Authenticator {
/// 增加header
func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
}
/// 实现改写流程
func refresh(_ credential: OAuthCredential,
for session: Session,
completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
}
func didRequest(_ urlRequest: URLRequest,
with response: HTTPURLResponse,
failDueToAuthenticationError error: Error) -> Bool {
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
}
}
之后,咱们就能够运用结构内部的AuthenticationInterceptor
了:
// 生成授权凭据。用户没有登陆时,能够不生成。
let credential = OAuthCredential(accessToken: "a0",
refreshToken: "r0",
userID: "u0",
expiration: Date(timeIntervalSinceNow: 60 * 60))
// 生成授权中心
let authenticator = OAuthAuthenticator()
// 运用授权中心和凭据(若没有能够不传)装备拦截器
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
credential: credential)
// 将拦截器装备在Session上或在单独的Request中运用
let session = Session()
let urlRequest = URLRequest(url: URL(string: "https://api.example.com/example/user")!)
session.request(urlRequest, interceptor: interceptor)
能够看到,运用上面的方法,咱们只需关心怎么获取accessToken
和refreshToken
,以及在refreshToken
也失效时触发用户从头登录授权。能够说,咱们自己的工作少到了极致。自己写的少就意味这bug少,特别是改写token这一块,什么时候应该改写accessToken
、怎么控制过度改写这些繁琐的部分咱们都无需关心了。
怎么做到的
知道了怎么做,或许你还会一头雾水。为什么需求界说那两个数据结构?这一部分为你解答。
AuthenticationCredential
它代表授权凭据,这个协议的界说很简单:
/// 授权凭据,能够运用它对URLRequest进行授权。
/// 例如:在OAuth2授权系统中,凭据包括accessToken,它能够对一个用户的一切恳求进行授权。
/// 通常状况下,该accessToken有用时长为60分钟;在过期前后(一段时刻内)能够运用refreshToken对accessToken进行改写。
public protocol AuthenticationCredential {
/// 授权凭据是否需求改写。
/// 在凭据在行将过期或过期后,应该回来true。
/// 例如,accessToken的有用期为60分钟,在凭据行将过期的5分钟应该回来true,保证accessToken得到改写。
var requiresRefresh: Bool { get }
}
该协议只关心这个凭据是否需求改写。关于不同的授权方法,需求的元信息也不相同,结构无法也无需知道这些细节。
Authenticator
正因为AuthenticationCredential
或许五花八门,这儿需求一个知道怎么运用它的角色。Authenticator
就来了。该协议的实现细节比较多,我现已写在注释里了。
/// 授权中心,能够运用凭据(AuthenticationCredential)对URLRequest授权;也能够办理token的改写。
public protocol Authenticator: AnyObject {
/// 该授权中心运用的凭据类型
associatedtype Credential: AuthenticationCredential
/// 运用凭据对恳求进行授权。
/// 例如:在OAuth2系统中,应该设置恳求头 [ "Authorization": "Bearer accessToken" ]
func apply(_ credential: Credential, to urlRequest: inout URLRequest)
/// 改写凭据,并经过completion回调成果。
/// 在下面两种状况下,会履行改写:
/// 1. 适配过程中 - 对应 拦截器的 adapt(_:for:completion:) 方法
/// 2. 重试过程中 - 对应拦截器的 retry(_:for:dueTo:completion:)方法
///
/// 例如:在OAuth2系统中,应该在该方法中运用refreshToken去改写accessToken,完成后在回调中回来新的凭据。
/// 若改写恳求被回绝(状况码401),refreshToken不该该再运用,此时应该要求用户从头授权。
func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
/// 判别URLRequest失利是否因为授权问题。
/// 若授权服务器不支持对现已收效的凭据进行吊销(也便是说凭据永久有用)应该回来false。不然应该根据详细状况判别。
/// 例如:在OAuth2系统中, 能够运用状况码401代表授权失利,此时应该回来true。
/// 注意:上面只是一般状况,你应该根据你所处的系统详细判别。
func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
/// 判别URLRequest是否运用凭据进行了授权。
/// 若授权服务器不支持对现已收效的凭据进行吊销(也便是说凭据永久有用)应该回来true。不然应该根据详细状况判别。
/// 例如:在OAuth2系统中, 能够比照`URLRequest中header的授权字段Authorization的值` 和 `Credential中的token`;
/// 若他们相等,回来true,不然回来false
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
}
AuthenticationInterceptor
为了完成授权流程,该拦截器对恳求的适配和重试都进行了实现。
Adapter
先上一个适配流程图:
下面是相关代码,我现已加上了详细注释:
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
let adaptResult: AdaptResult = $mutableState.write { mutableState in
// 适配一个URLRequest时,正在改写凭据,将此次适配记录下来,推迟履行
guard !mutableState.isRefreshing else {
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
mutableState.adaptOperations.append(operation)
return .adaptDeferred
}
// 没有授权凭据时,报错
guard let credential = mutableState.credential else {
let error = AuthenticationError.missingCredential
return .doNotAdapt(error)
}
// 若凭据需求改写,将此次适配记录下来,推迟履行。并触发改写操作
guard !credential.requiresRefresh else {
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
mutableState.adaptOperations.append(operation)
refresh(credential, for: session, insideLock: &mutableState)
return .adaptDeferred
}
// 上面的状况都没有触发,则需求进行适配
return .adapt(credential)
}
switch adaptResult {
case let .adapt(credential):
// 运用授权中心进行授权,之后回调
var authenticatedRequest = urlRequest
authenticator.apply(credential, to: &authenticatedRequest)
completion(.success(authenticatedRequest))
case let .doNotAdapt(adaptError):
// 出错了就直接回调过错
completion(.failure(adaptError))
case .adaptDeferred:
// 凭据需求改写或正在改写, 适配需求推迟到改写完成后履行
break
}
}
其间的改写流程,比就有意思。触及到改写窗口的概念。简单讲便是必定的时刻范围。在这个范围内,还能够设置一个最大的改写次数。在正式改写之前,会判别改写条件是否满足窗口设定。详细如下:
/// 判别是否过度改写
private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
// refreshWindow是判别过度改写的参考,没有refreshWindow时阐明不限制改写
guard let refreshWindow = mutableState.refreshWindow else { return false }
// 计算可改写的时刻点
let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
// 统计在可改写时刻点之前的改写次数
let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
guard refreshWindowMin <= refreshTimestamp else { return }
attempts += 1
}
// 若改写次数 大于等于 装备的最大答应改写次数,认为过度改写
let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts
return isRefreshExcessive
}
若上述条件经过,就会履行改写:
private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) {
// 若过度改写,直接报错
guard !isRefreshExcessive(insideLock: &mutableState) else {
let error = AuthenticationError.excessiveRefresh
handleRefreshFailure(error, insideLock: &mutableState)
return
}
// 记录改写时刻,设置改写标志
mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
mutableState.isRefreshing = true
queue.async {
// 运用授权中心进行改写。这儿便是咱们自己实现的授权中心。
self.authenticator.refresh(credential, for: session) { result in
self.$mutableState.write { mutableState in
switch result {
case let .success(credential):
self.handleRefreshSuccess(credential, insideLock: &mutableState)
case let .failure(error):
self.handleRefreshFailure(error, insideLock: &mutableState)
}
}
}
}
}
Retrier
还是先看流程图:
这儿会判别是否和授权有关,无关的就不会重试。另外,若当时最新凭据没有运用,会进入重试流程。最后的改写是因为:已然需求授权,也存在凭据,也授权过了,还进入了重试那就阐明凭据过期了。下面是详细代码:
public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
// 没有原始恳求或没有收到服务器的呼应,无需重试
guard let urlRequest = request.request, let response = request.response else {
completion(.doNotRetry)
return
}
// 不是因为授权原因失利的,无需重试
guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
completion(.doNotRetry)
return
}
// 需求授权,却没有凭据的,回调过错
guard let credential = credential else {
let error = AuthenticationError.missingCredential
completion(.doNotRetryWithError(error))
return
}
// 需求授权,但未运用当时凭据,需求重试
guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
completion(.retry)
return
}
// 需求授权,存在凭据,也授权过了,还进入了重试那就阐明凭据过期了,改写凭据
$mutableState.write { mutableState in
mutableState.requestsToRetry.append(completion)
guard !mutableState.isRefreshing else { return }
refresh(credential, for: session, insideLock: &mutableState)
}
}
到这儿,整个流程也就清晰了。更详细的,能够参考GitHub
总结
今日咱们从详细问题出发,先了解了怎么运用Alamofire
去解决该问题,然后又分析了AuthenticationInterceptor
的详细实现,它是怎么解决该问题的。最后,只能说Alamofire
真是太细了。