ApplePay 服务端单据验证

前言

记录一下ApplePay 非订阅商品的服务端单据验证流程,以及在其中遇到的问题。总的来说苹果的这套单据验证流程还是挺反人类的。

苹果应用内支付流程 iap(in app purchase)

这个是我正在使用的苹果支付大致支付成功的流程图。

  1. 首先获取商品列表,这里面包含苹果的 product_id,这个非常重要。
  2. 之后客户端直接唤醒支付并支付,这里并没有提前找商户服务创建订单,主要是为了减少流程。
  3. 支付成功后,苹果会返回一份单据,这份单据客户端也可以核实和解析,但是大部分情况下都是发送给服务端做。
  4. 商户服务端接受到单据会通过苹果提供的接口进行核实和解析。
  5. 核实成功拿到解析后的数据后就可以进行业务上的验证了,重点:单据核实有效并不到比代表这笔支付有效!!!! 下面会详述

支付单据 receipt 说明

支付成功后,苹果会返回一份单据,这份单据中包含非常多的信息。原始单据大概长下面这样,是一个加密编码后的文本。需要通过苹果提供的接口去核实单据的有效性。其次这个单据在某些情况下会非常非常长。

1
MIIUYgYJKoZIhvcNAQcCoIIUUzCCFE8CAQExCzAJBgUrDgMCGgUAMIIDoAYJKoZIhvcNAQcBoIIDkQSCA40xggOJMAoCARQCAQEEAgwAMAsCARkCAQEEAwIBAzAMAgEOAgEBBAQCAgDnMA0CAQoCAQEEBRYDMTcrMA0CAQ0CAQEEBQIDAnKRMA4CAQECAQEEBgIEWjY+pTAOAgEDAgEBBAYMBDY3LjAwDgIBCQIBAQQGAgRQMjYwMA4CAQsCAQEEBgIEBx5smzAOAgEQAgEBBAYCBDMfsaYwDgIBEwIBAQQGDAQ0OS4wMBICAQ8CAQEECgIIBvbT3O8NtPwwFAIBAAIBAQQMDApQcm9kdWN0aW9uMBgCAQQCAQIEEO7yXwxbjOjnUta2fyPUyLQwHAIBBQIBAQQUOm1OECcJC+fsCkg0UHAKddj32awwHgIBCAIBAQQWFhQyMDIzLTA2LTI4VDAwOjU1OjUyWjAeAgEMAgEBBBYWFDIwMjMtMDYtMjhUMDA6NTU6NTJaMB4CARICAQEEFhYUMjAyMi0xMC0xMFQxMjo0OTo1OFowHwIBAgIBAQQXDBVjb20ueWFsbGEueWFsbGFiYWxvb3QwRgIBBwIBAQQ+B6MbtmZLO6LVh2pyFTGXK4kIhCjr0tkJLiumfYMw6zshg3dpBuO86iJkXLcys2WV1HgLbyT8e+g9VncbnKAwTQIBBgIBAQRFCHEdkckJbnMT3AaUE3ex61BFAs41sUtMq02JGxpC63yJl/1pNljyjiQyFWhu8wGf9Rze4nNZv5q5o2LYuKeeQ2tZwjAUMIIBZgIBEQIBAQSCAVwxggFYMAsCAgasAgEBBAIWADALAgIGrQ

