Universal Links 新增功能
What's new in Universal Links
2020年6月23日
一句话判断
Universal Links 在 iOS 14 中新增了 Apple App Site Association(AASA)文件的 JSON Web Signature 验证和 App Clips 支持,如果你在做 App Clips 或者想加强 AASA 文件的安全性,这个 Session 值得一看。
这场 Session 讲了什么
Universal Links 允许用户点击一个 HTTPS 链接时直接打开你的 App 而非浏览器。它的核心机制是:你的 Web 服务器根目录或 .well-known 目录下放置一个 apple-app-site-association(AASA)文件,声明哪些 URL 路径应该由哪个 App 处理。
iOS 14 带来了三项更新。第一,AASA 文件现在支持 JWS(JSON Web Signature)签名验证,让你的 App 可以验证 AASA 文件确实来自你信任的服务器。第二,Universal Links 现在与 App Clips 完全集成——当用户还没有安装完整 App 时,点击 Universal Link 可以直接打开 App Clip。第三,Apple 改进了 AASA 文件的缓存和更新策略,缩短了从配置到生效的延迟。
值得深挖的点
AASA 文件的 JWS 签名
传统的 AASA 文件通过 HTTPS 传输来保证完整性,但文件本身没有签名。如果你的 CDN 被入侵,攻击者可以篡改 AASA 文件。JWS 签名让你可以用私钥对 AASA 文件签名,App 端用内置的公钥验证。这是一个可选的安全增强,但 Apple 推荐面向企业或金融场景的 App 采用这种方式。
App Clips 与 Universal Links 的配合
这是 iOS 14 中 Universal Links 最重要的新能力。当用户点击一个 Universal Link 但没有安装你的完整 App 时,系统会检查是否有匹配的 App Clip。如果有,用户可以直接使用 App Clip 来完成任务,无需安装完整 App。AASA 文件中新增了 appclips 字段来声明 App Clip 的 URL 匹配规则。
代码片段
配置支持 App Clips 的 AASA 文件
场景:在服务器端配置 AASA 文件,同时支持完整 App 和 App Clip。
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.app"],
"components": [
{
"/": "/orders/*",
"comment": "订单详情页"
},
{
"/": "/menu/*",
"comment": "餐厅菜单页"
}
]
}
]
},
"appclips": {
"apps": ["ABCDE12345.com.example.app.Clip"],
"details": [
{
"appID": "ABCDE12345.com.example.app.Clip",
"paths": [
"/menu/*",
"/order/new"
]
}
]
}
}
在 App 中处理 Universal Link
场景:根据 URL 路径导航到不同的页面。
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleUniversalLink(url)
}
// 处理 scene(_:continue:) 用于已运行时接到的链接
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}
handleUniversalLink(url)
}
private func handleUniversalLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let pathComponents = components.path.components(separatedBy: "/").filter({ !$0.isEmpty }) else {
return
}
switch pathComponents.first {
case "orders":
// /orders/{orderId}
if let orderId = pathComponents.count > 1 ? pathComponents[1] : nil {
navigateToOrderDetail(orderId: orderId)
}
case "menu":
// /menu/{restaurantId}
if let restaurantId = pathComponents.count > 1 ? pathComponents[1] : nil {
navigateToMenu(restaurantId: restaurantId)
}
default:
// 未匹配的路径,导航到首页
navigateToHome()
}
}
}
配置 Entitlements
场景:在 Xcode 中正确配置 Associated Domains。
// 在项目的 Signing & Capabilities 中添加 Associated Domains
// 格式:applinks:yourdomain.com
// 支持通配符:applinks:*.yourdomain.com
// 如果使用 JWS 空签名验证,添加:
// applinks:yourdomain.com?mode=developer
// 开发模式下可以使用本地服务器测试
// Entitlements 文件示例:
// <key>com.apple.developer.associated-domains</key>
// <array>
// <string>applinks:example.com</string>
// <string>appclips:example.com</string>
// </array>
测试 Universal Links
场景:在开发和调试阶段验证 Universal Links 配置。
// 验证 AASA 文件是否可访问
func verifyAASAFile(domain: String) {
let urls = [
"https://\(domain)/.well-known/apple-app-site-association",
"https://\(domain)/apple-app-site-association"
]
for urlString in urls {
guard let url = URL(string: urlString) else { continue }
URLSession.shared.dataTask(with: url) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print("URL: \(urlString)")
print("状态码: \(httpResponse.statusCode)")
print("Content-Type: \(httpResponse.value(forHTTPHeaderField: "Content-Type") ?? "未知")")
// AASA 文件不应有 .json 后缀
// Content-Type 应该是 application/json
// 不能有重定向
}
}.resume()
}
}
最佳实践
已有项目:检查你的 AASA 文件配置是否正确。常见问题包括:文件放在了错误的位置(应该在 .well-known 下)、文件有 .json 后缀(不应该有)、文件通过重定向获取(AASA 文件不支持重定向)。使用 Apple 的 AASA 验证工具或 curl 命令检查文件是否可以正常获取。
新项目:从第一天就配置好 Universal Links,即使暂时不需要深度链接。在开发阶段使用 ?mode=developer 参数来绕过 AASA 文件的 CDN 缓存。为每个核心功能定义 URL 路由规则,确保 URL Scheme 和 Universal Links 的处理逻辑一致。
还有什么值得关注
- AASA 文件的最大大小限制为 128KB,超过这个大小会被系统忽略。
- 系统会在 App 安装后立即检查 AASA 文件,之后大约每 7 天更新一次缓存。
- 如果你需要立即更新 AASA 配置,用户需要卸载并重新安装 App。