
AppAuth-iOS 拆解它如何让原生应用 OAuth 2.0 登录不泄露令牌【免费下载链接】AppAuth-iOSiOS and macOS SDK for communicating with OAuth 2.0 and OpenID Connect providers.项目地址: https://gitcode.com/gh_mirrors/ap/AppAuth-iOS为什么把登录页塞进 App 内的 WebView会让授权码被其他应用顺手截走这是 OpenID 基金会维护的 AppAuth-iOS 要消灭的问题。这个 OAuth 2.0 客户端 SDK 严格遵循 RFC 8252把认证界面交给系统浏览器并自动完成 PKCE 保护让你的令牌从授权、回调到刷新全程不脱离 App 掌控。 30 秒认识它先定位它是什么、给谁用项目信息维护组织OpenID 基金会OpenID Foundation支持平台iOS 12、macOS 10.12、tvOS 9设备授权流程核心定位原生应用 OAuth 2.0 与 OpenID Connect 客户端 SDK实现 RFC 8252适用场景授权码 PKCE 流程、令牌刷新管理、tvOS 设备码登录许可协议Apache 2.0当前版本2.1.0见 AppAuth.podspec它的源码分成三个 SwiftPM 产品见 Package.swift无 UI 依赖的AppAuthCore、带平台用户代理的AppAuth、以及专做设备授权流程的AppAuthTV。这个划分本身就透露了它的设计思路——协议逻辑和平台交互必须分家。 跟着一枚令牌走完登录流程OAuth 2.0 令牌的一生上面说了它「分家」那具体一枚令牌在这几家人之间如何流转我们按生命周期走一遍共五站。第一站发现。你只给出一个 issuer如https://accounts.google.comOIDAuthorizationService.discoverConfiguration会拉取 OpenID Connect Discovery 文档解析成OIDServiceConfiguration。授权端点、令牌端点全在这里SDK 拒绝让你手写 URL 拼协议。第二站授权。OIDAuthorizationRequest拿着配置、clientID、scopes 组装授权 URL。关键动作发生在这里它生成一个高熵随机串code_verifier并算出BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))作为code_challenge方法固定 S256见 OIDAuthorizationRequest.m 中对 RFC 7636 第 4.2 节的实现——PKCE 就像取件码出门时留一个哈希凭证在服务器回来时用原始串对暗号。随机熵由SecRandomCopyBytes提供OIDTokenUtilities.m。随后请求交给OIDExternalUserAgent协议iOS 上的实现OIDExternalUserAgentIOS用ASWebAuthenticationSession拉起系统认证界面用户登录后系统浏览器跳回你的 redirect URI。第三站回调。会话捕获到重定向 URL授权码code藏在 query 里交还给你的回调。注意verifier 从未离开你的 App 内存。第四站换令牌。这是OIDAuthState.authState(byPresenting:)的便利之处——它内部自动执行 code exchange从授权响应构造OIDTokenRequest带上原始code_verifier向令牌端点换出 access token 与 refresh token结果封装为OIDTokenResponse。第五站刷新。令牌到期前你不需要关心细节OIDAuthState在performActionWithFreshTokens:里统一判断过期、发起 refresh_token 刷新甚至会把刷新期间的并发请求排队去重。状态本身实现了NSSecureCoding序列化后你可以安全地存进钥匙串。令牌的一生到此闭环。但每个环节背后其实都有一个被明确否决的方案这才是值得拆解的部分。⚖️ 四个关键设计决策1. 认证界面归系统不归 App它选了ASWebAuthenticationSessioniOS/ 系统默认浏览器macOSREADME 明说UIWebView和WKWebView是explicitly not supported。为什么必须这么选。RFC 8252 第 8.12 节直接点名嵌入式 WebView 的问题它共享 App 的 cookie 域和 JS 执行环境恶意页面可读取会话拿不到系统 SSO 登录态用户每次重新输密码还有指纹识别风险。被放弃的 WebView 方案换来的是安全与体验双输所以作者干脆在 API 层面不给这条路留口子。你的经验。认证 UI 是「别人的界面」交给操作系统托管SSO、防钓鱼、隐私隔离全免费。2. PKCE 是默认行为不是可选项它选了每次授权请求都内置 S256 挑战。为什么必须这么选。原生应用是「公共客户端」没有服务端能保管 client_secret二进制可被反编译传统靠密钥保护的授权码流程天然失效。PKCE 用「verifier 只在换令牌时出现一次」补齐了这条安全链授权码被中间人截获也无法兑换。被放弃的 implicit 流程则把 access token 直接暴露在重定向 URL 里风险更高。你的经验。公共客户端永远上 PKCE如果你的 IdP 不支持它换 IdP 比换 SDK 更划算。3. OIDAuthState 是状态机不是令牌袋子它选了把授权响应、令牌响应、错误状态收敛进一个对象对外只暴露isAuthorized与performActionWithFreshTokens:。为什么必须这么选。若只给你一堆裸 token 字段各业务模块会各自写过期判断和刷新逻辑并发时重复刷新导致 refresh token 竞态失效部分 IdP 一次用后即吊销。它还在description里对 token 做 redact脱敏打印防止日志泄露。你的经验。令牌状态要单一归属谁持有状态机谁负责刷新业务方只拿「新鲜的令牌」。4. 平台差异用协议吸收不用 if-else它选了OIDExternalUserAgent协议抽象Core 层零 UI 依赖iOS、macOS 各自实现tvOS 干脆独立成 AppAuthTV 模块。为什么必须这么选。三个平台的回调机制根本不兼容iOS 靠 URI 回调macOS 起本地服务器tvOS 没有屏幕交互能力。塞进一个类只会长出条件编译的泥潭。分模块的代价是集成时多理解一个产品边界Extension 里只能引 AppAuthCore换来的是每层代码都能独立编译、独立测试。你的经验。跨平台 SDK 的切分线应该画在「机制不可共享」处而不是「UI 长得不一样」处。 iOS / macOS / tvOS 平台差异对照机制差异落到代码上是这样的维度iOSmacOStvOS授权界面ASWebAuthenticationSession系统默认浏览器无 UI走设备码回调方式自定义 URI Scheme / Universal Links本地Loopback HTTP 服务器127.0.0.1用户在手机/电脑输入设备码典型类OIDExternalUserAgentIOSOIDExternalUserAgentMacOIDLoopbackHTTPServerOIDTVAuthorizationService一句话原因移动端用跳转回 App 最自然桌面端 RFC 8252 推荐 loopback 回环避免 scheme 被抢注TV 无键盘无浏览器只能 RFC 8628 设备授权macOS 那个 loopback 服务器值得多看一眼它监听本地回环地址一次性接住浏览器回调后关闭本质是把「App 拦截 scheme」换成了「App 接 HTTP 请求」更安全也更符合桌面习惯。 iOS OAuth 集成步骤一步跑通授权流程决策看懂了下面是最小可跑通路径。假设你已把示例工程拉下来参考Examples/Example-iOS_Swift-Carthage/这里只列骨架。第 1 步装依赖。CocoaPods 里pod AppAuthSPM 则选AppAuth产品纯后台 Extension 场景选AppAuthCore。第 2 步声明回调 URI。这段 Info.plist 配置让系统把com.example.app:/...的跳转交还给你keyCFBundleURLTypes/key array dict keyCFBundleURLSchemes/key array stringcom.example.app/string /array /dict /array第 3 步发现配置。演示用 issuer 换取端点信息let issuer URL(string: https://accounts.google.com)! OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { config, error in guard let config config else { return } // config 内含授权端点、令牌端点 }第 4 步发起授权并自动换令牌。演示请求构造与 PKCE 自动生成let request OIDAuthorizationRequest( configuration: config, clientId: YOUR_CLIENT_ID, scopes: [OIDScopeOpenID, OIDScopeEmail], redirectURL: URL(string: com.example.app:/oauth2redirect)!, responseType: OIDResponseTypeCode) // codeVerifier 与 codeChallenge(S256) 已自动生成 OIDAuthState.authState(byPresenting: request, presenting: self) { state, error in // state.lastTokenResponse.accessToken }第 5 步用令牌。之后所有 API 请求都走这一个入口刷新被封装在内部authState.performAction { accessToken, idToken, error in guard let token accessToken, error nil else { return } // 携 token 请求业务接口 }别忘了把authState用NSSecureCoding序列化后存入钥匙串App 重启才能续上会话。✅ 令牌安全与集成踩坑自检清单跑通之前逐条过一遍redirect URI 与授权服务器侧注册的字符串完全一致scheme 路径差一个斜杠就会失败Info.plist 的CFBundleURLSchemes已配置且 SceneDelegate / openURL 回调已处理公共客户端请求中未携带client_secret调试时核对过授权 URL 里的code_challenge与 S256 方法确实存在authState存钥匙串而非 UserDefaults明文 token 不进普通文件所有取令牌动作都走performActionWithFreshTokens:没有业务代码私自刷新刷新失败返回invalid_grant时清除状态并重新授权而不是循环重试若走 Universal LinksAASA 文件与 Associated Domains entitlement 已验证生效 它适合谁谁该选别家它适合iOS/macOS/tvOS 原生应用接入任何遵循 RFC 8252 的 IdPGoogle、Okta、Auth0、Azure AD 均可需要标准的授权码 PKCE 流程和可审计的令牌生命周期。不适合两类场景跨端非 Apple 项目Android 请选对应生态的 AppAuth 系 SDK需要 confidential 客户端语义的服务端/CLI 场景这里的一切设计都建立在「密钥守不住」这个前提下。此外如果你的业务硬性要求登录页必须渲染在 App 内部深度定制 UI、复用站内登录态这个 SDK 在 API 层面就不提供这条路换方案比硬改源码现实。一句话收束AppAuth-iOS 的本质不是「帮你发 HTTP 请求」而是把 RFC 8252 的安全约束固化进类型系统里——让你想写错都得多绕几步。核心文件路径参考Sources/AppAuthCore/OIDAuthState.h—— 认证状态机与刷新逻辑Sources/AppAuthCore/OIDAuthorizationRequest.m—— 授权请求组装与 PKCE 生成Sources/AppAuthCore/OIDServiceDiscovery.h—— Discovery 文档解析Sources/AppAuth/iOS/OIDExternalUserAgentIOS.m—— iOS 系统认证会话实现Sources/AppAuth/macOS/LoopbackHTTPServer/OIDLoopbackHTTPServer.h—— macOS 回环服务器Sources/AppAuthTV/OIDTVAuthorizationService.h—— tvOS 设备授权流程Examples/Example-iOS_Swift-Carthage/与Examples/Example-iOS_ObjC/—— Swift / Objective-C 完整示例Package.swift、AppAuth.podspec—— 模块划分与版本依据【免费下载链接】AppAuth-iOSiOS and macOS SDK for communicating with OAuth 2.0 and OpenID Connect providers.项目地址: https://gitcode.com/gh_mirrors/ap/AppAuth-iOS创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考