针对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. 安全建议
- 使用HTTPS:确保所有通信都通过HTTPS进行
- 身份验证:验证请求的用户身份
- 防止重放攻击:检查交易ID是否已存在
- 定期检查订阅状态:对于订阅类产品,建议定期检查状态
- 使用共享密钥:对于自动续期订阅,在验证请求中包含共享密钥
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验证流程,包括:
- 接收客户端收据数据
- 向Apple服务器验证
- 处理验证结果
- 防止重复交易
- 保存购买记录
- 处理订阅续期
你可以根据实际需求进行调整,比如添加更多的日志记录、监控、或者更复杂的业务逻辑。

浙公网安备 33010602011771号