
Tutorial
September 8, 2022
23 min read
September 8, 2022
61 min read
Xác thực máy chủ (xác thực biên nhận (receipt validation) phía máy chủ) là một cách để xác minh tính xác thực của giao dịch mua. Không giống như xác thực dựa trên thiết bị, xác thực máy chủ xảy ra – đợi xác thực – ở phía máy chủ. Xác thực biểu thị rằng thiết bị hoặc máy chủ đưa ra yêu cầu tới máy chủ của Apple để tìm hiểu xem giao dịch mua có thực sự xảy ra hay không và có hợp lệ hay không.
Cần lưu ý rằng xác thực máy chủ không bắt buộc – mua trong ứng dụng sẽ vẫn hoạt động mà không cần xác thực. Tuy vậy, xác thực máy chủ vẫn đem lại lợi thế:
Từ kinh nghiệm của chúng tôi, chỉ lợi ích đầu tiên là đủ để thiết lập xử lý mua trên máy chủ.
Nhìn chung, quá trình xác thực biên nhận trên iOS như thế này:
Để gửi yêu cầu xác thực thanh toán, bạn phải thêm khóa bí mật chia sẻ để ủy quyền yêu cầu. Bạn có thể tạo một khóa trong App Store Connect.
Khóa bí mật chia sẻ có thể được tạo cho một ứng dụng cụ thể (khóa bí mật dành riêng cho ứng dụng hoặc cho tất cả các ứng dụng trong tài khoản).
Để tạo khóa bí mật dành riêng cho ứng dụng, hãy mở trang của ứng dụng trong App Store Connect, truy cập In-App Purchases → Manage và nhấp vào App-Specific Shared Secret. Trong cửa sổ mở ra, bạn có thể tạo mã token mới hoặc sao chép mã hiện có.
Để nhận khóa bí mật cho tất cả các ứng dụng trong tài khoản của bạn, hãy mở trang Users and Access và chọn tab Shared Secret.
Sau khi nhận khóa bí mật chia sẻ, bạn có thể gửi biên nhận để được xác thực trên các máy chủ của Apple. Điều này được thực hiện qua yêu cầu verifyReceipt. Bạn phải gửi một POST yêu cầu tới https://buy.itunes.apple.com/verifyReceipt. Trong phần nội dung JSON của yêu cầu, chuyển khóa bí mật chia sẻ vào trường password và biên nhận trong trường receipt-data. Ngoài ra còn có thông số exclude-old-transactions tùy chọn. Nếu nó có giá trị true thì đối với mỗi gói đăng ký tự động gia hạn, bạn sẽ chỉ nhận giao dịch cuối cùng thay vì toàn bộ lịch sử gia hạn.
Đây là phần dữ liệu truyền đi (payload) của yêu cầu xác thực mua:
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Nếu bạn đang làm việc trong môi trường Sandbox — nghĩa là bạn đang thử nghiệm mua — sau đó gửi yêu cầu xác thực tới https://sandbox.itunes.apple.com/verifyReceipt. Khóa bí mật chia sẻ cũng như dữ liệu truyền đi và định dạng phản hồi vẫn giữ nguyên.
Điều quan trọng cần lưu ý là bạn sẽ không thể xác thực biên nhận được tạo trong môi trường Sandbox trên máy chủ Production (Production server) và ngược lại. Đó là lý do tại sao trong các hệ thống trong thế giới thực, cách tốt nhất là chuyển yêu cầu đầu tiên đến máy chủ Production và chuyển hướng yêu cầu đó đến máy chủ Sandbox trong trường hợp khóa trạng thái trả về mã lỗi 21007. Đây là hành vi bắt buộc trong quá trình đánh giá ứng dụng vì hành vi này cho phép nhân viên Apple thử nghiệm mua đồng thời cũng cho phép người dùng thực của ứng dụng thực hiện mua.
Trong số các lỗi khác cần lưu ý, mã lỗi 21004 nghĩa là chúng ta đang dùng sai khóa bí mật. Điều quan trọng là phải theo dõi vì dùng sai sẽ ảnh hưởng đến cả trải nghiệm người dùng và độ chính xác của số liệu phân tích. Trong trường hợp xấu nhất, ứng dụng có thể bị xóa khỏi App Store nếu người dùng không được cấp quyền truy cập vào các tính năng cao cấp sau khi họ trả tiền cho những tính năng này.
Nếu xác thực thành công (status=0) thì phản hồi sẽ chứa chi tiết giao dịch của người dùng.
Đây là phản hồi yêu cầu xác thực thanh toán:
{
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 123,
"app_item_id": 123,
"bundle_id": "com.adapty.sample_app",
"application_version": "1",
"download_id": 123,
"version_external_identifier": 123,
"receipt_creation_date": "2021-04-28 19:42:01 Etc/GMT",
"receipt_creation_date_ms": "1619638921000",
"receipt_creation_date_pst": "2021-04-28 12:42:01 America/Los_Angeles",
"request_date": "2021-08-09 18:26:02 Etc/GMT",
"request_date_ms": "1628533562696",
"request_date_pst": "2021-08-09 11:26:02 America/Los_Angeles",
"original_purchase_date": "2017-04-09 21:18:41 Etc/GMT",
"original_purchase_date_ms": "1491772721000",
"original_purchase_date_pst": "2017-04-09 14:18:41 America/Los_Angeles",
"original_application_version": "1",
"in_app": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
],
"status": 0
}
Phản hồi khá rườm rà và đã được đơn giản hóa trong phiên bản App Store Server API mới nhưng việc triển khai hiện tại không khó để nắm bắt.
Để tìm hiểu xem liệu người dùng có quyền truy cập vào các tính năng cao cấp của ứng dụng hay không, bạn cần có cách xác định trạng thái gói đăng ký của họ. Vì không có yêu cầu riêng để truy xuất trạng thái gói đăng ký trong phiên bản API hiện tại nên bạn sẽ cần phải làm việc với lịch sử giao dịch trong mọi trường hợp.
Theo mặc định, dãy latest_receipt_info chứa tất cả các giao dịch mua trong ứng dụng của một người dùng cụ thể, ngoại trừ các sản phẩm tiêu thụ được hoàn thành ở phía ứng dụng. Theo cách này, bạn có thể truy xuất toàn bộ lịch sử mua của người dùng. Điều đó khá hữu ích cho cả việc phân tích và xác định trạng thái gói đăng ký hiện tại.
Có vẻ như các giao dịch mới nhất luôn được xếp trước. Tuy nhiên, để chắc chắn, tôi vẫn khuyên bạn nên triển khai phân loại giao dịch theo ngày theo cách riêng của bạn.
Dữ liệu truyền đi của giao dịch:
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
Để kiểm tra trạng thái đăng ký hiện tại, chỉ cần truy xuất giao dịch mới nhất của chuỗi và xem thuộc tính expires_date là đủ. Ngoại lệ sẽ là thời gian gia hạn (grace period) mà chúng ta sẽ nhắc đến sau.
Đối với mục đích phân tích, tôi khuyên bạn nên lưu các thuộc tính sau:
Apple đã thông báo ở WWDC 2021 rằng họ đang có kế hoạch thêm trường appAccountToken vào thông tin giao dịch. Trường sẽ chứa mã nhận dạng của người dùng trong hệ thống của bạn. Mã nhận dạng này phải ở định dạng UUID và được xác định ở phía ứng dụng khi giao dịch mua được khởi tạo. Nếu được xác định thì mã trong tất cả các giao dịch của chuỗi này (gia hạn gói đăng ký, vấn đề thanh toán, v.v.) sẽ được trả lại, có nghĩa là bạn sẽ dễ dàng hiểu được người dùng nào đã mua.
Bạn cũng nên theo dõi thông số subscription_group_identifier. Nếu người dùng trước đó đã có bất kỳ giao dịch nào với bản dùng thử đang hoạt động hoặc ưu đãi ra mắt thì họ sẽ không có quyền truy cập vào các giao dịch này cho cùng một nhóm đăng ký. Phía máy chủ cũng nên theo dõi thông số này.
Dãy pending_renewal_info lưu trữ dữ liệu gia hạn gói đăng ký. Dãy cho bạn hiểu điều gì sẽ xảy ra với gói đăng ký trong kỳ thanh toán tiếp theo. Ví dụ: nếu bạn phát hiện ra người dùng từ chối tự động gia hạn thì bạn có thể đề xuất họ chuyển sang gói khác hoặc tặng họ một ưu đãi khuyến mại. Những sự kiện này có thể được theo dõi thủ công bằng các thông báo của máy chủ mà tôi sẽ thảo luận ngay sau đây.
Dữ liệu gia hạn gói đăng ký:
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
Nếu người dùng không có sản phẩm tự động gia hạn (auto-renewable product) thì các khóa latest_receipt_info và pending_renewal_info sẽ không được trả lại. Trong trường hợp này, các giao dịch có thể được tìm thấy trong receipt → in_app. Định dạng giao dịch tương tự như giao dịch tự động gia hạn nhưng không có trường dành cho thời hạn, gia hạn, ưu đãi và các thuộc tính khác dành riêng cho các giao dịch tự động gia hạn.
Cần lưu ý rằng receipt → in_app cũng sẽ đến cho các giao dịch tự động gia hạn, nhưng cách tốt hơn là sử dụng latest_receipt_info, vì dữ liệu đăng ký trong định dạng sẽ mới nhất.
Thời gian trước đây, bạn phải thiết lập một hệ thống phức tạp để theo dõi các thay đổi trạng thái gói đăng ký. Ví dụ: để biết gói đăng ký có được gia hạn hay không, bạn phải gửi yêu cầu trạng thái gói đăng ký đến máy chủ của Apple mỗi giờ, bắt đầu từ 24 giờ trước khi đăng ký hết hạn. Apple ngày càng thêm nhiều thông báo máy chủ (server notification) hơn theo thời gian và những thông báo này hiện bao gồm tất cả các sự kiện quan trọng liên quan đến gói đăng ký. Điều đó rất hữu ích: sau khi có bất kỳ thay đổi nào từ phía Apple, bạn sẽ nhận được thông báo thay đổi trên máy chủ của mình. Nghĩa là, bạn sẽ được thông báo về các giao dịch mua mới, gia hạn gói đăng ký, vấn đề thanh toán, v.v. Điều này cho phép bạn thu thập số liệu phân tích chính xác hơn, cũng như giúp quản lý trạng thái đăng ký dễ dàng hơn nhiều.
Bạn có thể bật thông báo máy chủ trong App Store Connect. Mở trang của ứng dụng và chuyển đến General -> App information. Tiếp theo, đặt liên kết vào URL cho trường App Store Server Notifications và lưu các thay đổi của bạn.
Thông báo máy chủ:
{
"notification_type": "DID_RENEW",
"password": "f4d35830e3...52aae",
"environment": "PROD",
"auto_renew_product_id": "basic_subscription_1_month",
"auto_renew_status": "true",
"unified_receipt": {
"status": 0,
"environment": "Production",
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_status": "1",
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853"
}
]
},
"bid": "com.adapty.sample_app",
"bvrs": "0"
}
Định dạng thông báo của máy chủ tương tự như phản hồi xác thực thanh toán. Chi tiết giao dịch được lưu trữ trong unified_receipt → latest_receipt_info. Khóa mật khẩu lưu trữ khóa bí mật chia sẻ cho ứng dụng của bạn, cho phép bạn xác minh tính xác thực của yêu cầu. Khóa notification_type lưu trữ loại sự kiện. Theo tôi, hữu ích nhất trong số này là:
Xác thực máy chủ có thể tăng cường phân tích của bạn cho dữ liệu bạn thu thập từ ứng dụng của mình. Nó cũng khiến những kẻ gian lận khó tiếp cận nội dung cao cấp của bạn hơn cũng như cho phép bạn tạo những người dùng đăng ký trên nhiều nền tảng. Đồng thời, việc triển khai xác thực máy chủ có thể mất khá nhiều thời gian, đặc biệt nếu bạn cần dữ liệu có độ chính xác cao. Điều này sẽ yêu cầu tính đến nhiều trường hợp phụ: nâng cấp gói đăng ký, nâng cấp chéo gói đăng ký, thời gian dùng thử, ưu đãi khuyến mại/ra mắt, thời gian gia hạn, hoàn tiền, đăng ký gia đình, v.v. Bạn cũng phải biết và tính đến các sắc thái khác nhau, ví dụ: Apple có chính sách giảm hoa hồng từ 30% xuống 15% cho các gói đăng ký thường xuyên được gia hạn trong hơn một năm.
Further reading
Tutorial
September 8, 2022
23 min read
Tutorial
September 8, 2022
21 min read
Tutorial
September 8, 2022
70 min read