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.comtrong 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.
Bước 1: Khai báo trong app
Phần tiêu đề “Bước 1: Khai báo trong app”Xcode: chọn target → Signing & Capabilities → + Capability → Associated Domains, thêm:
applinks:shop.example.comwebcredentials:shop.example.comKế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>Bước 2: Khai báo trên website
Phần tiêu đề “Bước 2: Khai báo trên website”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:
// SwiftUITextField("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?”Liên kết hai chiều
Phần tiêu đề “Liên kết hai chiều”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ía | Khai báo | Ý nghĩa |
|---|---|---|
| App | Entitlement com.apple.developer.associated-domains | “App muốn dùng service X với domain Y” |
| Website | File 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 ý).
Các service
Phần tiêu đề “Các service”| Service | Dùng cho | Khoá trong AASA |
|---|---|---|
applinks | Universal Links | applinks.details[].appIDs + components |
webcredentials | Tự điền mật khẩu, passkey dùng chung giữa web và app | webcredentials.apps |
activitycontinuation | Handoff 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 |
appclips | App Clip | appclips.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.
Định dạng entitlement
Phần tiêu đề “Định dạng entitlement”<service>:<domain đầy đủ>[?mode=<alternate mode>]- Chỉ ghi domain, không có
https://, path, query hay dấu/ở cuối. example.com,www.example.comvàshop.example.comlà 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ừ serviceappclipskhông hỗ trợ wildcard.
File apple-app-site-association
Phần tiêu đề “File apple-app-site-association”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.
CDN của Apple
Phần tiêu đề “CDN của Apple”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-associationTheo 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:
curl -i https://app-site-association.cdn-apple.com/a/v1/shop.example.comAlternate mode: bỏ qua CDN
Phần tiêu đề “Alternate mode: bỏ qua CDN”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 |
|---|---|
developer | Chỉ 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 |
managed | Chỉ 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+managed | Thiế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 đó.
Lưu ý khi làm dự án thật
Phần tiêu đề “Lưu ý khi làm dự án thật”Provisioning profile phải chứa entitlement
Phần tiêu đề “Provisioning profile phải chứa entitlement”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.
Mỗi môi trường một entitlement
Phần tiêu đề “Mỗi môi trường một entitlement”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.entitlementsvà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.
webcredentials và passkey
Phần tiêu đề “webcredentials và passkey”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.
Kiểm tra và debug
Phần tiêu đề “Kiểm tra và debug”-
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 -
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.
-
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 -
Dùng developer mode (
?mode=developer+ bật Associated Domains Development) để loại trừ nguyên nhân do cache của CDN. -
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. -
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ử.