Bỏ qua để đến nội dung

Associated Domains

Associated Domains là cơ chế iOS dùng để xác nhận một app và một website thuộc cùng một chủ. Khi đã xác nhận, app được dùng các tính năng gắn với domain đó: Universal Links, tự điền mật khẩu và passkey, Handoff, App Clips. Bài Universal Links và App Links mới chỉ dùng một phần của cơ chế này; bài này nói về toàn bộ.

Ví dụ: app MyShop dùng chung tài khoản với website

Phần tiêu đề “Ví dụ: app MyShop dùng chung tài khoản với website”

App MyShop (Team ID ABCDE12345, bundle ID com.example.myshop) có website https://shop.example.com. Ngoài việc mở link sản phẩm bằng app, team muốn:

  • Người dùng đã lưu mật khẩu của shop.example.com trong Safari (iCloud Keychain hoặc trình quản lý mật khẩu) thì khi mở màn hình đăng nhập của app, bàn phím gợi ý đúng tài khoản đó.
  • Sau này hỗ trợ đăng nhập bằng passkey dùng chung cho cả web và app.

Cả hai đều cần Associated Domains, chỉ khác service.

Xcode: chọn target → Signing & Capabilities → + Capability → Associated Domains, thêm:

applinks:shop.example.com
webcredentials:shop.example.com

Kết quả trong file entitlements (với Flutter là ios/Runner/Runner.entitlements):

<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:shop.example.com</string>
<string>webcredentials:shop.example.com</string>
</array>

File https://shop.example.com/.well-known/apple-app-site-association (không có đuôi .json):

{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.myshop"],
"components": [
{ "/": "/products/*" },
{ "/": "/orders/*" }
]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.myshop"]
}
}

Bước 3: Đánh dấu ô nhập liệu trong app

Phần tiêu đề “Bước 3: Đánh dấu ô nhập liệu trong app”

Để iOS biết đâu là ô tài khoản, ô mật khẩu:

// SwiftUI
TextField("Email", text: $email)
.textContentType(.username)
SecureField("Mật khẩu", text: $password)
.textContentType(.password)

Với Flutter, dùng autofillHints và bọc form trong AutofillGroup:

AutofillGroup(
child: Column(
children: [
TextField(autofillHints: const [AutofillHints.username]),
TextField(obscureText: true, autofillHints: const [AutofillHints.password]),
],
),
)

Cài lại app, mở màn hình đăng nhập: thanh gợi ý phía trên bàn phím hiện tài khoản đã lưu của shop.example.com. Không có webcredentials, iOS vẫn có thể gợi ý mật khẩu nhưng không biết app thuộc website nào nên không gợi ý đúng tài khoản. Khi người dùng đăng nhập hoặc đăng ký trong app, iOS cũng đề nghị lưu mật khẩu gắn với domain, để lần sau dùng được trên Safari.

Associated Domains hoạt động như thế nào?

Phần tiêu đề “Associated Domains hoạt động như thế nào?”

Giống Android App Links, liên kết phải được khai báo ở cả hai phía và phải khớp nhau:

PhíaKhai báoÝ nghĩa
AppEntitlement com.apple.developer.associated-domains“App muốn dùng service X với domain Y”
WebsiteFile apple-app-site-association (AASA)“Domain Y cho phép app có ID này dùng service X”

App tự khai báo thì ai cũng làm được; file trên website chứng minh chủ domain đồng ý. Vì entitlement được nhúng vào chữ ký của app, nó cũng phải có trong provisioning profile (xem phần lưu ý).

ServiceDùng choKhoá trong AASA
applinksUniversal Linksapplinks.details[].appIDs + components
webcredentialsTự điền mật khẩu, passkey dùng chung giữa web và appwebcredentials.apps
activitycontinuationHandoff giữa app và website (đang xem trên Mac Safari, chuyển sang app trên iPhone và ngược lại)activitycontinuation.apps
appclipsApp Clipappclips.apps (app ID của App Clip, ví dụ ABCDE12345.com.example.myshop.Clip)

Mỗi service chỉ hoạt động khi có cả dòng trong entitlement và khoá tương ứng trong file AASA. Thiếu một bên thì service đó không chạy, các service còn lại không ảnh hưởng.

Định danh app trong file có dạng <Team ID>.<Bundle ID>, ví dụ ABCDE12345.com.example.myshop. Team ID xem ở trang Membership của Apple Developer.

<service>:<domain đầy đủ>[?mode=<alternate mode>]
  • Chỉ ghi domain, không có https://, path, query hay dấu / ở cuối.
  • example.com, www.example.com và shop.example.com là các domain khác nhau, mỗi domain một dòng và mỗi domain phải có file AASA riêng.
  • Có thể dùng wildcard *.example.com để khớp mọi subdomain, trừ service appclips không hỗ trợ wildcard.

Yêu cầu:

  • Đặt tại https://<domain>/.well-known/apple-app-site-association, không có đuôi file.
  • Phục vụ qua HTTPS với chứng chỉ hợp lệ, không redirect.
  • Nên trả Content-Type: application/json.
  • Không yêu cầu đăng nhập, không bị chặn bởi tường lửa hay dịch vụ chống bot.

Một file có thể khai báo nhiều app, ví dụ app chính và app dành cho người bán cùng dùng một domain:

{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.myshop"],
"components": [{ "/": "/products/*" }]
},
{
"appIDs": ["ABCDE12345.com.example.myshop.seller"],
"components": [{ "/": "/seller/*" }]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.myshop", "ABCDE12345.com.example.myshop.seller"]
}
}