苹果提了一个verifyReceipt接口进行核实解析,这个接口是全球通用的,不需要任何授权信息,下面是是一个解析后的单据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"environment": "Production",
"status": 0,
"receipt": {
"receipt_type": "Production",
"adam_id": 13647829875,
"app_item_id": 13647829875,
"bundle_id": "test",
"application_version": "70.0",
"download_id": 501821354355307772,
"version_external_identifier": 857715110,
"receipt_creation_date": "2023-06-28 00:55:52 Etc/GMT",
"receipt_creation_date_ms": "1687913752000",
"receipt_creation_date_pst": "2023-06-27 17:55:52 America/Los_Angeles",
"request_date": "2023-06-29 05:57:40 Etc/GMT",
"request_date_ms": "1688018260446",
"request_date_pst": "2023-06-28 22:57:40 America/Los_Angeles",
"original_purchase_date": "2022-10-10 12:49:58 Etc/GMT",
"original_purchase_date_ms": "1665406198000",
"original_purchase_date_pst": "2022-10-10 05:49:58 America/Los_Angeles",
"original_application_version": "49.0",
"in_app": [
{
"quantity": "1",
"product_id": "lllll.20.com",
"transaction_id": "521111385457811",
"original_transaction_id": "521111385457811",
"purchase_date": "2023-06-28 00:55:52 Etc/GMT",
"purchase_date_ms": "1687913752000",
"purchase_date_pst": "2023-06-27 17:55:52 America/Los_Angeles",
"original_purchase_date": "2023-06-28 00:55:52 Etc/GMT",
"original_purchase_date_ms": "1687913752000",
"original_purchase_date_pst": "2023-06-27 17:55:52 America/Los_Angeles",
"is_trial_period": "false",
"in_app_ownership_type": "PURCHASED"
},
{
"quantity": "1",
"product_id": "lllll.20.com",
"transaction_id": "522222385457811",
"original_transaction_id": "522222385457811",
"purchase_date": "2023-06-28 00:55:52 Etc/GMT",
"purchase_date_ms": "1687913752000",
"purchase_date_pst": "2023-06-27 17:55:52 America/Los_Angeles",
"original_purchase_date": "2023-06-28 00:55:52 Etc/GMT",
"original_purchase_date_ms": "1687913752000",
"original_purchase_date_pst": "2023-06-27 17:55:52 America/Los_Angeles",
"is_trial_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
}
}
  1. status 是单据验证结果的表示。常见的有以下几种
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    21000 App Store无法读取你提供的JSON数据  
    21002 单据数据不符合格式
    21003 单据无法被验证
    21004 提供的共享密钥和账户的共享密钥不一致
    21005 单据服务器当前不可用
    21006 单据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
    21007 单据信息是测试用(Sandbox),但却被发送到产品环境中验证
    21008 单据信息是产品环境中使用,但却被发送到测试环境中验证
    21009 内部数据访问错误。稍后再试
    21010 系统找不到用户帐户或用户帐户已被删除。

  2. environment 表示支付交易发生的环境类型。它表示支付是在实际的生产环境还是在测试环境进行的,Production(生产环境)Sandbox(沙盒环境)。

receipt 里面就是你单据解析后的数据。

名称 描述
receipt_type 苹果支付单据的类型,Production, ProductionVPP, ProductionSandbox, ProductionVPPSandbox。)
adam_id 应用程序的唯一标识符。
app_item_id 唯一标识所购买的应用程序。仅在生产中有
bundle_id 应用程序唯一包名。
application_version 应用程序的版本号,每个版本都是唯一的
download_id 用于标识应用程序下载的唯一标识符。
version_external_identifier 用于标识应用程序版本的唯一标识符,用于表示用户购买或订阅的应用程序版本
receipt_creation_date 支付单据的创建日期和时间,时区 utc
receipt_creation_date_ms 支付单据的创建的毫秒时间戳
receipt_creation_date_pst 支付单据的创建日期时间,时区 utc-8
request_date 请求并生成响应的时间 时区utc
request_date_ms 请求并生成响应的时间毫秒时间戳
request_date_pst 请求并生成响应的时间 时区 utc-8
original_purchase_date 支付单据的原始购买日期和时间 时区 utc
original_purchase_date_ms 支付单据的原始购买日期和时间时间戳
original_purchase_date_pst 支付单据的原始购买日期和时间 时区 utc-8
original_application_version 当前账号首次购买的应用程序的版本。值不会改变

in_app 中是应用内购买的相关信息的集合。

