如何在SwiftUI中无需封装旧库实现OAuth2认证?
在SwiftUI中使用AppAuth实现Spotify OAuth2认证(无需Storyboard)
核心问题解决思路
你不需要手动封装所有UI Delegate和协议类,通过获取当前顶层视图控制器+用ObservableObject封装认证逻辑,就能在纯SwiftUI环境下完成OAuth2流程,同时实现令牌的持久化管理。
步骤1:获取当前顶层UIViewController
SwiftUI视图本身不是UIViewController,需要先获取当前活跃的顶层VC,作为presenting参数传入AppAuth的认证方法:
extension UIApplication { func topViewController() -> UIViewController? { let keyWindow = connectedScenes .filter({$0.activationState == .foregroundActive}) .map({$0 as? UIWindowScene}) .compactMap({$0}) .first?.windows .filter({$0.isKeyWindow}).first var topVC = keyWindow?.rootViewController while let presentedVC = topVC?.presentedViewController { topVC = presentedVC } return topVC } }
步骤2:封装认证逻辑到ObservableObject
创建AuthManager类,统一处理认证请求、状态存储、令牌刷新,完全隔离SwiftUI视图和AppAuth的底层逻辑:
import AppAuth class AuthManager: ObservableObject { @Published var isAuthenticated = false private var authState: OIDAuthState? private let spotifyClientID = "你的Spotify客户端ID" private let spotifyRedirectURI = URL(string: "你的重定向URI")! private let spotifyIssuer = URL(string: "https://accounts.spotify.com")! init() { // 从本地恢复认证状态(示例用UserDefaults,生产环境建议用Keychain) if let stateData = UserDefaults.standard.data(forKey: "SpotifyAuthState"), let savedState = try? NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: stateData) { self.authState = savedState self.isAuthenticated = true } } func startSpotifyAuth() { guard let topVC = UIApplication.shared.topViewController() else { return } // 发现Spotify的OAuth配置 OIDAuthorizationService.discoverConfiguration(forIssuer: spotifyIssuer) { config, error in guard let config = config else { print("获取OAuth配置失败: \(error?.localizedDescription ?? "未知错误")") return } // 构建认证请求(申请获取用户喜爱歌曲的权限) let authRequest = OIDAuthorizationRequest( configuration: config, clientId: self.spotifyClientID, scopes: ["user-library-read"], redirectURL: self.spotifyRedirectURI, responseType: OIDResponseTypeCode, additionalParameters: nil ) // 发起认证流程 let authFlow = OIDAuthState.authState(byPresenting: authRequest, presenting: topVC) { state, error in if let validState = state { self.authState = validState self.isAuthenticated = true // 保存认证状态到本地 let stateData = try? NSKeyedArchiver.archivedData(withRootObject: validState, requiringSecureCoding: false) UserDefaults.standard.set(stateData, forKey: "SpotifyAuthState") print("认证成功,AccessToken: \(validState.lastTokenResponse?.accessToken ?? "")") } else { print("认证失败: \(error?.localizedDescription ?? "未知错误")") } } // 保留流程引用,避免被提前释放 (UIApplication.shared.delegate as? AppDelegate)?.currentAuthorizationFlow = authFlow } } func refreshTokenIfNeeded(completion: @escaping (String?) -> Void) { guard let state = authState else { completion(nil) return } if state.needsTokenRefresh { state.performAction(freshTokens: { accessToken, _, error in if let token = accessToken, error == nil { // 刷新成功,更新本地存储 let stateData = try? NSKeyedArchiver.archivedData(withRootObject: state, requiringSecureCoding: false) UserDefaults.standard.set(stateData, forKey: "SpotifyAuthState") completion(token) } else { print("令牌刷新失败: \(error?.localizedDescription ?? "")") completion(nil) } }) } else { completion(state.lastTokenResponse?.accessToken) } } }
步骤3:配置AppDelegate
在AppDelegate中添加currentAuthorizationFlow属性,用于保留AppAuth的流程引用,并处理重定向回调:
class AppDelegate: UIResponder, UIApplicationDelegate { var currentAuthorizationFlow: OIDAuthorizationFlowSession? func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // 处理重定向URI,完成认证流程 if let authFlow = currentAuthorizationFlow, authFlow.resumeAuthorizationFlow(with: url) { currentAuthorizationFlow = nil return true } return false } // ...其他默认方法 }
步骤4:在SwiftUI视图中使用
通过@StateObject注入AuthManager,实现登录状态的响应式展示和操作:
struct SpotifyAuthView: View { @StateObject private var authManager = AuthManager() var body: some View { VStack(spacing: 20) { if authManager.isAuthenticated { Text("已登录Spotify") .font(.title) Button("获取我的喜爱歌曲") { authManager.refreshTokenIfNeeded { token in guard let token = token else { return } self.fetchLikedSongs(with: token) } } } else { Button("登录Spotify") { authManager.startSpotifyAuth() } .font(.title) .padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) } } .padding() } private func fetchLikedSongs(with token: String) { let url = URL(string: "https://api.spotify.com/v1/me/tracks")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") URLSession.shared.dataTask(with: request) { data, _, error in if let data = data { // 解析并处理返回的歌曲数据 print(String(data: data, encoding: .utf8) ?? "") } }.resume() } }
关键注意事项
- 令牌安全存储:示例中用
UserDefaults仅做演示,实际生产环境必须用Keychain存储OIDAuthState,避免敏感数据泄露。 - 重定向URI配置:确保Spotify开发者后台的重定向URI和代码中一致,并且在
Info.plist中配置对应的CFBundleURLTypes。 - 权限申请:根据需求调整
scopes参数,Spotify的权限列表可参考官方文档。
内容的提问来源于stack exchange,提问作者pedzer
相关产品推荐
相关产品推荐

