针对Java服务器端的IAP收据验证

针对Java服务器端的IAP收据验证,我会为你设计一个完整的解决方案。以下是基于Spring Boot的实现方案:

1. 首先创建接收验证请求的Controller

@RestController
@RequestMapping("/api/iap")
public class IapVerificationController {
    
    private final IapVerificationService iapVerificationService;
    
    public IapVerificationController(IapVerificationService iapVerificationService) {
        this.iapVerificationService = iapVerificationService;
    }
    
    @PostMapping("/verify")
    public ResponseEntity<ApiResponse> verifyReceipt(
            @RequestBody ReceiptVerificationRequest request,
            @RequestHeader(value = "Authorization") String authHeader) {
        
        // 验证用户身份
        String userId = validateUserToken(authHeader);
        
        // 调用验证服务
        VerificationResult result = iapVerificationService.verifyReceipt(
            request.getReceiptData(),
            request.getProductId(),
            request.getTransactionId(),
            userId
        );
        
        return ResponseEntity.ok(ApiResponse.success(result));
    }
    
    private String validateUserToken(String authHeader) {
        // 实现你的用户认证逻辑
        // 返回用户ID
    }
}

// 请求DTO
@Data
public class ReceiptVerificationRequest {
    private String receiptData;  // Base64编码的收据数据
    private String productId;    // 产品ID
    private String transactionId; // 交易ID
}

// 响应DTO
@Data
public class VerificationResult {
    private boolean isValid;
    private String latestReceipt; // 最新的收据数据(用于订阅续期)
    private Date expiresDate;     // 过期时间(订阅类产品)
    private String productId;     // 验证后的产品ID
}

2. 创建验证服务实现

@Service
public class IapVerificationService {
    
    private static final String APPLE_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt";
    private static final String APPLE_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt";
    
    private final RestTemplate restTemplate;
    private final PurchaseRecordRepository purchaseRecordRepository;
    
    public IapVerificationService(RestTemplateBuilder restTemplateBuilder, 
                                PurchaseRecordRepository purchaseRecordRepository) {
        this.restTemplate = restTemplateBuilder.build();
        this.purchaseRecordRepository = purchaseRecordRepository;
    }
    
    public VerificationResult verifyReceipt(String receiptData, 
                                         String productId,
                                         String transactionId,
                                         String userId) {
        
        // 1. 先尝试生产环境验证
        VerificationResponse response = verifyWithApple(APPLE_PRODUCTION_URL, receiptData);
        
        // 2. 如果是沙箱环境收据(21007状态码),则用沙箱环境验证
        if (response.getStatus() == 21007) {
            response = verifyWithApple(APPLE_SANDBOX_URL, receiptData);
        }
        
        // 3. 验证响应状态
        if (response.getStatus() != 0) {
            throw new IapVerificationException("Apple验证失败,状态码: " + response.getStatus());
        }
        
        // 4. 检查购买的产品是否匹配
        if (!isProductValid(response, productId)) {
            throw new IapVerificationException("购买产品不匹配");
        }
        
        // 5. 检查是否重复交易(防止重复处理)
        checkDuplicateTransaction(transactionId, userId);
        
        // 6. 解析并保存购买记录
        return savePurchaseRecord(response, userId);
    }
    
    private VerificationResponse verifyWithApple(String url, String receiptData) {
        Map<String, String> request = new HashMap<>();
        request.put("receipt-data", receiptData);
        // 可以添加密码(如果有)
        // request.put("password", "your_shared_secret");
        
        return restTemplate.postForObject(url, request, VerificationResponse.class);
    }
    
    private boolean isProductValid(VerificationResponse response, String expectedProductId) {
        // 检查购买的产品ID是否匹配
        // 对于订阅,需要检查latest_receipt_info中的product_id
        return response.getReceipt().getProductId().equals(expectedProductId);
    }
    
    private void checkDuplicateTransaction(String transactionId, String userId) {
        if (purchaseRecordRepository.existsByTransactionIdAndUserId(transactionId, userId)) {
            throw new IapVerificationException("重复的交易ID");
        }
    }
    
    private VerificationResult savePurchaseRecord(VerificationResponse response, String userId) {
        PurchaseRecord record = new PurchaseRecord();
        record.setUserId(userId);
        record.setProductId(response.getReceipt().getProductId());
        record.setTransactionId(response.getReceipt().getTransactionId());
        record.setPurchaseDate(new Date(response.getReceipt().getPurchaseDateMs()));
        record.setExpiresDate(response.getReceipt().getExpiresDateMs() != null ? 
                            new Date(response.getReceipt().getExpiresDateMs()) : null);
        record.setOriginalResponse(JsonUtils.toJson(response));
        
        purchaseRecordRepository.save(record);
        
        VerificationResult result = new VerificationResult();
        result.setValid(true);
        result.setProductId(record.getProductId());
        result.setExpiresDate(record.getExpiresDate());
        result.setLatestReceipt(response.getLatestReceipt()); // 用于订阅续期
        
        return result;
    }
}

