From ed61f49ecab110a3bc2a8d113ce0921779733071 Mon Sep 17 00:00:00 2001 From: hext Date: Wed, 14 Sep 2022 00:14:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=97=B6=E5=A2=9E=E5=8A=A0=E4=BA=86simple=5Ftoken?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=AD=98=E4=B8=8B=E6=9D=A5=EF=BC=8C?= =?UTF-8?q?token=E8=BF=87=E6=9C=9F=E4=BB=A5=E5=90=8E=E8=B0=83=E7=94=A8=20/?= =?UTF-8?q?login/simple=5Ftoken=20=E9=87=8D=E6=96=B0=E8=8E=B7=E5=BE=97?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E8=AE=A4=E8=AF=81token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/PushDeer-iOS/Podfile.lock | 2 +- .../PushDeer.xcodeproj/project.pbxproj | 16 +++++ ios/PushDeer-iOS/PushDeer/Model/Result.swift | 7 ++ .../PushDeer/Service/AppState.swift | 29 ++++++++ .../PushDeer/Service/HttpRequest.swift | 66 ++++++++++++++++++- .../PushDeer/Service/PushDeerApi.swift | 16 +++++ .../PushDeer/Service/WXDelegate.swift | 3 + .../PushDeer/Tool/CommonUtils.swift | 26 ++++++++ .../PushDeer/View/LoginView.swift | 2 + .../PushDeer/View/SettingsView.swift | 2 + 10 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 ios/PushDeer-iOS/PushDeer/Tool/CommonUtils.swift diff --git a/ios/PushDeer-iOS/Podfile.lock b/ios/PushDeer-iOS/Podfile.lock index fe5d5e2..89a1b56 100644 --- a/ios/PushDeer-iOS/Podfile.lock +++ b/ios/PushDeer-iOS/Podfile.lock @@ -49,4 +49,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 42e3d8abd976589c1043ff9f9e864c275a490160 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj b/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj index a335ba2..a709f16 100644 --- a/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj +++ b/ios/PushDeer-iOS/PushDeer.xcodeproj/project.pbxproj @@ -97,6 +97,9 @@ 52EB90B32778DA4E0048E0ED /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EB90B22778DA4E0048E0ED /* Line.swift */; }; 52EED71E27C9394D0086A804 /* WXDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EED71D27C9394D0086A804 /* WXDelegate.swift */; }; 52EED71F27C93B960086A804 /* WXDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EED71D27C9394D0086A804 /* WXDelegate.swift */; }; + 52EF0AFC28CE081A00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; }; + 52EF0AFD28CE081A00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; }; + 52EF0AFE28CE08EC00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; }; 52F0243F277737470071D861 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0243E277737470071D861 /* LoginView.swift */; }; 52F2C223277961D7006F08DC /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F2C222277961D7006F08DC /* SettingsItemView.swift */; }; 52F40D2F277CA05600766C24 /* MessageItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F40D2E277CA05600766C24 /* MessageItemView.swift */; }; @@ -218,6 +221,7 @@ 52EB90AF2778D67F0048E0ED /* KeyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyItemView.swift; sourceTree = ""; }; 52EB90B22778DA4E0048E0ED /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; 52EED71D27C9394D0086A804 /* WXDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WXDelegate.swift; sourceTree = ""; }; + 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonUtils.swift; sourceTree = ""; }; 52F0243C277733CE0071D861 /* PushDeer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PushDeer.entitlements; sourceTree = ""; }; 52F0243E277737470071D861 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 52F2C222277961D7006F08DC /* SettingsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItemView.swift; sourceTree = ""; }; @@ -367,6 +371,7 @@ 527872C327CE0EE800AD79AD /* Info-SelfHosted.plist */, 52E317DA279305BB000B8BB1 /* Localizable.strings */, 52E317D7279305BB000B8BB1 /* InfoPlist.strings */, + 52EF0AFA28CE07A600C99E4F /* Tool */, 52450F362784822C003652D8 /* Service */, 52B8CF5D277DE5FF004CB680 /* Common */, 52450F3D27849228003652D8 /* Model */, @@ -447,6 +452,14 @@ path = Common; sourceTree = ""; }; + 52EF0AFA28CE07A600C99E4F /* Tool */ = { + isa = PBXGroup; + children = ( + 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */, + ); + path = Tool; + sourceTree = ""; + }; 52F0243D2777370F0071D861 /* View */ = { isa = PBXGroup; children = ( @@ -776,6 +789,7 @@ 5206009E27CF76BC00188431 /* PushDeerApi.swift in Sources */, 520600A627D0AE2300188431 /* Line.swift in Sources */, 5206009D27CF74C100188431 /* HttpRequest.swift in Sources */, + 52EF0AFE28CE08EC00C99E4F /* CommonUtils.swift in Sources */, 520600A127CF770600188431 /* Env.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -810,6 +824,7 @@ 52EED71E27C9394D0086A804 /* WXDelegate.swift in Sources */, 52EB90AE2778AFD60048E0ED /* BaseNavigationView.swift in Sources */, 524E99E627B3CD0F00292396 /* EndpointView.swift in Sources */, + 52EF0AFC28CE081A00C99E4F /* CommonUtils.swift in Sources */, 52EB90AC2778ADF80048E0ED /* CardView.swift in Sources */, 52EB90B02778D67F0048E0ED /* KeyItemView.swift in Sources */, 52AC5C2827B7FE1D00EEB185 /* ViewExtension.swift in Sources */, @@ -853,6 +868,7 @@ 52FBA09427874879003308C2 /* ContentView.swift in Sources */, 52B8CF85277E0C12004CB680 /* Line.swift in Sources */, 52AC5C2927B7FE1D00EEB185 /* ViewExtension.swift in Sources */, + 52EF0AFD28CE081A00C99E4F /* CommonUtils.swift in Sources */, 52450F402784923D003652D8 /* Result.swift in Sources */, 526A1E712791E00400BA2177 /* PushDeerData.xcdatamodeld in Sources */, 52450F432784943F003652D8 /* HttpRequest.swift in Sources */, diff --git a/ios/PushDeer-iOS/PushDeer/Model/Result.swift b/ios/PushDeer-iOS/PushDeer/Model/Result.swift index 6a2b994..61aad88 100644 --- a/ios/PushDeer-iOS/PushDeer/Model/Result.swift +++ b/ios/PushDeer-iOS/PushDeer/Model/Result.swift @@ -29,6 +29,7 @@ struct UserInfoContent: Codable{ let level: Int let created_at: String let updated_at: String + let simple_token: String? } struct DeviceItem: Codable, Identifiable{ @@ -78,6 +79,12 @@ struct ResultContent: Codable{ let result: Array } + +struct STokenContent: Codable{ + let stoken: String? +} + + let dateFormatter = DateFormatter() extension KeyItem { diff --git a/ios/PushDeer-iOS/PushDeer/Service/AppState.swift b/ios/PushDeer-iOS/PushDeer/Service/AppState.swift index 96e5dc2..0b50d19 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/AppState.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/AppState.swift @@ -125,6 +125,35 @@ class AppState: ObservableObject { } } + func loginAfter() { + Task { + // 查询 UserInfo + self.userInfo = try await HttpRequest.getUserInfo() + var stoken = self.userInfo?.simple_token + if isEmpty(stoken) { + // UserInfo 中的 stoken 为空, 就重新生成一个 + stoken = try await HttpRequest.stokenRegen().stoken + } + if isNotEmpty(stoken) { + // 最后 stoken 不为空, 就保存到本地 + self.saveSTokenToLocal(stoken: stoken) + } + } + } + + func saveSTokenToLocal(stoken: String?) -> Void { + let key = "stoken_\(self.api_endpoint)" + getUserDefaults().set(stoken, forKey: key) + } + func getLocalSToken() -> String? { + let key = "stoken_\(self.api_endpoint)" + return getUserDefaults().string(forKey: key) + } + func deleteLocalSToken() -> Void { + let key = "stoken_\(self.api_endpoint)" + getUserDefaults().removeObject(forKey: key) + } + func appleIdLogin(_ result: Result) async throws -> TokenContent { switch result { case let .success(authorization): diff --git a/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift b/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift index 1e91793..221ee10 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/HttpRequest.swift @@ -8,13 +8,63 @@ import Foundation import Moya +struct TokenAuthorizationPlugin: PluginType { + + func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { + if + let url = request.url, + var components = URLComponents(url: url, resolvingAgainstBaseURL: true), + let queryItems = components.queryItems + { + let queryItems_new = queryItems.map { item -> URLQueryItem in + if item.name == "token" { + // 把请求参数中的 token 都换成最新的 + return URLQueryItem(name: "token", value: AppState.shared.token) + } + return item + } + components.queryItems = queryItems_new + var request_mutable = request + request_mutable.url = components.url + return request_mutable + } + return request + } +} + @MainActor struct HttpRequest { - static let provider = MoyaProvider(callbackQueue: DispatchQueue.main) + static let provider = MoyaProvider(callbackQueue: DispatchQueue.main, plugins: [TokenAuthorizationPlugin()]) /// 统一处理接口请求, 并且封装成 Swift Concurrency 模式 (async / await) static func request(_ targetType: PushDeerApi, resultType: T.Type) async throws -> T { + do { + return try await _request(targetType, resultType: resultType) + } catch { + if (error as NSError).code == 80403 { + // token 失效处理 + let stoken = AppState.shared.getLocalSToken() // 取本地 stoken + if isNotEmpty(stoken) { + let token = try? await stokenLogin(stoken: stoken!).token // 用 stoken 登录换 token + if isNotEmpty(token) { + AppState.shared.token = token! // 更新 token + do { + return try await _request(targetType, resultType: resultType) // 用新 token 再次请求原接口 + } catch { + throw error // 再次请求报错 + } + } + AppState.shared.deleteLocalSToken() // 到这来说明 stoken 已经失效, 删掉 stoken + } + // 退出登录 + AppState.shared.token = "" // 到这来 说明 stoken 不存在 或 已经失效, 清空 token 使其切换到登录界面 + } + throw error // 抛出原请求的 error (stoken流程失败, 或者不是token失效的错误, 会到这儿来) + } + } + + static func _request(_ targetType: PushDeerApi, resultType: T.Type) async throws -> T { return try await withCheckedThrowingContinuation { continuation in provider.request(targetType) { result in switch result { @@ -31,7 +81,6 @@ struct HttpRequest { if let content = result.content, result.code == 0 { continuation.resume(returning: content) } else if result.code == 80403 { - AppState.shared.token = "" continuation.resume(throwing: NSError(domain: result.error ?? NSLocalizedString("登录过期", comment: "token失效时提示"), code: result.code, userInfo: [NSLocalizedDescriptionKey: result.error ?? NSLocalizedString("登录过期", comment: "token失效时提示")])) } else { continuation.resume(throwing: NSError(domain: result.error ?? NSLocalizedString("接口报错", comment: "接口报错时提示"), code: result.code, userInfo: [NSLocalizedDescriptionKey: result.error ?? NSLocalizedString("接口报错", comment: "接口报错时提示")])) @@ -139,4 +188,17 @@ struct HttpRequest { static func rmAllMessage() async throws -> ActionContent { return try await request(.rmAllMessage(token: AppState.shared.token), resultType: ActionContent.self) } + + + static func stokenLogin(stoken: String) async throws -> TokenContent { + return try await request(.stokenLogin(stoken: stoken), resultType: TokenContent.self) + } + + static func stokenRegen() async throws -> STokenContent { + return try await request(.stokenRegen(token: AppState.shared.token), resultType: STokenContent.self) + } + + static func stokenRemove() async throws -> STokenContent { + return try await request(.stokenRemove(token: AppState.shared.token), resultType: STokenContent.self) + } } diff --git a/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift b/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift index 652f6b4..8856f04 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/PushDeerApi.swift @@ -42,6 +42,9 @@ enum PushDeerApi { case rmMessage(token: String, id: Int) case rmAllMessage(token: String) + case stokenLogin(stoken: String) + case stokenRegen(token: String) + case stokenRemove(token: String) } extension PushDeerApi: TargetType { @@ -94,6 +97,13 @@ extension PushDeerApi: TargetType { return "/message/remove" case .rmAllMessage: return "/message/clean" + + case .stokenLogin: + return "/login/simple_token" + case .stokenRegen: + return "/simple_token/regen" + case .stokenRemove: + return "/simple_token/remove" } } var method: Moya.Method { @@ -145,6 +155,12 @@ extension PushDeerApi: TargetType { case let .rmAllMessage(token): return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString) + case let .stokenLogin(stoken): + return .requestParameters(parameters: ["stoken": stoken],encoding: URLEncoding.queryString) + case let .stokenRegen(token): + return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString) + case let .stokenRemove(token): + return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString) } } var headers: [String: String]? { diff --git a/ios/PushDeer-iOS/PushDeer/Service/WXDelegate.swift b/ios/PushDeer-iOS/PushDeer/Service/WXDelegate.swift index 1d5564f..428863a 100644 --- a/ios/PushDeer-iOS/PushDeer/Service/WXDelegate.swift +++ b/ios/PushDeer-iOS/PushDeer/Service/WXDelegate.swift @@ -30,6 +30,9 @@ class WXDelegate: NSObject, WXApiDelegate { if state == "login" { AppState.shared.token = try await HttpRequest.wechatLogin(code: code).token // 给 AppState 的 token 赋值后, SwiftUI 写的 ContentView 页面会监听到并自动进入主页 + // 登录成功后的处理 + AppState.shared.loginAfter() + } else if state == "bind" { _ = try await HttpRequest.mergeUser(type: "wechat", tokenorcode: code) // 合并成功, 更新数据 diff --git a/ios/PushDeer-iOS/PushDeer/Tool/CommonUtils.swift b/ios/PushDeer-iOS/PushDeer/Tool/CommonUtils.swift new file mode 100644 index 0000000..02148e6 --- /dev/null +++ b/ios/PushDeer-iOS/PushDeer/Tool/CommonUtils.swift @@ -0,0 +1,26 @@ +// +// CommonUtils.swift +// PushDeer +// +// Created by HEXT on 2022/9/11. +// + +import Foundation + +/// 判断一个集合 **为空** +/// - Parameter emptiable: 一个可能为空的集合, 需要实现了 Collection 协议, 如: String / Array / Dictionary / Set / Data 等 +/// - Returns: nil 或 空 为 true +func isEmpty(_ emptiable: T?) -> Bool { + if emptiable == nil || emptiable!.isEmpty { + return true + } else { + return false + } +} + +/// 判断一个集合 **不为空** +/// - Parameter emptiable: 一个可能为空的集合, 需要实现了 Collection 协议, 如: String / Array / Dictionary / Set / Data 等 +/// - Returns: nil 或 空 为 false +func isNotEmpty(_ emptiable: T?) -> Bool { + return !isEmpty(emptiable) +} diff --git a/ios/PushDeer-iOS/PushDeer/View/LoginView.swift b/ios/PushDeer-iOS/PushDeer/View/LoginView.swift index b23f2b6..81649ce 100644 --- a/ios/PushDeer-iOS/PushDeer/View/LoginView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/LoginView.swift @@ -41,6 +41,8 @@ struct LoginView: View { showLoading = true store.token = try await store.appleIdLogin(result).token // 获取成功去主页 + // 登录成功后的处理 + store.loginAfter() } catch { showLoading = false if (error as NSError).code == 1001 { diff --git a/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift b/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift index 1c3b0ea..e648ffb 100644 --- a/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift +++ b/ios/PushDeer-iOS/PushDeer/View/SettingsView.swift @@ -21,6 +21,7 @@ struct SettingsView: View { VStack { SettingsItemView(title: NSLocalizedString("登录为", comment: "") + " " + userName(), button: NSLocalizedString("退出", comment: "退出登录按钮上的文字")) { store.token = "" + store.deleteLocalSToken() HToast.showText(NSLocalizedString("退出", comment: "退出登录按钮上的文字")) } .padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20)) @@ -37,6 +38,7 @@ struct SettingsView: View { SettingsItemView(title: NSLocalizedString("API endpoint", comment: ""), button: NSLocalizedString("重置", comment: "")) { store.api_endpoint = "" store.token = "" + store.deleteLocalSToken() } .padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20)) }