Universal Links校验不通过?AASA文件与Associated Domains全链路排查指南

发布时间:2026/9/16 17:41:07
Universal Links校验不通过?AASA文件与Associated Domains全链路排查指南 如果你正在被 Universal Links 校验不通过这个问题卡住那这篇文章应该能帮你把链路从头到尾捋一遍。Universal Links 本身不是新东西iOS 9 就有了但它横跨苹果开发者后台、Xcode 签名配置、服务器部署、HTTPS 证书、CDN 缓存这么多环节任何一个地方出问题外在表现都一样Safari 打开链接时直接跳到网页App 没有被唤起。这个现象特别有迷惑性因为这个错误不会告诉你具体是哪一环出了问题。我在做 iOS 开发这十年里接手过不下 20 次 Universal Links 相关的排查需求其中绝大多数问题集中在 AASA 文件格式、Associated Domains 能力没配对、服务器 Content-Type 错了这三类。这篇文章我不打算只给结论而是把从底层机制到线上验证的完整排查思路写出来包括哪些写法是“看起来对但实际会挂”的以及微信内跳转、WKWebView 旁路、首次冷启动这些容易让人误判的场景。1. Universal Links 校验不过的第一道坎关联域名的底层机制没搞清很多人配置 Universal Links 失败根源在于不理解系统到底在“校验”什么。你以为它校验的是链接能不能打开 App实际上它校验的是三样东西域名关联关系、AASA 文件的合法性和 App 注册状态。这三样缺一不可。1.1 苹果的校验链路从点击链接到唤起 App系统究竟做了什么用户在 Safari 里点击一个https://链接时系统会先看这个域名有没有被任何 App 注册过 Associated Domains。如果注册了它会向https://yourdomain.com/.well-known/apple-app-site-association这个固定地址发起请求拉取 apple-app-site-association 文件下文简称 AASA 文件。这个文件就是一张“白名单”里面的appID字段记录了TeamID.BundleID的组合。系统拿着你的 App 的唯一标识去比对匹配成功并且路径规则也能对上就直接唤起 App匹配不上就在浏览器里正常打开这个网页。整个过程用户是感知不到的你只能从结果反推是哪里断了。我说这个机制是因为排查的时候很多人不知道从哪里下手。有人一直换 AASA 文件里的路径格式但实际上他的 App ID 还没开启 Associated Domains 能力系统压根儿就不会把你的 App 纳入这个域名的处理者列表。方向错了改再多次也过不了。1.2 校验不通过的三个可能层面域名、文件、App 侧配置把整个链路拆开Universal Links 校验不通过只可能是下面三层出了错层面具体问题现象App 侧配置App ID 没勾选 Associated Domains、证书没包含该能力、entitlements 文件缺失系统根本不请求 AASA 文件文件服务AASA 文件路径不对、Content-Type 错误、超过大小限制、服务器断点iOS 拉取文件失败文件内容appID 写错、路径规则不匹配、JSON 格式错误拉取成功但验证失败我排优先级的时候永远先查 App 侧配置再查文件可访问性最后查文件内容。因为 App 侧配置如果错了后面的排查全都是在浪费时间。用一个比喻来说这就像快递寄件收件地址写错了App ID 配置快递员查无此人那你包裹里装的东西再完好AASA 文件内容正确也没有意义。1.3 配置前的必要准备先确认你手上这几项信息在动手配置之前请先把以下信息确认清楚不然后面反复折腾Team ID登录苹果开发者账号在 Membership 页面能看到一个 10 位字符串。Bundle IDXcode 项目里的 Product Bundle Identifier必须和 App ID 一致。服务器根域名AASA 文件必须放在 HTTPS 服务的根目录.well-known下是根域而不是子路径。开发者账号权限Associated Domains 能力需要对应的 App ID 权限如果你用的是免费的个人账号这个能力是开启不了的。尤其是最后一条网上很多“我配完了怎么还是不行”的求助帖最后发现是个人开发账号根本没有 Associated Domains 权限。这个资格问题建议放最前面确认省得后面白忙活。2. apple-app-site-association 文件我见过的六种“看起来正确但实际错误”的写法AASA 文件是 Universal Links 的“通行证”绝大多数校验不通过的问题都是出在这个文件的格式和部署上。下面这六种情况是我在排查时反复遇到的很多开发者按照网上老教程配完却忘了苹果其实调整过文件格式要求。2.1 正确的 AASA 文件模板一个可以直接抄的 JSON先给一个当前可用的标准模板注意没有.json后缀{ applinks: { apps: [], details: [ { appID: TEAMID.com.yourcompany.yourapp, paths: [*] } ] } }TEAMID换成你的 Team IDcom.yourcompany.yourapp换成你的 Bundle ID。paths定义哪些路径可以唤起 App*表示全部路径都唤起也可以写成[/product/*]只匹配特定路径。这里有个很多人踩过的坑早期 iOS 8 时代的applinks字段里会有appIDs复数数组现在虽然系统还兼容但新配置建议一律使用appID单数details数组的写法。如果你的 AASA 文件里只有appIDs而缺少details在新版本系统上可能直接校验失败。2.2 文件部署的三个硬性要求路径、Content-Type 和文件大小AASA 文件的部署规则比 JSON 内容更容易出问题我按踩坑概率排序第一路径必须是https://你的域名/.well-known/apple-app-site-association。注意两点必须是 HTTPS证书链要完整且被系统信任文件实际存储位置可以是.well-known/apple-app-site-association不需要在 URL 里加.json后缀。很多开发者把文件命名成apple-app-site-association.json并直接放根目录结果 iOS 请求的还是/.well-known/apple-app-site-association当然 404。第二HTTP 响应头Content-Type必须严格返回application/json。很多云服务器默认会把无后缀文件返回成application/octet-stream或text/plainiOS 会拒绝解析。这个错误用浏览器访问文件完全看不出来因为浏览器能正常打开文本所以特别坑。第三文件大小不能超过 150KBiOS 13 之后的上限之前是 128KB。如果你用了一些在线生成工具往里面塞了大量路径规则文件很容易超限。而且超限时 UIApplication 端不会报明显错误就是拉取后静默失败。2.3 六种“看起来对但实际会挂”的写法我把实际踩过的坑汇总成一张表每一项后面都标注后果方便你对照自查错误写法错误原因结果appID: com.xxx.app漏了 Team ID验证失败App 无法唤起appID: TEAMID.com.xxx.app但 Team ID 填的是开发者账号昵称把账号昵称当 Team ID验证失败paths: [https://xxx.com/*]path 里带了完整 URL路径永远匹配不上文件路径设置为/apple-app-site-association根目录缺少.well-known前缀iOS 请求文件名对不上文件用 UTF-8 BOM 编码某些编辑器默认带 BOMJSON 解析失败HTTPS 证书是自签名或证书链不完整ATS 和 AASA 拉取都会失败系统直接忽略该文件这里面最隐蔽的就是paths带 URL 前缀的写法。有些开发者从 Android 那边的 App Links 配置习惯带过来了paths里写完整域名加路径但苹果的paths只需要写路径部分*通配符只匹配路径不匹配域名。2.4 关于签名 AASA 文件的一个常见误解很多教程会让你用codesign对 AASA 文件做签名然后用.well-known/apple-app-site-association指向签名后的文件。这个说法有一定的历史背景iOS 13 之后苹果确实支持签名 AASA 文件签名后文件大小可以超过 150KB 的限制。但我要说的是绝大多数项目根本不需要签名。普通小程序、内容分享类 App 的路径规则不会超过 150KB 。签名反而会引入新的问题签名证书配置、签名格式、服务器返回的 Content-Type 都要严格对应稍有不慎校验更难过。你如果只是配置一个普通 App直接用无签名 JSON 文件就行。不过如果你真的要签名记住一个前提签名必须在支持 Associated Domains 的证书环境中进行并且签出来的二进制要放到指定路径不是随便签名就完事。我的建议是只有当 AASA 文件超过 150KB 或需要动态下发时才考虑签名方案常规项目直接避开这条支线。3. 完整排查链路从 Developer 后台到线上服务器的逐层验证既然说清楚了机制和文件规范下面就是实战环节。我按照一个标准的排查顺序来写你按这个顺序逐层确认比在论坛里搜“Universal Links 校验不通过”要快得多。3.1 第一步确认 App ID 和描述文件的 Associated Domains 能力打开 Apple Developer 后台找到你的 App ID检查 Associated Domains 是否为 Enabled。这个开关从 Xcode 11 开始通常是自动的但如果是老项目迁移过来的很可能是关闭状态。然后确认你打出来的描述文件或者 Profile 里包含com.apple.developer.associated-domains这个 entitlement。用 Xcode 打开项目选中 target - Signing Capabilities检查Associated Domains这个卡片是否已经在列表里。如果没有点 Capability搜索添加。这个环节有个非常隐蔽的坑如果你是在两个开发者账号之间切换旧账号的 App ID 和新账号的 Team ID 不一致即使 Xcode 里配置了 Associated Domains真机调试时用的描述文件也不匹配。所以一旦切换了开发者账号必须重新生成描述文件并且确认里面 Team ID 业务正确。3.2 第二步用 curl 命令验证 AASA 文件的真实可访问性很多“校验不通过”问题其实出在文件根本访问不到但开发者因为在浏览器里能打开这个文件就误以为网络侧没问题。浏览器和 iOS 的请求行为在上文提到过存在差异所以请务必用命令行验证而不是只靠浏览器。执行下面这两个命令# 查看响应头确认 HTTP 状态码、Content-Type、文件大小 curl -i https://yourdomain.com/.well-known/apple-app-site-association# 直接打印文件内容 curl -s https://yourdomain.com/.well-known/apple-app-site-association正常返回头应该长这样关键字段重点看HTTP/1.1 200 OK Content-Type: application/json Content-Length: 246需要重点确认的包括状态码必须是 200。如果出现 301/302说明文件地址被重定向了。虽然有说法称 iOS 支持少量跳转但实际经验里CDN 重定向后经常拉取失败尽量保证直达 200。Content-Type必须是application/json不要带charsetutf-8也不要是application/octet-stream。Content-Length不要超过 150KB。如果你用的是阿里云 OSS 或腾讯云 COS注意它们的默认 MIME 类型可能不识别无后缀文件。需要在对象存储的控制台手动设置 Content-Type 为application/json这一点非常容易被忽略。3.3 第三步检查服务器有没有给你“意外惊喜”服务器端的配置坑比想象中要多。我列几个实际遇过的案例案例一Nginx 配置文件里默认 deny 了隐藏目录。很多安全加固后的 Nginx 会拦截.well-known目录的访问因为默认规则只放行了/.well-known/acme-challenge/用于 Let’s Encrypt 证书校验而 AASA 文件所在的/.well-known/apple-app-site-association路径被拦截了。处理方式是单独放行location /.well-known/apple-app-site-association { default_type application/json; alias /var/www/apple-app-site-association; }案例二CDN 缓存了旧文件。如果你在 CDN 后面更新过 AASA 文件用户端拉到的可能还是缓存里的旧版本。这个排查起来最磨人因为你本地 curl 明明是新内容iOS 就是拉旧的。绕开 CDN 直接请求源站 IP 和 Host 试试如果源站返回是正确的那基本就是 CDN 缓存问题。案例三HTTPS 证书链不完整。有些廉价证书服务商只签发了域名证书没把中间证书链一起给到服务器桌面浏览器会“自动补链”但 iOS 系统网络栈不会。解决方法是把中间证书和根证书合并到服务器证书链里。3.4 第四步真机验证确认 App 侧回调是否触发确认完上面三步后把 App 装到真机上首次冷启动一次这一步很关键首次启动会让系统注册该 App 的 Universal Links然后退出 App在系统自带的备忘录或 Safari 里输入你要测试的链接并点击。注意不要用微信或其他第三方 App 里打开测试因为嵌套第三方 WebView 时系统级 Universal Links 不一定会按预期触发。App 侧能否正确接收取决于你是否实现了回调节点。在 AppDelegate 里加上这个方法func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: escaping ([UIUserActivityRestoring]?) - Void ) - Bool { guard userActivity.activityType NSUserActivityTypeBrowsingWeb, let url userActivity.webpageURL else { return false } // 在这里处理 url并返回 true print(Universal Link received: \(url)) return true }如果你已经实现了这个回调但根本没打印日志说明链接压根儿没有唤起你的 App。这时回到前面几步继续排查如果打印日志了说明 Universal Links 链路已经通了剩下的只是处理 URL 路由逻辑。3.5 附一条快速自查表把上面步骤合并成一张自查表建议贴到项目 Wiki 里以后谁再遇到同类问题直接照着走排查项验证方式通过标准App ID 能力Developer 后台Associated Domains Enabled描述文件Xcode - Signing Capabilities有 Associated Domains 卡片AASA 文件路径curl -i https://domain/.well-known/apple-app-site-associationHTTP 200Content-Type同上application/json文件大小同上 150KBappID 字段打开文件内容检查TEAMID.BundleID真机回调点击备忘录中的链接AppDelegate 回调有日志4. 几个容易踩的冷门场景微信内跳转、WebView 旁路、App 首次冷启动通过了上面全套排查你可能会觉得万事大吉但实际项目中这几个场景经常让人产生“怎么又失效了”的错觉。它们不是标准配置的问题而是调用环境的问题。4.1 微信内跳转不是 iPhone 上所有浏览器都走系统逻辑很多 App 有分享到微信的需求用户在微信里点开分享链接时Universal Links 的行为和 Safari 完全不一样。微信会在自己的 WKWebView 里拦截链接并且不会自动唤起你的 App。如果你想在微信内打开链接时唤起 App必须在微信开放平台配置 Universal Links并且使用微信 SDK 的WXApi handleOpenUniversalLink相关接口来处理。这里有个细节微信开放平台填写的 Universal Links 地址必须和你在 Xcode 里配置的applinks:域名保持一致。很多人配完 AASA 文件却忘了去微信后台补这一项结果用户在微信里点链接永远都只是打开网页。这个问题的表象和标准校验不过一样但根因是微信侧的关联配置缺失。4.2 WKWebView 旁路App 自己内部的 WebView 点击链接不触发 Universal Links如果你的 App 内部使用了 WKWebView用户在里面点击一个含有 Universal Links 的网页链接时默认行为是 WebView 直接加载这个页面而不是唤起对应 App因为系统级的 Universal Links 处理不会自动发生在任意 WKWebView 里。这个问题在我们的历史项目中真的出现过用户反馈“点了链接怎么又跳到另一个网页了应该唤起 XX App 才对”。解决方案是在decidePolicyFor navigationAction里拦截判断 URL 的 host 是否匹配你的关联域名匹配则调用UIApplication.shared.open(url)交给系统处理并返回.cancel取消 WebView 加载func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: escaping (WKNavigationActionPolicy) - Void ) { if let host navigationAction.request.url?.host, host yourdomain.com { UIApplication.shared.open(navigationAction.request.url!) decisionHandler(.cancel) return } decisionHandler(.allow) }4.3 App 首次冷启动与模拟器两个容易被误判的“失效”场景新安装的 App 首次点击 Universal Link 不唤起很多时候不是配置失败而是系统还没注册。iOS 要求用户至少手动打开过一次 App之后系统才会将 Universal Links 关联交给你的 App。测试时如果发现不唤起先把 App 冷启动一次再回到浏览器点击链接。另外模拟器对 Universal Links 的支持非常有限经常出现 Safari 里点击链接不触发关联的情况。这不是你配置的问题是模拟器本身对系统级关联的处理不完整。所以 Universal Links 测试务必以真机为准。4.4 跨端框架打包的配置陷阱uniapp 与 Flutter如果你是做 uniapp 或 Flutter 开发Universal Links 的配置又多一层坑不光要在原生工程里配还可能因为框架的打包机制导致 entitlements 丢失。uniapp 项目需要在manifest.json里找到 iOS 模块配置添加Associated Domains然后再打包自定义调试基座。麻烦的是如果你用云打包而没配置原生 entitlements最终打出来的 App 是没有关联域名能力的。离线打包则需要在 Xcode 工程里手动检查.entitlements文件里是否有com.apple.developer.associated-domains数组。Flutter 项目相对直接一些只需要在ios/Runner.xcodeproj里手动添加 Associated Domains capability然后在AppDelegate.swift里写好application:continue:restorationHandler:方法即可。Flutter 默认模板不会帮你加这个所以很多 Flutter 开发者会漏掉原生侧配置这一步。如果你在跨端开发中遇到了校验不通过多留个心眼检查最终安装包里的 entitlements 文件是不是真的包含关联域名数组而不是只看 IDE 里的配置界面。5. 验证通过后还建议做的一次端到端自检好了当你把上面的步骤全部过完链接终于能唤起 App 了我建议不要就此收工。在一键配置类工具满天飞的今天很多人拿到一个可用的配置就开始复制粘贴但 AASA 文件一旦改动CDN 缓存和客户端缓存会导致新旧版本并存。最后再走一遍端到端自检确认各环节不会在正式发布那天掉链子。一个完整的端到端自检流程是这样用一个闲置的手机清空 Safari 缓存设置 - Safari - 清除历史记录与网站数据避免旧的 AASA 缓存干扰判断。第一次安装 App 后先手动打开一次然后回到桌面。在备忘录里输入你的 Universal Link 并点击确认系统直接唤起 App。回到 App 后检查continueUserActivity回调里的 URL 是否正确传参。再打开你 App 内的 WKWebView 页面模拟点击一个指向该域名的链接确认不会跳网页而是唤起外部 App。最后用微信测试一次分享链接如果配置了微信开放平台确认能正常拉起。这套走完基本能保证线上环境八九不离十。我之前遇到过一次更新 AASA 文件后 CDN 缓存导致旧文件迟迟不失效的情况最后是给 CDN 配置了更短的 TTL比如 300 秒并且在苹果开发者后台重新生成描述文件后强制客户端重新注册问题才得以解决。这类“到了线上才暴露”的坑最好在自检阶段就通过强制刷新和多个网络环境测试提前发现。