一、准备工作
首先去蚂蚁金服的开放平台注册一个开发者账号,右上角免费入住。可以选自研或者企业账号,用支付宝账号注册登录。
蚂蚁金服开放平台
注册好之后进入沙箱环境:
之所以用沙箱是因为正式应用想要使用支付宝提供的功能话要营业执照,个人学习没有,所以用沙箱环境,而且沙箱环境所有的功能都是开放的,在沙箱环境集成了后换到正式的应用,也就是改改配置的事情。
点进去是这样的:
滑到下面有一个沙箱支付宝,可以用来模拟付款,沙箱账号里的钱可以自己充的。支付宝破解版
密钥:
可以看到有几个xxx密钥,也就是几种加密方式,这里用支付宝推荐的RSA2加密。
鼠标滑到RSA2菜单边上那个蓝点上就有一个密钥生成方法,点进去下载对应的工具就可以生成我们自己的公钥和私钥:
然后把生成公钥上传到支付宝(别上传错了):
内网穿透
由于支付宝需要回调接口,通知我们支付结果,所以要有一个外网可以正常访问的网址。个人学习如果没有服务器、域名,可以使用natapp软件。这个软件可以提供免费的域名、把本地ip映射到外网,具体使用请参考博客:https://blog.csdn.net/vbirdbest/article/details/80635880
集成
在开始集成前建议先看一下阿里给的时序图,对整个支付的流程有一个大致的了解:
配置
pom添加依赖:
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.6.0.ALL</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
配置文件
把公钥、私钥、appid等写到配置文件:
这里有一个坑,就是公钥要填支付宝的公钥,而不是自己生成的公钥,不然之后回调会验签失败。
配置类
package com.example.shiroStudy.config.alipay;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author 黄豪琦
* 日期:2019-07-12 16:38
* 说明:
*/
@Data
@Slf4j
@Component
@ConfigurationProperties("pay.alipay")
public class AlipayProperties {
/**
* 商户appid
*/
private String appid;
/**
* 私钥 pkcs8格式的
*/
private String appPrivateKey;
/**
* 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
*/
private String notifyUrl;
/**
* 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
*/
private String returnUrl;
/**
* 请求网关地址
*/
private String gatewayUrl;
/**
* 编码
*/
private String charset;
/**
* 返回格式
*/
private String format;
/**
* 支付宝公钥
*/
private String alipayPublicKey;
/**
* RSA2 签名方式
*/
private String signtype;
}
@ConfigurationProperties注解的作用就是把配置文件中 前缀为"pay.alipay"下的值,赋值到这个类的属性中去。
PayService
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.example.shiroStudy.config.alipay.AlipayProperties;
import com.example.shiroStudy.entity.AlipayParam;
import com.example.shiroStudy.service.AlipayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author 黄豪琦
* 日期:2019-07-16 16:40
* 说明:
*/
@Service("alipayService")
@Slf4j
public class AlipayServiceImpl implements AlipayService {
@Autowired
private AlipayProperties alipayProperties;
@Override
public String toAliPay(AlipayParam param){
if(param == null){
throw new RuntimeException("产品参数为空");
}
//构建客户端
AlipayClient client = new DefaultAlipayClient(alipayProperties.getGatewayUrl(), alipayProperties.getAppid(),
alipayProperties.getAppPrivateKey(), alipayProperties.getFormat(), alipayProperties.getCharset(),
alipayProperties.getAlipayPublicKey(), alipayProperties.getSigntype());
//构建请求对象
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
//设置回调地址
alipayRequest.setReturnUrl(alipayProperties.getReturnUrl());
alipayRequest.setNotifyUrl(alipayProperties.getNotifyUrl());
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setOutTradeNo(param.getOutTradeNo());
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(param.getTotalAmount());
model.setSubject(param.getSubject());
model.setBody(param.getBody());
//设置业务参数
alipayRequest.setBizModel(model);
String form="";
try {
//发起请求
form = client.pageExecute(alipayRequest).getBody();
} catch (AlipayApiException e) {
log.error("支付宝支付异常:" + e.getErrMsg());
}
return form;
}
}
有几个地方解释、注意一下
//构建请求对象
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
支付模式有很多种,网站支付、手机app支付、手机网页支付等,注意不要弄错了,否则会有问题。
private AlipayProperties alipayProperties;
这个就是配置文件里配置的公钥私钥等,用这种形式注入进来的,目的只是要那个值而已,不一定要用这样方式。
alipayRequest.setBizModel(model);
设置业务参数,比如商品名、价格等,设置的方式有两种,一种就是封装这样的一个model类,另一种是
alipayRequest.setBizContent(String json);
注意参数格式一定要是json,可以参照官方文档的:
alipayRequest.setBizContent("{" +
" \"out_trade_no\":\"20150320010101001\"," +
" \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," +
" \"total_amount\":88.88," +
" \"subject\":\"Iphone6 16G\"," +
" \"body\":\"Iphone6 16G\"," +
" \"passback_params\":\"merchantBizType%3d3C%26merchantBizNo%3d2016010101111\"," +
" \"extend_params\":{" +
" \"sys_service_provider_id\":\"2088511833207846\"" +
" }"+
" }");//填充业务参数
支付宝的返回:
支付宝返回来的就是一个表单,里面封装了一些参数。可以把这个表单直接传给前台,浏览器一解析,就会提交到支付宝的域名下,然后用户扫码支付。
PayController
@Autowired
private AlipayService alipayService;
@Autowired
private AlipayProperties alipayProperties;
@GetMapping("/pay")
public String pay(ModelMap map){
String result = "";
try {
result = alipayService.toAliPay(new AlipayParam(
StringUtils.getUUID(),
"100",
"迪迦奥特曼",
"正品奥特曼"
));
} catch (IOException e) {
e.printStackTrace();
}
map.addAttribute("form", result);
return "alipay";
}
@PostMapping("/notify")
public String notifys(HttpServletRequest request){
Map<String, String> paramsMap = convertRequestParamsToMap(request);
log.info("异步回调");
String trade_status= paramsMap.get("trade_status");
try {
boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap,
alipayProperties.getAlipayPublicKey(),
alipayProperties.getCharset(),
alipayProperties.getSigntype());
//无论同步异步都要验证签名
log.info("异步验签结果:"+ signVerified);
if(signVerified){
if("TRADE_FINISHED".equals(trade_status) || "TRADE_SUCCESS".equals(trade_status)){
//处理自己系统的业务逻辑,如:将支付记录状态改为成功,需要返回一个字符串success告知支付宝服务器
log.info("支付成功");
return "success";
} else {
//支付失败不处理业务逻辑
return "failure";
}
}else {
//签名验证失败不处理业务逻辑
return "failure";
}
} catch (AlipayApiException e) {
e.printStackTrace();
return "failure";
}
}
@GetMapping("/return")
public String returns(HttpServletRequest request){
log.info("同步回调");
Map<String, String> paramsMap = convertRequestParamsToMap(request);
try {
boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap,
alipayProperties.getAlipayPublicKey(),
alipayProperties.getCharset(),
alipayProperties.getSigntype());
log.info("同步验签结果:"+ signVerified);
if(signVerified){
//跳转支付成功界面
return "success";
}else {
//跳转支付失败界面
return "failure";
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
return "success";
}
代码解释:
这个只是我为了约束参数定义的类,传map什么的都可以,只要不搞错。注意订单号需要是唯一的,就是在你数据库里是唯一的,我这里用的uuid,自己发挥。
AlipayParam
package com.example.shiroStudy.entity;
/**
* @author 黄豪琦
* 日期:2019-07-16 16:51
* 说明: 支付宝支付参数模型
*/
public class AlipayParam {
private String totalAmount;
private String subject;
private String body;
private String outTradeNo;
/**
*
* @param outTradeNo 订单号
* @param totalAmount 金额
* @param subject 商品标题
* @param body 商品描述
*/
public AlipayParam(String outTradeNo, String totalAmount, String subject, String body) {
if (outTradeNo == null || totalAmount == null || subject == null){
throw new NullPointerException("支付宝支付参数不能为空");
}
if(body == null){
this.body = "此商品没有描述";
}
this.totalAmount = totalAmount;
this.subject = subject;
this.body = body;
this.outTradeNo = outTradeNo;
}
public String getOutTradeNo() {
return outTradeNo;
}
public String getTotalAmount() {
return totalAmount;
}
public String getSubject() {
return subject;
}
public String getBody() {
return body;
}
}
这里的success是个字符串,是返回给支付宝的,不是跳页面的,如果这里不返回或者返回太慢,支付宝会重复调用。
遇到的坑
回调验签失败
排除掉密钥错误,那可能是这个问题:
这个方法是有重载方法的,如果最后一个参数[加密方式]不加,那么调用的就是RSA的加密方法,而我们用的是RSA2加密方式。
网关问题
支付宝官方文档中用的网关是生产环境用的网关,沙箱环境的网关是这个https://openapi.alipaydev.com/gateway.do,相互弄错了就会有问题。