quantity 购买的消耗品数量,基本情况下都会是1
product_id 所购买产品的唯一标识符,这个唯一仅限于你自己的应用
transaction_id 支付交易的唯一标识符,这个标识符是全局唯一的
original_transaction_id 原始交易的唯一标识符,主要用于订阅商品,非订阅商品可以忽略
purchase_date 向用户帐户收取购买或恢复产品费用日期时间 时区 utc
purchase_date_ms 向用户帐户收取购买或恢复产品费用的毫秒时间戳
purchase_date_pst 向用户帐户收取购买或恢复产品费用日期时间 时区 utc-8
original_purchase_date 应用内购买该项目的原始购买日期和时间 时区 utc
original_purchase_date_ms 应用内购买该项目的原始购买日期和时间时间戳
original_purchase_date_pst 应用内购买该项目的原始购买日期和时间 时区 utc-8
is_trial_period 否处于试用期
in_app_ownership_type 应用内购买项目所有权类型的字段 FAMILY_SHARED(可以与家庭共享) PURCHASED(只属于买方)

服务端校验以及风险点

苹果单据中的数据还是很全,有一些数据拿到后是必须在服务端进行业务上的判断的。如果疏忽了就会造成一些支付上的漏洞。下面大致是我的单据判断流程。

  1. 判断 status 如果非0,说明单据存在问题,但是也可能根据情况去放开沙盒支付,可以做一个沙盒支付的白名单,值允许白名单内的用户进行沙盒支付。
  2. 判断 bundle_idadam_id 是否是自己应用的标示。
  3. 判断 in_app 中是否有用户此次购买的 product_id 记录,并取出根据 purchase_date_ms 排序得到的最新一条。
  4. 判断取出数据中 transaction_id 是否发送过退款,或者是否已经发货。
  5. 没有发生退款和发货,就退当前订单进行发货,并记录、
伪造苹果服务器

如果单据是在客户端去验证的话,可以伪造苹果的服务器,让客户端的单据验证请求走到假的服务并返回一些假的成功信息,这种方式基本已经不存在了,因为基本没有单据是在客户端去验证的

单据重复验证

单据可能会被重复发到服务端,如果服务端不记录transaction_id 状态,就可能出现重复发货的情况,所以服务端不仅需要核实单据的有效性,还需要判断in_app中的哪些 transaction_id 是已经发货的,哪些是没有发货的。

跨应用单据认证

通过重新打包应用,替换应用包名等信息,然后在替换的应用中上架product_id 一样的商品。这样就可以通过他的应用支付单据去我们自己的服务器进行验证,如果服务端没有判断单据的bundle_idadam_id 这类单据的验证流程就会这个问题。

下面是大致单据校验流程伪代码。成功就返回 transaction_id ,之后就直接判断是否有过退款和发货就可以了。 可以适当的增加一些超时重试,这个接口经常会超时严重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private readonly IHttpClientFactory _clientFactory;

public async Task<string> ApplePayValidateAsync(string receipt, string productId)
{
var client = _clientFactory.CreateClient();
var param = new JsonObject()
{
{ "receipt-data", receipt },
};
var content = new StringContent(param.ToString());
var response = await client.PostAsync(""https://buy.itunes.apple.com/verifyReceipt", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
throw new Exception("失败");
}

var jsonNode = JsonNode.Parse(responseContent);
if (jsonNode["status"].ToString() == "21007")
{
throw new Exception("不允许沙盒支付");
}

if (jsonNode["status"].ToString() == "0")
{
throw new Exception("失败");
}

if (jsonNode["receipt"]["bundle_id"].ToString() != "")
{
throw new Exception("非法包名");
}

var list = JsonHelper.DeserializeJsonToList<ReceiptJsong>(jsonNode["receipt"]["in_app"].ToString());

var first = list.OrderByDescending(m => m.purchase_date_ms).FirstOrDefault(m => m.product_id == productId);

if (first == null)
{
throw new Exception("单据中没有此次购买的商品记录");
}

return first.transaction_id;
}

最后

前段时间苹果已经弃用了这种单据的验证方式。转而采用了App Store Server API 的方式去验证,可以提供更好的支付体验和更加快速的支付流程。


ApplePay 服务端单据验证
http://example.com/posts/2385.html
作者
她微笑的脸y
发布于
2023年6月28日
许可协议