使用RxSwift处理Refresh Token问题

3,481 阅读3分钟

背景

在网络数据交互中,为了识别用户的身份,一般会设置Token来处理与用户相关的业务逻辑;然而为了确保用户的信息安全,或是单点登录,那么在使用Token的时候会设置有效时间;当Token时间过期时,业务接口会抛出指定的错误码,基于该错误码来进行Token刷新及相关逻辑。

当多个接口同时需要刷新Token时,容易造成Token重复刷新,或Token边刷新边失效的情况,鉴于此种情况,特介绍如下解决方案;

分析

首先分析可能引发这一问题出现的具体情形,比如 R1R2R3三个请求都是需要Token的接口,而此时Token已失效,那么请求这三个接口必然会失败进入Token刷新逻辑,需要处理的情形大致分为如下两种:

  1. R1正在刷新Token中,而R2R3准备发起请求;
  2. R1准备刷新Token时,而R2R3也准备刷新Token

准备工作

模拟判断Token是否失效:

var tokenable: Bool {
    get {
        UserDefaults.standard.bool(forKey: "token")
    }
    set {
        UserDefaults.standard.set(newValue, forKey: "token")
    }
}

模拟请求Token的刷新:

func _request() -> Single<Bool> {
        return Single.create { [weak self] observer in
            let item = DispatchWorkItem {
                print("Token刷新完毕")
                tokenable = true
                observer(.success(true))
            }
            print("正在刷新Token...")
            self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)

            return Disposables.create {
                item.cancel()
            }
        }
}

模拟请求业务接口:

func _request(url: String) -> Single<String> {
        return Single.create { [weak self] observer in

            let item = DispatchWorkItem {
                if tokenable {
                    print("数据请求完毕:\(url)")
                    observer(.success(url))
                } else {
                    print("Token过期:\(url)")
                    observer(.success(""))
                }
            }

            print("正在请求数据:\(url)")
            self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)

            return Disposables.create {
                item.cancel()
            }
        }
}

解决方案

基于第一种情况的解决办法,标记Token的刷新状态,当R1正在刷新时,R2R3的请求先挂起,直到R1Token刷新完成后再继续R2R3的请求:

定义Token的刷新状态:

struct Token {
    static let shared = Token()

    /// 是否正在刷新
    let refreshStatus = BehaviorRelay(value: false)
}

封装业务接口,内部处理Token刷新逻辑,当Token正在刷新时,其他请求挂起(抛弃当前事件,由于订阅关系还存在,Token状态的改变会再次触发事件):

func request(url: String) -> Single<String> {
        return Token.shared.refreshStatus
            // 确保状态发生改变时触发
            .distinctUntilChanged()
            // 正在刷新时,丢弃此次请求
            // 订阅关系还存在,可在下次状态变更时继续请求
            .filter { !$0 }
            // 转换为Single,保证完成请求后失去订阅关系
            .first()
            // 请求业务接口
            .flatMap { _ in
                self._request(url: url)
            }
            ...
}

根据业务的返回结果判断是否需要刷新Token,并设置Token的刷新状态(用返回数据为空模拟Token失效的情况):

func request(url: String) -> Single<String> {
        ...
            // 请求业务接口
            .flatMap { _ in
                self._request(url: url)
            }
            // 接口数据处理
            .flatMap { data -> Single<String> in
                // 错误处理:Token过期
                if data.isEmpty {

                    Token.shared.refreshStatus.accept(true)

                    return self._request()
                            .flatMap { _ in

                                Token.shared.refreshStatus.accept(false)

                                return self._request(url: url)
                            }
                }

                // 正确处理:结果透传
                return Single<String>.just(data)
            }
    }

此时,第一种情况已解决,当R1正在刷新Token时,其他请求暂时挂起,只有Token状态发生改变,且只有未刷新时继续请求;

然而,第二种情况并未解决,等同于R1R2R3同时发起请求,此时三个请求获得到的Token状态都是未刷新的,所以都会进入刷新Token的逻辑中,造成重复刷新Token或边刷新边过期的情况;

解决思路也很简单,当第一个请求准备刷新Token时,其他请求要等待前者刷新Token完毕,由于三个请求可能归属于不同线程,涉及到Token刷新状态的资源争夺,所以可以增加一个线程锁来解决此问题:

func request(url: String) -> Single<String> {
        ...
            // 接口数据处理
            .flatMap { data -> Single<String> in
                // 错误处理:Token过期
                if data.isEmpty {
                    // 将Token刷新状态和刷新逻辑加锁
                    defer { self.lock.unlock() }
                    self.lock.lock()

                    // 没有刷新,则开始刷新
                    if !Token.shared.refreshStatus.value {
                        Token.shared.refreshStatus.accept(true)
                        return self._request()
                            .flatMap { _ in
                                Token.shared.refreshStatus.accept(false)
                                return self._request(url: url)
                            }
                    }
                    // 注意!!!此处是递归哦
                    return self.request(url: url)
                }

                // 正确处理:结果透传
                return Single<String>.just(data)
            }
    }
}

