Spring Boot集成Amazon Payment Services(Pay Fort)自定义商户页面实现信用卡验证及扣款支付流程问询
Spring Boot集成Amazon Payment Services(Pay Fort)Custom Merchant Page 完整流程
我之前在Spring Boot项目里集成过Pay Fort的Custom Merchant Page,踩过不少文档里没说清的坑,现在把完整的信用卡验证+扣款流程整理出来给你参考:
前置准备
- 先在Pay Fort后台拿到核心参数:
merchant_identifier、access_code、sha_request_phrase、sha_response_phrase - 确定使用环境:测试环境/生产环境,两者的API地址和配置参数完全独立,别搞混
1. 前端自定义支付表单(收集卡信息)
Custom Merchant Page允许你自己搭建支付表单,直接收集用户的信用卡信息,不用跳转Pay Fort的页面:
<form id="paymentForm"> <div> <label>信用卡号:</label> <input type="text" id="cardNumber" placeholder="4111 1111 1111 1111" required> </div> <div> <label>有效期 (MM/YY):</label> <input type="text" id="expiryDate" placeholder="05/25" required> </div> <div> <label>CVV:</label> <input type="text" id="cvv" placeholder="123" required> </div> <div> <label>支付金额:</label> <input type="text" id="amount" value="100.00" required> </div> <button type="button" onclick="submitPayment()">确认支付</button> </form>
用JS把表单数据传给后端接口:
function submitPayment() { const paymentData = { cardNumber: document.getElementById('cardNumber').value.replace(/\s/g, ''), expiryDate: document.getElementById('expiryDate').value, cvv: document.getElementById('cvv').value, amount: document.getElementById('amount').value, currency: "SAR", customerEmail: "user@example.com", customerName: "John Doe" }; fetch('/api/payfort/process-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(paymentData) }) .then(res => res.json()) .then(data => { if (data.status === "SUCCESS") { alert("支付成功!"); window.location.href = "/payment-success"; } else { alert(`支付失败: ${data.message}`); } }) .catch(err => console.error('请求异常:', err)); }
2. Spring Boot后端核心实现
2.1 配置Pay Fort参数
在application.properties里配置核心参数:
# Pay Fort配置 payfort.merchant-id=你的merchant_identifier payfort.access-code=你的access_code payfort.sha-request-phrase=你的sha_request_phrase payfort.sha-response-phrase=你的sha_response_phrase # 测试环境地址,生产环境换成正式地址 payfort.base-url=https://sbpaymentservices.payfort.com/FortAPI/paymentApi
2.2 签名工具类(重中之重)
Pay Fort所有请求/响应都需要SHA签名验证,这是官方文档最模糊的部分,一定要严格按规则来:
import org.apache.commons.codec.digest.DigestUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Map; import java.util.TreeMap; @Component public class PayFortSignatureUtil { @Value("${payfort.sha-request-phrase}") private String shaRequestPhrase; @Value("${payfort.sha-response-phrase}") private String shaResponsePhrase; // 生成请求签名:参数按字母排序 → 首尾拼接sha短语 → SHA256加密转大写 public String generateRequestSignature(Map<String, String> params) { TreeMap<String, String> sortedParams = new TreeMap<>(params); StringBuilder sb = new StringBuilder(shaRequestPhrase); for (Map.Entry<String, String> entry : sortedParams.entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()); } sb.append(shaRequestPhrase); return DigestUtils.sha256Hex(sb.toString()).toUpperCase(); } // 验证响应签名,防止篡改 public boolean validateResponseSignature(Map<String, String> responseParams) { TreeMap<String, String> sortedParams = new TreeMap<>(responseParams); sortedParams.remove("signature"); // 移除签名本身再计算 StringBuilder sb = new StringBuilder(shaResponsePhrase); for (Map.Entry<String, String> entry : sortedParams.entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()); } sb.append(shaResponsePhrase); String calculatedSign = DigestUtils.sha256Hex(sb.toString()).toUpperCase(); return calculatedSign.equals(responseParams.get("signature")); } }
2.3 支付接口实现
创建Controller处理前端请求,调用Pay Fort API完成扣款:
import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/payfort") public class PayFortPaymentController { @Value("${payfort.merchant-id}") private String merchantId; @Value("${payfort.access-code}") private String accessCode; @Value("${payfort.base-url}") private String payFortBaseUrl; private final PayFortSignatureUtil signatureUtil; private final RestTemplate restTemplate; public PayFortPaymentController(PayFortSignatureUtil signatureUtil, RestTemplate restTemplate) { this.signatureUtil = signatureUtil; this.restTemplate = restTemplate; } @PostMapping("/process-payment") public ResponseEntity<Map<String, Object>> processPayment(@RequestBody PaymentRequest request) { // 1. 构建Pay Fort请求参数 Map<String, String> payFortParams = new HashMap<>(); payFortParams.put("command", "PURCHASE"); // PURCHASE直接扣款,AUTHORIZE是预授权后再扣款 payFortParams.put("merchant_identifier", merchantId); payFortParams.put("access_code", accessCode); payFortParams.put("amount", convertToMinorUnit(request.getAmount())); // 金额转最小单位,比如100.00 SAR → 10000 payFortParams.put("currency", request.getCurrency()); payFortParams.put("customer_email", request.getCustomerEmail()); payFortParams.put("customer_name", request.getCustomerName()); payFortParams.put("card_number", request.getCardNumber()); payFortParams.put("expiry_date", request.getExpiryDate().replace("/", "")); // 转成MMYY格式,比如05/25→0525 payFortParams.put("cvv", request.getCvv()); payFortParams.put("language", "en"); payFortParams.put("merchant_reference", generateOrderNo()); // 你的唯一订单号,必须唯一 // 2. 生成签名并加入参数 String signature = signatureUtil.generateRequestSignature(payFortParams); payFortParams.put("signature", signature); // 3. 发送请求到Pay Fort HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Map<String, String>> httpRequest = new HttpEntity<>(payFortParams, headers); try { ResponseEntity<Map> response = restTemplate.postForEntity(payFortBaseUrl, httpRequest, Map.class); Map<String, Object> responseBody = response.getBody(); // 4. 验证响应签名,防止非法请求 if (signatureUtil.validateResponseSignature((Map<String, String>) responseBody)) { String responseCode = (String) responseBody.get("response_code"); if (responseCode.startsWith("00")) { // 支付成功,这里可以更新你的订单状态 responseBody.put("status", "SUCCESS"); return ResponseEntity.ok(responseBody); } else { // 支付失败,返回错误信息 responseBody.put("status", "FAILED"); return ResponseEntity.badRequest().body(responseBody); } } else { return ResponseEntity.badRequest().body(Map.of("status", "FAILED", "message", "响应签名验证失败")); } } catch (Exception e) { return ResponseEntity.internalServerError().body(Map.of("status", "FAILED", "message", "支付请求异常: " + e.getMessage())); } } // 金额转最小单位(元→分,里亚尔→哈拉拉) private String convertToMinorUnit(String amount) { return String.valueOf((int) (Double.parseDouble(amount) * 100)); } // 生成唯一订单号 private String generateOrderNo() { return "ORDER_" + System.currentTimeMillis(); } } // 支付请求DTO class PaymentRequest { private String cardNumber; private String expiryDate; private String cvv; private String amount; private String currency; private String customerEmail; private String customerName; // Getters & Setters public String getCardNumber() { return cardNumber; } public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; } public String getExpiryDate() { return expiryDate; } public void setExpiryDate(String expiryDate) { this.expiryDate = expiryDate; } public String getCvv() { return cvv; } public void setCvv(String cvv) { this.cvv = cvv; } public String getAmount() { return amount; } public void setAmount(String amount) { this.amount = amount; } public String getCurrency() { return currency; } public void setCurrency(String currency) { this.currency = currency; } public String getCustomerEmail() { return customerEmail; } public void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } }
3. 关键注意事项
- 金额格式:必须转成最小单位,比如10.5欧元要传1050,否则会直接报错
- 签名规则:参数必须按字母顺序排序,拼接sha短语的位置不能错(首尾都要加),加密后必须转大写
- 卡信息格式:有效期要去掉斜杠,卡号不能有空格
- 异步回调:如果需要Pay Fort主动通知支付结果,要在后台配置回调URL,并且在后端实现回调接口(用上面的签名工具类验证响应)
- 测试用卡:测试环境可以用
4111 1111 1111 1111,有效期05/25,CVV123来模拟成功支付
内容的提问来源于stack exchange,提问作者Abdalrhman Alkraien
相关产品推荐
相关产品推荐