Cú pháp components của applinks (path /, query ?, fragment #, exclude) đã nói trong bài Universal Links và App Links. Lưu ý details và components chỉ dùng cho applinks, các service khác chỉ có mảng apps.

Từ iOS 14 và macOS 11, thiết bị không tải file AASA trực tiếp từ server của bạn mà hỏi một CDN do Apple quản lý. Nếu CDN chưa có file hoặc bản đã cũ, CDN mới tự tải từ server của bạn.

Thiết bị ──▶ CDN của Apple ──(khi chưa có/đã cũ)──▶ https://shop.example.com/.well-known/apple-app-site-association

Theo tài liệu của Apple:

  • Khi cài app, hệ thống tải file AASA và xác minh các domain trong entitlement.
  • CDN yêu cầu file của domain trong vòng 24 giờ.
  • Sau khi cài app, thiết bị kiểm tra cập nhật khoảng mỗi tuần một lần.

Hệ quả:

  • Server phải truy cập được từ Internet công cộng. Domain nội bộ, server staging sau VPN sẽ không chạy ở chế độ bình thường.
  • Sửa file AASA thì không có hiệu lực ngay. Muốn thêm path mới cho Universal Links trong ngày phát hành, hãy cập nhật file trước vài ngày.

Xem bản CDN đang lưu:

Terminal window
curl -i https://app-site-association.cdn-apple.com/a/v1/shop.example.com

Khi dev với server không public, hoặc muốn thấy ngay thay đổi của file AASA, thêm ?mode= vào entitlement:

applinks:staging.example.com?mode=developer
ModeÝ nghĩa
developerChỉ thiết bị bật developer mode mới truy cập domain, tải file trực tiếp từ server, bỏ qua CDN. Chấp nhận cả chứng chỉ SSL không được hệ thống tin cậy (ví dụ chứng chỉ tự ký). Chỉ áp dụng cho app ký bằng development profile, và người dùng phải tự bật trên từng máy
managedChỉ thiết bị được quản lý bằng MDM mới truy cập domain. Cần quản trị viên MDM đồng ý. Dùng cho app nội bộ doanh nghiệp với domain nội bộ
developer+managedThiết bị phải ở cả hai chế độ trên

Để dùng developer mode trên máy test: bật Settings → Developer → Associated Domains Development. Mục Developer chỉ xuất hiện khi máy đã bật Developer Mode (cắm vào Mac và chạy app từ Xcode ít nhất một lần).

Vì chế độ developer chấp nhận cả chứng chỉ không tin cậy, Apple giới hạn nó cho bản build dev. Không đưa domain có ?mode=developer vào bản phát hành, và cũng không cần: bản App Store ký bằng distribution profile sẽ không dùng được domain đó.

Bật capability trong Xcode sẽ đồng thời bật Associated Domains cho App ID trên Apple Developer. Provisioning profile tạo trước thời điểm đó không có entitlement này, build sẽ báo lỗi kiểu “Provisioning profile doesn’t include the com.apple.developer.associated-domains entitlement”.

  • Automatic signing: Xcode tự động quản lý và tải về profile mới nếu cần.
  • Manual signing (thường gặp trên CI): vào Apple Developer bật capability cho App ID nếu chưa có, tạo lại profile, tải về và cập nhật lên CI.

Về certificate, provisioning profile và ký app trên CI, xem bài Certificate và Provisioning Profile.

Lưu ý danh sách domain không nằm trong profile, profile chỉ cần có capability. Thêm hay bớt domain chỉ cần sửa file entitlements, không phải tạo lại profile.

Thường mỗi môi trường dùng domain khác nhau: production shop.example.com, staging staging.example.com (có thể kèm ?mode=developer vì không public). Cách làm:

  • Tạo nhiều file entitlements, ví dụ Runner-Prod.entitlements và Runner-Staging.entitlements.
  • Trong Build Settings → Code Signing Entitlements (CODE_SIGN_ENTITLEMENTS), trỏ mỗi build configuration tới file tương ứng.

Với Flutter dùng flavor, mỗi flavor đã có build configuration riêng (ví dụ Debug-staging, Release-prod), nên chỉ cần gán file entitlements cho từng configuration. Bundle ID của từng flavor (com.example.myshop.staging…) cũng phải có trong file AASA của domain tương ứng.

Passkey (đăng nhập không mật khẩu bằng Face ID/Touch ID) của iOS dựa trên webcredentials: passkey tạo trên website shop.example.com chỉ dùng được trong app khi app khai báo webcredentials:shop.example.com và domain đồng ý trong file AASA. Thiếu cấu hình này, API passkey trong app sẽ báo lỗi app không được liên kết với domain. Muốn web và app dùng chung passkey, Associated Domains là điều kiện bắt buộc.

  1. Kiểm tra file trên server trả 200, không redirect:

    Terminal window
    curl -i https://shop.example.com/.well-known/apple-app-site-association
  2. Kiểm tra bản trên CDN của Apple đã giống bản trên server chưa (lệnh ở phần CDN bên trên). Nếu CDN trả 404 hoặc bản cũ, iOS cũng thấy như vậy.

  3. Kiểm tra entitlement đã thật sự vào app (đặc biệt khi build bằng CI hoặc dùng nhiều configuration):

    Terminal window
    codesign -d --entitlements :- /đường/dẫn/MyShop.app
  4. Dùng developer mode (?mode=developer + bật Associated Domains Development) để loại trừ nguyên nhân do cache của CDN.

  5. Xem log hệ thống: mở app Console trên Mac, chọn thiết bị và lọc theo tiến trình swcd, tiến trình của iOS phụ trách tải và xác minh associated domains. Lỗi tải file hay không khớp app ID thường hiện ở đây.

  6. Cài lại app. Thiết bị chỉ tải file khi cài và định kỳ khoảng mỗi tuần, nên sau khi sửa cấu hình, gỡ app và cài lại là cách nhanh nhất để thử.