如上所示,在Token过期的处理逻辑中加入线程锁,保证同一时刻内仅有一个线程可以刷新Token,并将Token状态置为正在刷新,但是线程锁不能保证Token刷新完毕,所以如上有个递归,此处鸣谢大「明顺」,关键时刻点醒了我!注意是递归哦!

重点:此时的逻辑,总结为R1争取到线程锁,进入刷新Token逻辑,重置Token刷新状态为True,而R2R3进入递归后挂起(原理同第一种情况,Token正在刷新),之后R1刷新Token完毕后,重置Token刷新状态为False,恢复R2R3的请求;

至此结束,完成整个刷新流程!

以下为完整代码:

struct Token {
    /// 保证单次刷新
    static let lock = NSRecursiveLock()
    /// 是否正在刷新
    static let refreshStatus = BehaviorRelay(value: false)
}

class LogicService {
    static let shared = LogicService()

    let taskQueue = DispatchQueue(label: "logic", attributes: .concurrent)

    func request(url: String) -> Single<String> {
        return Token.refreshStatus
            .distinctUntilChanged()
            // 正在刷新的等待,丢弃信号
            .filter { !$0 }
            .first()
            // 请求业务接口
            .flatMap { _ in
                self._request(url: url)
            }
            // 接口数据处理
            .flatMap { data -> Single<String> in

                // 错误处理:Token过期
                // 需要刷新
                if data.isEmpty {

                    defer {
                        print("\(url) 解锁")
                        Token.lock.unlock()
                    }
                    print("\(url) 加锁")
                    Token.lock.lock()

                    // 没有刷新,则开始刷新
                    if !Token.refreshStatus.value {
                        print("\(url) 准备刷新Token")
                        Token.refreshStatus.accept(true)
                        return self._request(tag: url)
                            .flatMap { _ in
                                print("\(url) 刷新Token完毕")
                                Token.refreshStatus.accept(false)
                                return self._request(url: url)
                            }
                    }
                    print("\(url) 未刷新Token,请求业务接口")
                    return self.request(url: url)
                }

                // 正确处理:结果透传
                return Single<String>.just(data)
            }
    }
}

extension LogicService {

    func _request(tag: String) -> Single<Bool> {
        return Single.create { [weak self] observer in
            let item = DispatchWorkItem {
                print("response: \(tag) Token刷新完毕")
                tokenable = true
                observer(.success(true))
            }
            print("request: \(tag) 正在刷新Token...")
            self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)

            return Disposables.create {
                item.cancel()
            }
        }
    }

    func _request(url: String) -> Single<String> {
        return Single.create { [weak self] observer in

            let item = DispatchWorkItem {
                if tokenable {
                    print("response: 数据请求完毕:\(url)")
                    observer(.success(url))
                } else {
                    print("response:Token过期:\(url)")
                    observer(.success(""))
                }
            }

            print("request: 正在请求数据:\(url)")
            self?.taskQueue.asyncAfter(deadline: .now() + 1, execute: item)

            return Disposables.create {
                item.cancel()
            }
        }
    }
}

var tokenable: Bool {
    get {
        UserDefaults.standard.bool(forKey: "token")
    }
    set {
        UserDefaults.standard.set(newValue, forKey: "token")
    }
}

以下为R1R2R3同时请求时的打印顺序:

request: 正在请求数据:R1
request: 正在请求数据:R2
request: 正在请求数据:R3
response:Token过期:R1
R1 加锁
R1 准备刷新Token
R1 解锁
request: R1 正在刷新Token...
response:Token过期:R2
R2 加锁
R2 未刷新Token,请求业务接口
R2 解锁
response:Token过期:R3
R3 加锁
R3 未刷新Token,请求业务接口
R3 解锁
response: R1 Token刷新完毕
R1 刷新Token完毕
request: 正在请求数据:R3
request: 正在请求数据:R2
request: 正在请求数据:R1
response: 数据请求完毕:R3
request token: R3 <NSThread: 0x60000331bfc0>{number = 8, name = (null)}
response: 数据请求完毕:R2
response: 数据请求完毕:R1
request token: R1 <NSThread: 0x6000033f5d40>{number = 7, name = (null)}
request token: R2 <NSThread: 0x60000331bfc0>{number = 8, name = (null)}

感谢我大团队,以上解决方案是大家一起努力的成果,前人栽树后人乘凉,而我只是其中一个受益者。欢迎斧正!!