You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 18:10:35