You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何防止多用户使用同一Apple ID修改iOS应用内订阅

解决iOS端跨用户订阅操作的拦截问题

针对你遇到的场景,核心思路是在发起购买前提前验证当前Apple ID的订阅归属,匹配当前应用内登录用户,从而在收据生成前拦截非法操作。以下是具体实现步骤和代码示例:

1. 核心逻辑梳理

用户B能操作用户A的订阅,本质是当前设备登录的Apple ID(A的)与应用内用户(B的)不匹配。我们需要在购买流程启动前:

  • 查询当前Apple ID下的活跃订阅记录
  • 验证该订阅的归属是否为当前应用内登录用户
  • 若不匹配,直接拦截并提示用户更换Apple ID

2. 具体实现步骤

步骤1:购买前预查询订阅状态

使用InAppBillingPlugin的查询接口,获取当前Apple ID下的所有活跃订阅。这一步要在调用PurchaseAsync之前完成,确保提前拦截。

步骤2:后端验证订阅归属

你需要在用户首次购买订阅时,将iOS订阅的originalTransactionId(这个ID是订阅的唯一标识,不会随续订改变)与应用内用户ID绑定,存储到你的后端数据库。

当用户发起购买前,把查询到的originalTransactionId发送到后端,验证该订阅是否属于当前登录的应用内用户:

  • 如果后端返回归属不匹配,直接弹窗提示用户更换Apple ID
  • 如果匹配或无活跃订阅,继续执行购买流程

步骤3:代码示例(MAUI/Xamarin环境)

// 封装购买前的验证逻辑
private async Task<bool> ValidateSubscriptionOwnerBeforePurchase()
{
    var billingClient = CrossInAppBilling.Current;
    
    // 先连接到应用内购买服务
    if (!await billingClient.ConnectAsync())
    {
        await DisplayAlert("错误", "无法连接到应用内购买服务", "确定");
        return false;
    }

    try
    {
        // 查询当前Apple ID下的活跃订阅
        var activeSubscriptions = await billingClient.QueryPurchasesAsync(ItemType.Subscription);
        
        if (activeSubscriptions?.Any() == true)
        {
            // 取第一条活跃订阅(多订阅场景可根据业务调整)
            var targetSubscription = activeSubscriptions.First();
            
            // 调用后端接口验证订阅归属
            bool isOwner = await _backendApi.CheckSubscriptionBelongsToUser(
                targetSubscription.OriginalTransactionId, 
                CurrentLoggedInUserId // 当前应用内登录用户的ID
            );

            if (!isOwner)
            {
                await DisplayAlert("提示", "当前登录的Apple ID已绑定其他账号的订阅,请更换Apple ID后重试。", "确定");
                return false;
            }
        }
        
        // 验证通过,允许购买
        return true;
    }
    catch (Exception ex)
    {
        await DisplayAlert("错误", $"验证失败:{ex.Message}", "确定");
        return false;
    }
    finally
    {
        // 断开连接
        await billingClient.DisconnectAsync();
    }
}

// 发起订阅购买的入口方法
private async Task InitiateSubscriptionPurchase()
{
    // 先执行预验证
    if (!await ValidateSubscriptionOwnerBeforePurchase())
    {
        return; // 验证不通过,终止购买流程
    }

    // 验证通过,继续执行购买逻辑
    var billingClient = CrossInAppBilling.Current;
    try
    {
        var purchaseResult = await billingClient.PurchaseAsync(
            "your_subscription_product_id", 
            ItemType.Subscription, 
            "your_payload"
        );
        
        // 处理购买结果...
    }
    catch (Exception ex)
    {
        // 处理购买异常...
    }
}

步骤4:后端接口逻辑

后端需要提供一个接口,接收originalTransactionId和应用内用户ID,查询数据库中是否存在该订阅ID与用户ID的绑定记录:

# 示例:Python后端接口逻辑(伪代码)
@app.route('/api/verify-subscription-owner', methods=['POST'])
def verify_subscription_owner():
    data = request.get_json()
    original_transaction_id = data['originalTransactionId']
    current_user_id = data['currentUserId']
    
    # 查询数据库中该订阅对应的用户ID
    db_record = SubscriptionMapping.query.filter_by(
        original_transaction_id=original_transaction_id
    ).first()
    
    if not db_record:
        # 无绑定记录,说明是新订阅,允许购买
        return jsonify({"isOwner": True})
    
    # 对比绑定的用户ID与当前用户ID
    return jsonify({"isOwner": db_record.user_id == current_user_id})

3. 关键注意事项

  • 必须绑定originalTransactionId:这个ID是iOS订阅的永久唯一标识,续订或恢复订阅时不会改变,是验证归属的核心依据。
  • 客户端预检查+后端最终验证:客户端预检查提升用户体验,后端验证防止客户端被篡改,双重保障更安全。
  • 处理订阅过期场景:如果你的业务不允许用户B用A的Apple ID购买新订阅(即使A的订阅已过期),可以调整后端逻辑,只要存在过绑定记录就拦截;若允许,则只拦截活跃订阅。

内容的提问来源于stack exchange,提问作者basakes

火山引擎 最新活动