布景

在网络数据交互中,为了识别用户的身份,一般会设置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)}

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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。