支付宝沙箱环境是支付宝提供的测试环境,允许开发者在不影响真实交易的情况下进行支付宝支付接口的开发和测试。
步骤概述
-
注册开发者账号:在支付宝开放注册开发者账户,获取应用ID和相关密钥。
-
创建应用:登录开放平台后台,创建应用并获取沙箱环境所需的应用ID、密钥等信息。
-
沙箱环境配置:进入支付宝开放平台的沙箱环境,选择应用并获取沙箱环境所需的测试账号、密钥等信息。
-
接入沙箱环境:使用获取到的沙箱环境信息,调整你的支付宝支付接口代码,替换成沙箱的配置信息。
-
测试支付流程:使用沙箱环境的测试账号进行模拟支付流程,验证接口是否按预期工作。
详细实现
1. 上述准备工作完成后,找到 支付宝 -> 开放平台 -> 沙箱应用:
得到 APPID,接口加签方式选择:系统默认密钥,在公钥模式中 查看 得到 支付宝公钥,应用密钥。
这里 应用网关地址 和 授权回调地址 可以不配置。
2. 在Spring-boot项目中 导入 alipay 的maven依赖。
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
3. 定义AlipayConfigProperties属性类和AlipayConfig类
@ConfigurationProperties(prefix = "alipay.config", ignoreInvalidFields = true)
@Data
public class AlipayConfigProperties {
//在支付宝创建的应用的id
private String app_id = "9021000125600652";
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url = "http://你的地址/payed/notify";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
private String return_url = "http://你的返回页地址";
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "utf-8";
// 支付宝网关;https://openapi.alipaydev.com/gateway.do
private String GATEWAY_URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
private String timeout = "30m";
}
这里的url都要使用公网下可供访问的域名地址或ip地址。
@Slf4j
@Configuration
@EnableConfigurationProperties(AlipayConfigProperties.class)
public class AlipayConfig {
@Bean(name = "localAlipayClient")
@ConditionalOnProperty(value = "alipay.config.enabled", havingValue = "true", matchIfMissing = false)
public AlipayClient buildNativeAlipayClient(AlipayConfigProperties alipayConfigProperties) {
return new DefaultAlipayClient(alipayConfigProperties.getGATEWAY_URL()
, alipayConfigProperties.getApp_id(), alipayConfigProperties.getMerchant_private_key()
, "json", alipayConfigProperties.getCharset(), alipayConfigProperties.getAlipay_public_key(), alipayConfigProperties.getSign_type());
}
@Bean(name = "localAlipayTPPReq")
@ConditionalOnProperty(value = "alipay.config.enabled", havingValue = "true", matchIfMissing = false)
public AlipayTradePagePayRequest buildNativeAlipayTradePagePayRequest(AlipayConfigProperties alipayConfig) {
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(alipayConfig.getReturn_url());
alipayRequest.setNotifyUrl(alipayConfig.getNotify_url());
return alipayRequest;
}
}
通过@Configuration 和 @Bean 将设置了 自己的应用属性的AlipayClient对象和 AlipayTradePagePayRequest对象重新加载入spring工厂中。
4. 接下来攥写两个核心方法,一个是负责创建订单,并向前端返回支付页的方法(/create_pay_order),另一个是 由支付宝回调的异步通知支付成功的方法(/notify)。
@RequestMapping(value = "create_pay_order", produces = "text/html")
@RequestParam Integer productId) throws AlipayApiException {
ShopCartEntity shopCartEntity = ShopCartEntity.builder()
.openid(openid)
.productId(productId).build();
// 创建订单类,可自己重写按需求
PayOrderEntity payOrder = orderService.createOrder(shopCartEntity);
// 返回支付宝的跳转页
String pay = orderService.pay(payOrder, shopCartEntity);
return pay;
}
@Resource(name = "localAlipayClient")
private AlipayClient alipayClient;
@Resource(name = "localAlipayTPPReq")
private AlipayTradePagePayRequest alipayTradePagePayRequest;
@Override
public String pay(PayOrderEntity payOrder, ShopCartEntity shopCartEntity) throws AlipayApiException {
Integer productId = shopCartEntity.getProductId();
// 商品查询
ProductEntity productEntity = orderRepository.queryProduct(productId);
if (!productEntity.isAvailable()) {
throw new ChatGPTException(Constants.ResponseCode.ORDER_PRODUCT_ERR.getCode(), Constants.ResponseCode.ORDER_PRODUCT_ERR.getInfo());
}
// 订单号
String orderId = payOrder.getOrderId();
// 付款金额
String total_amount = String.valueOf(productEntity.getPrice().multiply(new BigDecimal(100)));
// 订单名称
String subject = productEntity.getProductName();
alipayTradePagePayRequest.setBizContent("{\"out_trade_no\":\""+ orderId +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ "null" +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayTradePagePayRequest).getBody();
// 会受到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
log.info("支付宝响应:"+result);
return result;
}
支付宝回调函数,需要对支付宝发布的信息 进行RSA2签名校验,使用支付宝的公钥解密,确保信息未被篡改。
@PostMapping("/payed/notify")
public String handleAliPayed(PayAsyncDTO dto, HttpServletRequest request) {
// 只要收到了支付宝给我们异步的通知,告诉我们订单成功。返回SUCCESS,支付宝就再也不通知。
// 验签
try {
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");
params.put(name, valueStr);
}
//获取支付宝的通知返回参数,可参考技术文档中页面跳转同步通知参数列表(以上仅供参考)//
//计算得出通知验证结果
boolean verify_result = false;
log.info("支付宝回调 验证:{}", verify_result);
if (verify_result) {
// 验证签名
log.info("支付宝回调成功, 签名验证成功...");
// TODO 支付成功,开始 入队 进行订单处理
// ... 执行收单逻辑
return "success";
} else {
log.error("签名验证失败...");
return "error";
}
} catch (AlipayApiException e) {
log.error("签名验证失败...");
return "error";
}
}
@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayAsyncDTO {
private String gmt_create;
private String charset;
private String gmt_payment;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date notify_time;
private String subject;
private String sign;
private String buyer_id;//支付者的id
private String body;//订单的信息
private String invoice_amount;//支付金额
private String version;
private String notify_id;//通知id
private String fund_bill_list;
private String notify_type;//通知类型;trade_status_sync
private String out_trade_no;//订单号
private String total_amount;//支付的总额
private String trade_status;//交易状态 TRADE_SUCCESS
private String trade_no;//流水号
private String auth_app_id;//
private String receipt_amount;//商家收到的款
private String point_amount;//
private String app_id;//应用id
private String buyer_pay_amount;//最终支付的金额
private String sign_type;//签名类型
private String seller_id;//商家的id
}
5. 前端 采用了 ts代码,在 立即付款点击后,调用create_pay_order函数,并将其返回的页面进行渲染加载。
<div className={styles["product-buy"]} onClick={() => payOrder(product.productId)}>
立即购买
</div>
---
const payOrder = async (productId: number) => {
const response = await createPayOrder(productId);
const text = await response.text();
console.log('Received HTML text:', text); // 检查返回的 HTML 文本内容
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const formElement = doc.querySelector('form');
console.log('Found form element:', formElement); // 检查是否成功获取表单元素
if (formElement) {
document.body.appendChild(formElement);
formElement.submit();
} else {
console.error('Unable to find form element or form is invalid.'); // 输出错误信息
}
}
---
export const createPayOrder = (productId: number) => {
return fetch(`${apiHostUrl}/create_pay_order`, {
method: "post",
headers: {
...getHeaders(),
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
},
body: `productId=${productId}`
});
}
6. 效果展示
这里使用第一步中,沙箱账户中的买方账户,进行付款就可以了。
7. 整体流程
在非对称加密中,使用公钥加密通常用于加密信息,而使用私钥加密通常用于数字签名。
- 公钥加密:公钥加密是将信息使用接收方的公钥进行加密,只有拥有私钥的接收方才能解密该信息。这种方式确保了只有指定的接收方能够解密和访问信息,通常用于保护信息在传输过程中的安全性。
- 私钥加密:私钥加密主要用于数字签名。发送方使用自己的私钥对信息进行加密,接收方可以使用发送方的公钥来解密签名,验证信息的来源和完整性。这种方式确保信息的发送方是可信的,并且信息在传输过程中未被篡改。
所以,在非对称加密中,公钥主要用于加密信息以保证机密性,而私钥则用于数字签名以验证身份和确保信息完整性。