Struts 1.3 中应使用哪个 API 验证信用卡并处理支付?
结论
Struts 1.3 没有验证信用卡真实性或处理付款的标准 API。GenericValidator.isCreditCard() 只能检查卡号的格式,例如长度、字符和校验位。它无法确认卡片是否真实存在、是否过期、CVV 是否正确,也不能完成扣款。
实际支付需要接入收单机构或支付服务商的支付网关 API。建议优先使用服务商托管的支付页面、Hosted Fields 或前端 Tokenization,避免信用卡号和 CVV 经过 Struts 应用服务器。
如何划分验证和支付
一次信用卡支付通常分为三层处理:
-
页面验证
检查必填项以及月份、年份的格式,减少用户输入错误。这类验证只能改善用户体验,不能作为安全保障。 -
服务端基础验证
检查卡号格式、过期日期、金额和订单状态等。GenericValidator.isCreditCard()属于这一层,只能识别明显的输入错误。 -
支付网关验证和授权
支付服务商连接发卡行,检查卡片状态、CVV、余额或信用额度,并执行 3-D Secure 等认证。付款能否获得授权,最终由这一层决定。
即使 GenericValidator.isCreditCard() 返回 true,也不代表这张卡能够完成付款。
推荐的处理流程
较安全的支付流程可以这样设计:
- Struts 页面加载支付服务商提供的托管表单或 Hosted Fields。
- 浏览器把卡号、有效期和 CVV 直接提交给支付服务商。
- 支付服务商返回一次性的 payment token 或 payment method ID。
- 页面将该 token 和订单编号提交给 Struts
Action。 - 服务端重新读取订单的应付金额,再调用支付网关 API。
- 根据 API 的返回结果,将订单标记为待处理、成功或失败。
- 通过支付网关的 webhook 再次确认最终支付状态。
最终付款金额不能由浏览器决定,也不能只根据支付成功页面或跳转参数更新订单状态。
Struts 1.3 中的代码结构
不同支付网关的接口各不相同。下面的代码只用于说明合理的分层方式,不对应任何特定服务商。
public interface PaymentGateway {
PaymentResult authorize(
String paymentToken,
long amountInMinorUnits,
String currency,
String orderId,
String idempotencyKey
) throws PaymentGatewayException;
}
PaymentResult 应保存网关返回的交易编号和具体状态,不要只用一个布尔值表示结果:
public final class PaymentResult {
private final String transactionId;
private final PaymentStatus status;
private final String failureCode;
public PaymentResult(
String transactionId,
PaymentStatus status,
String failureCode) {
this.transactionId = transactionId;
this.status = status;
this.failureCode = failureCode;
}
public String getTransactionId() {
return transactionId;
}
public PaymentStatus getStatus() {
return status;
}
public String getFailureCode() {
return failureCode;
}
}
public enum PaymentStatus {
SUCCEEDED,
PENDING,
DECLINED
}
Struts Action 只接收支付 token,不接收原始卡号和 CVV:
public class SubmitPaymentAction extends Action {
private PaymentGateway paymentGateway;
private OrderService orderService;
@Override
public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
PaymentForm paymentForm = (PaymentForm) form;
String orderId = paymentForm.getOrderId();
String paymentToken = paymentForm.getPaymentToken();
Order order = orderService.findPayableOrder(orderId);
if (order == null || paymentToken == null
|| paymentToken.trim().length() == 0) {
return mapping.findForward("invalid");
}
/*
* 金额必须从服务端订单中读取。
* 例如 1099 表示 10.99 个货币单位,避免使用浮点数处理金额。
*/
long amount = order.getAmountInMinorUnits();
try {
PaymentResult result = paymentGateway.authorize(
paymentToken,
amount,
order.getCurrency(),
order.getId(),
order.getPaymentAttemptId()
);
if (result.getStatus() == PaymentStatus.SUCCEEDED) {
orderService.recordAuthorizedPayment(
order.getId(),
result.getTransactionId()
);
return mapping.findForward("success");
}
if (result.getStatus() == PaymentStatus.PENDING) {
orderService.markPaymentPending(
order.getId(),
result.getTransactionId()
);
return mapping.findForward("pending");
}
orderService.recordPaymentFailure(
order.getId(),
result.getFailureCode()
);
return mapping.findForward("declined");
} catch (PaymentGatewayException ex) {
/*
* 网络超时不一定表示付款失败。
* 应保留本次支付尝试,并通过交易查询接口或 webhook 确认状态。
*/
orderService.markPaymentUnknown(order.getId());
return mapping.findForward("pending");
}
}
}
具体的 PaymentGateway 实现应调用所选支付服务商的官方 SDK 或 HTTPS API,并按照服务商文档处理认证、签名、超时、重试和 webhook。
服务端检查有效期
如果页面仍分别提交月份和年份,可以先做基础检查:
YearMonth expiration;
try {
int month = Integer.parseInt(paymentForm.getExpirationMonth());
int year = Integer.parseInt(paymentForm.getExpirationYear());
expiration = YearMonth.of(year, month);
} catch (DateTimeException | NumberFormatException ex) {
errors.add("expirationDate",
new ActionMessage("error.payment.expiration.invalid"));
return errors;
}
YearMonth currentMonth = YearMonth.now(ZoneOffset.UTC);
if (expiration.isBefore(currentMonth)) {
errors.add("expirationDate",
new ActionMessage("error.payment.expiration.expired"));
}
YearMonth 适用于 Java 8 及以上版本。如果旧项目使用更早的 Java 版本,需要通过 Calendar 或其他日期库实现相同的月份比较。无论采用哪种方式,本地检查都不能代替支付网关的授权结果。
选择支付 API 时需要考虑什么
信用卡支付没有适用于所有场景的统一 Java API。选择支付服务商时,需要考虑业务所在的国家或地区、结算币种、商户账户和合规要求,并确认服务商支持:
- 托管支付页面或 Hosted Fields;
- 前端 Tokenization,避免服务器接触卡号和 CVV;
- 3-D Secure 等持卡人认证;
- webhook 签名验证;
- 幂等键,防止重复提交导致重复扣款;
- 授权、扣款、撤销和退款;
- 交易状态查询;
- 官方支持的 Java SDK,或文档明确的 HTTPS API;
- 当前适用的 PCI DSS 合规方案。
安全注意事项
不要将信用卡号、CVV 或完整的支付请求写入数据库、Session、访问日志或异常日志。授权完成后尤其不能保存 CVV。
还需要注意以下事项:
- 所有页面和接口都必须使用 HTTPS;
- 支付金额、币种和订单状态由服务端确定;
- 提交支付时使用一次性 token 和幂等键;
- 验证 webhook 的签名和事件标识;
- 支付请求超时后主动查询交易状态,不要直接再次扣款;
- 页面只显示卡号末四位等必要信息;
- 支付密钥存放在安全配置或密钥管理系统中,不要写入 JSP 或源码;
- 不要通过异常信息向用户暴露支付网关的内部细节。
GenericValidator.isCreditCard() 可以继续用于检查输入格式,但卡片验证和付款必须交给支付网关。新的实现应让 Struts 应用只处理支付 token、订单和交易状态,尽量避免接触原始信用卡数据。
备注:内容仅供参考。