3. 响应数据结构

@Data
public class VerificationResponse {
    private int status;  // 状态码(0表示成功)
    private Receipt receipt;
    private String latest_receipt; // 最新的收据数据(订阅)
    private List<Receipt> latest_receipt_info; // 订阅的最新收据信息
    
    @Data
    public static class Receipt {
        private String product_id;
        private String transaction_id;
        private Long purchase_date_ms;
        private Long expires_date_ms;  // 订阅过期时间
        // 其他可能的字段...
    }
}

4. 异常处理

@ControllerAdvice
public class IapExceptionHandler {
    
    @ExceptionHandler(IapVerificationException.class)
    public ResponseEntity<ApiResponse> handleIapException(IapVerificationException ex) {
        return ResponseEntity.badRequest().body(ApiResponse.error(ex.getMessage()));
    }
}

public class IapVerificationException extends RuntimeException {
    public IapVerificationException(String message) {
        super(message);
    }
}

5. 数据库实体

@Entity
@Table(name = "iap_purchase_records")
@Data
public class PurchaseRecord {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String userId;
    
    @Column(nullable = false)
    private String productId;
    
    @Column(nullable = false, unique = true)
    private String transactionId;
    
    @Column(nullable = false)
    private Date purchaseDate;
    
    @Column
    private Date expiresDate; // 对于订阅有效
    
    @Column(columnDefinition = "TEXT")
    private String originalResponse; // 原始响应数据
    
    @Column(nullable = false)
    private Date createdAt = new Date();
}

6. 安全建议

  1. 使用HTTPS:确保所有通信都通过HTTPS进行
  2. 身份验证:验证请求的用户身份
  3. 防止重放攻击:检查交易ID是否已存在
  4. 定期检查订阅状态:对于订阅类产品,建议定期检查状态
  5. 使用共享密钥:对于自动续期订阅,在验证请求中包含共享密钥

7. 客户端调用示例

// 在之前的validate_Receipt_With_Backend方法中实现:
- (void)validate_Receipt_With_Backend:(NSString *)receipt_base64 
                          product_id:(NSString *)product_id 
                     transaction_id:(NSString *)transaction_id 
                        completion:(void(^)(BOOL success, NSError *error))completion {
    
    // 构建请求参数
    NSDictionary *params = @{
        @"receipt_data": receipt_base64,
        @"product_id": product_id,
        @"transaction_id": transaction_id
    };
    
    // 获取用户token(如果有)
    NSString *authToken = [self getCurrentUserAuthToken];
    
    // 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://your-server.com/api/iap/verify"]];
    request.HTTPMethod = @"POST";
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setValue:[NSString stringWithFormat:@"Bearer %@", authToken] forHTTPHeaderField:@"Authorization"];
    
    NSError *jsonError;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&jsonError];
    if (jsonError) {
        completion(NO, jsonError);
        return;
    }
    request.HTTPBody = jsonData;
    
    // 发送请求
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            completion(NO, error);
            return;
        }
        
        NSError *parseError;
        NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
        if (parseError) {
            completion(NO, parseError);
            return;
        }
        
        BOOL success = [responseDict[@"success"] boolValue];
        if (success) {
            completion(YES, nil);
        } else {
            NSString *errorMsg = responseDict[@"message"] ?: @"服务器验证失败";
            NSError *serverError = [NSError errorWithDomain:@"IAPErrorDomain" 
                                                     code:-1 
                                                 userInfo:@{NSLocalizedDescriptionKey: errorMsg}];
            completion(NO, serverError);
        }
    }];
    
    [task resume];
}

8. 订阅状态检查定时任务

对于订阅类产品,建议添加定时任务检查订阅状态:

@Scheduled(cron = "0 0 12 * * ?") // 每天中午12点执行
public void checkSubscriptionStatus() {
    List<PurchaseRecord> subscriptions = purchaseRecordRepository.findSubscriptionsNeedingVerification();
    
    for (PurchaseRecord record : subscriptions) {
        try {
            VerificationResponse response = verifyWithApple(
                record.isSandbox() ? APPLE_SANDBOX_URL : APPLE_PRODUCTION_URL,
                record.getOriginalReceipt()
            );
            
            // 更新订阅状态
            updateSubscriptionStatus(record, response);
        } catch (Exception e) {
            log.error("验证订阅状态失败: {}", record.getTransactionId(), e);
        }
    }
}

这个设计提供了完整的IAP验证流程,包括:

  1. 接收客户端收据数据
  2. 向Apple服务器验证
  3. 处理验证结果
  4. 防止重复交易
  5. 保存购买记录
  6. 处理订阅续期

你可以根据实际需求进行调整,比如添加更多的日志记录、监控、或者更复杂的业务逻辑。

posted @ 2025-07-12 09:46  CH520  阅读(36)  评论(0)    收藏  举报