微信jsapi支付是通过扫码后,直接进入微信支付页
说明一下,jsapi我采用的是公众号获取,需要在公众号上配置域名才可以回调获取openID,当然你可以直接配置回调在你上面的地址,但是我由于特殊原因不能够使用配置的域名,所以要通过nginx转发
前置条件(申请jsapi ,公共号配置回调域名)
正常流程是:编写接口->下载并配置ngrok实现内网穿透(获取到公网ip)->接口生成二维码->手机微信扫码,接口配置回调接口(ngrok获取到的公网ip)->回调接口收到通知,通过code获取到openId
我的流程是:编写接口->下载并配置ngrok实现内网穿透(获取到公网ip)->配置nginx将公众号配置的域名转发到我的公网ip上->接口生成二维码->手机微信扫码
如果公众号能直接配置内网穿透的域名就直接配置即可,我也就多了配置nginx这一步
流程:首先写一个接口,这个接口是通过手机微信扫码访问的,必须通过微信扫码,不然会出现请在微信客户端访问链接,访问后微信不会返回东西,需要通过另外一个公网可访问的接口来给微信调用,获取到code,拿到code再去获取openid实现扫码支付
问题:java如果是本地调试是无法拿到微信的回调的,但是部署到服务器又不好调试,所以采用内网穿透的形式我这边使用的ngrok,至于netapp和花生壳好像是收费的而且实名啥的比较麻烦
ngrok安装与配置:
1.首先进入官网:ngrok | Unified Application Delivery Platform for Developers,注册账号到这个页面
2. 点击下载,解压
3.获取授权码
4.拷贝授权码,然后运行刚刚的exe文件,他会放一个yml文件再c盘不用管只要生成成功就没问题
ngrok authtoken 你的授权码
5.运行命令
ngrok http 8080
8080是我网关的端口,根据自己项目自行配置
这样就ok, https://bbdd-240e-398-92-80d0-484c-b30b-166f-846d.ngrok-free.app 是自己的公网ip,之后我们就要想办法把回调转到这里访问
后端配置:
配置手机扫码访问接口,必须手机微信客户端扫码访问接口,不然会报错!!!!
1.编写接口:
2.生成二维码访问:
这样直接用微信扫码就可以访问接口
3.配置回调地址马赛克的地方是自己的公众号配置的域名,我配置了个local是用于nginx转发到自己内网穿透的地址,公众号配置的域名就是内网穿透的地址的话忽略这一步
rewrite ^/local(.*)$ $1 break;意思是删除local不然local也会拼接上去
4.编写回调接口:
/** 回调地址 **/
@RequestMapping("/oauth2Callback/{aesData}")
public void oauth2Callback(@PathVariable("aesData") String aesData,HttpServletRequest request) throws Exception {
System.out.println("进入回调");
System.out.println(aesData);
System.out.println(request.getParameterMap());
System.out.println(request.getParameter("code"));
}
5.成功回调
6.获取 openid,需要有appid,appsecret,code,如下所示
public String getChannelUserId(JSONObject reqParams, MchAppConfigContext mchAppConfigContext) {
String code = reqParams.getString("code");
String redirectUrl = String.format("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
mchAppConfigContext.getAppId(), mchAppConfigContext.getChannel().getWxAppSecret(), code);
try {
JSONObject jsonObject = JSON.parseObject(restTemplate.getForObject(redirectUrl, String.class));
return jsonObject.get("openid").toString();
} catch (NullPointerException e) {
log.error("获取openid失败=={}",reqParams);
}
return null;
}
7.获取prepayId将相应的参数填入jsapiService,这个是微信官方的demo中的代码,直接拿来改的
/**
* JSAPI支付下单
*/
public PrepayResponse prepay(Map<String, Object> map) {
String orderNo = map.get(PayConstants.ORDERNO).toString();
String openId = map.get(PayConstants.OPENID).toString();
Integer money = (Integer) map.get(PayConstants.AMOUNT);
String notifyUrl = map.get(PayConstants.NOTIFY_URL).toString();
PrepayRequest request = new PrepayRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
Amount amount = new Amount();
amount.setTotal(money);
request.setAmount(amount);
request.setAppid(wxpayProperties.getAppid());
request.setMchid(wxpayProperties.getMerchantId());
request.setDescription("测试商品标题");
request.setNotifyUrl(notifyUrl);
request.setOutTradeNo(orderNo);
//获取openID
Payer payer = new Payer();
payer.setOpenid(openId);
request.setPayer(payer);
// 调用接口
return jsapiService.prepay(request);
}
官方代码如下
package com.wechat.pay.java.service.payments.jsapi;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByIdRequest;
import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest;
import com.wechat.pay.java.service.payments.model.Transaction;
/** JsapiService使用示例 */
public class JsapiServiceExample {
/** 商户号 */
public static String merchantId = "190000****";
/** 商户API私钥路径 */
public static String privateKeyPath = "/Users/yourname/your/path/apiclient_key.pem";
/** 商户证书序列号 */
public static String merchantSerialNumber = "5157F09EFDC096DE15EBE81A47057A72********";
/** 商户APIV3密钥 */
public static String apiV3Key = "...";
public static JsapiService service;
public static void main(String[] args) {
// 初始化商户配置
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
// 初始化服务
service = new JsapiService.Builder().config(config).build();
// ... 调用接口
try {
closeOrder();
} catch (HttpException e) { // 发送HTTP请求失败
// 调用e.getHttpRequest()获取请求打印日志或上报监控,更多方法见HttpException定义
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
// 调用e.getResponseBody()获取返回体打印日志或上报监控,更多方法见ServiceException定义
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
// 调用e.getMessage()获取信息打印日志或上报监控,更多方法见MalformedMessageException定义
}
}
/** 关闭订单 */
public static void closeOrder() {
CloseOrderRequest request = new CloseOrderRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
service.closeOrder(request);
}
/** JSAPI支付下单 */
public static PrepayResponse prepay() {
PrepayRequest request = new PrepayRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.prepay(request);
}
/** 微信支付订单号查询订单 */
public static Transaction queryOrderById() {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.queryOrderById(request);
}
/** 商户订单号查询订单 */
public static Transaction queryOrderByOutTradeNo() {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
// 调用接口
return service.queryOrderByOutTradeNo(request);
}
}
8.获取到prepayId后,整合thymeleaf掉起支付
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在templates下写一个pay.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支付页面</title>
</head>
<body>
<script th:inline="javascript">
/*<![CDATA[*/
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": /*[[${appId}]]*/,
"timeStamp": /*[[${timeStamp}]]*/,
"nonceStr": /*[[${nonce}]]*/,
"package": "prepay_id=" + /*[[${prepay_id}]]*/,
"signType": "RSA",
"paySign": /*[[${base64Signature}]]*/
},
function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功后的逻辑
console.log("支付成功");
} else {
// 支付失败后的逻辑
console.error("支付失败");
}
});
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
/*]]>*/
</script>
</body>
</html>
9. 回调接口返回对应页面并将值传过去
/**
* 回调地址
**/
@RequestMapping("/oauth2Callback/{aesData}")
public String oauth2Callback(@PathVariable("aesData") String aesData, Model model) throws Exception, TemplateInputException {
JSONObject callbackData = JSON.parseObject(JeepayKit.aesDecode(aesData));
String mchNo = callbackData.getString("mchNo");
String appId = callbackData.getString("appId");
String channel = callbackData.getString("channel");
String notifyUrl = callbackData.getString("notifyUrl");
String extParam = callbackData.getString("extParam");
// 获取接口
String ifCode = "wxpay";
IChannelUserService channelUserService = SpringBeansUtil.getBean(ifCode + "ChannelUserService", IChannelUserService.class);
//获取商户配置信息
MchAppConfigContext mchAppConfigContext = configContextQueryService.queryMchInfoAndAppInfo(mchNo, channel);
if(channelUserService == null){
throw new BizException("不支持的客户端");
}
//获取到openId
String openId = channelUserService.getChannelUserId(getReqParamJSON(), mchAppConfigContext);
Map<String, Object> map = JSONObject.parseObject(extParam,Map.class);
map.put("openid",openId);
//去下单获取prepay_id
String prepayId = wxpayJSApiPayService.gotoJSApiPay(map);
Map<String, String> stringStringMap = buildPayMap(appId, prepayId);
model.addAttribute("appId", appId);
model.addAttribute("timeStamp", stringStringMap.get("timeStamp"));
model.addAttribute("nonce", stringStringMap.get("nonceStr"));
model.addAttribute("prepay_id", prepayId);
model.addAttribute("base64Signature", stringStringMap.get("paySign"));
return "pay";
}
10.签名:
首先将4个参数保存在list当中,构建参数:
/**
* 构建参数
*
* @param signMessage
* @return
*/
public static String buildSignMessage(List<String> signMessage) {
if (signMessage != null && signMessage.size() > 0) {
StringBuilder sbf = new StringBuilder();
Iterator var2 = signMessage.iterator();
while (var2.hasNext()) {
String str = (String) var2.next();
sbf.append(str).append("\n");
}
return sbf.toString();
} else {
return null;
}
}
获取签名:要使用到privateKey(我这边是直接保存到数据库的,如果保存在目录的自行解析)
public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return StrUtil.str(Base64.encode(signed));
}
完整接口代码:
@Controller
@RequestMapping("/api/channelUserId")
@Slf4j
public class ChannelUserIdController extends ApiController {
@Autowired
private ConfigContextQueryService configContextQueryService;
@Autowired
private IChannelService channelService;
@Autowired
private IUserService userService;
@Autowired
protected HttpServletResponse response; //自动注入response
@Autowired
private RestTemplate restTemplate;
@DubboReference
private WxpayJSApiPayService wxpayJSApiPayService;
/**
* 重定向到微信地址
**/
@GetMapping("/jump")
@ResponseBody
public void jump() throws Exception {
//获取请求数据
String ifCode = "wxpay";
ChannelUserIdRQ rq = getRQByWithMchSign(ChannelUserIdRQ.class);
// 获取接口
IChannelUserService channelUserService = SpringBeansUtil.getBean(ifCode + "ChannelUserService", IChannelUserService.class);
if (channelUserService == null) {
throw new BizException("不支持的客户端");
}
ChannelVo channelVo = channelService.queryByChannelNo(rq.getChannelNo());
JSONObject jsonObject = new JSONObject();
jsonObject.put("mchNo",rq.getMchNo());
jsonObject.put("appId", channelVo.getAppid());
jsonObject.put("ifCode", ifCode);
jsonObject.put("channel", rq.getChannelNo());
jsonObject.put("notifyUrl", rq.getNotifyUrl());
jsonObject.put("extParam", Base64.decodeStr(rq.getExtParam()));
log.info("额外参数值===={}", Base64.decodeStr(rq.getExtParam()));
//回调地址
String callbackUrl = URLUtil.encodeAll("域名" + "/local/payproc/api/channelUserId/oauth2Callback/" + JeepayKit.aesEncode(jsonObject.toString()));
//获取商户配置信息
MchAppConfigContext mchAppConfigContext = configContextQueryService
.queryMchInfoAndAppInfo(rq.getMchNo(), channelVo.getChannelNo().toString());
String redirectUrl = channelUserService.buildUserRedirectUrl(callbackUrl, mchAppConfigContext);
response.sendRedirect(redirectUrl);
}
/**
* 回调地址
**/
@RequestMapping("/oauth2Callback/{aesData}")
public String oauth2Callback(@PathVariable("aesData") String aesData, Model model) throws Exception, TemplateInputException {
JSONObject callbackData = JSON.parseObject(JeepayKit.aesDecode(aesData));
String mchNo = callbackData.getString("mchNo");
String appId = callbackData.getString("appId");
String channel = callbackData.getString("channel");
String notifyUrl = callbackData.getString("notifyUrl");
String extParam = callbackData.getString("extParam");
// 获取接口
String ifCode = "wxpay";
IChannelUserService channelUserService = SpringBeansUtil.getBean(ifCode + "ChannelUserService", IChannelUserService.class);
//获取商户配置信息
MchAppConfigContext mchAppConfigContext = configContextQueryService.queryMchInfoAndAppInfo(mchNo, channel);
if(channelUserService == null){
throw new BizException("不支持的客户端");
}
//获取到openId
String openId = channelUserService.getChannelUserId(getReqParamJSON(), mchAppConfigContext);
Map<String, Object> map = JSONObject.parseObject(extParam,Map.class);
map.put("openid",openId);
//去下单获取prepay_id
String prepayId = wxpayJSApiPayService.gotoJSApiPay(map);
Map<String, String> stringStringMap = buildPayMap(appId, prepayId);
model.addAttribute("appId", appId);
model.addAttribute("timeStamp", stringStringMap.get("timeStamp"));
model.addAttribute("nonce", stringStringMap.get("nonceStr"));
model.addAttribute("prepay_id", prepayId);
model.addAttribute("base64Signature", stringStringMap.get("paySign"));
return "pay";
}
public static Map<String, String> buildPayMap(String appId, String prepayId) throws Exception {
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000L);
String nonceStr = String.valueOf(System.currentTimeMillis());
String packageStr = "prepay_id=" + prepayId;
Map<String, String> packageParams = new HashMap(6);
packageParams.put("appId", appId);
packageParams.put("timeStamp", timeStamp);
packageParams.put("nonceStr", nonceStr);
packageParams.put("package", packageStr);
packageParams.put("signType", "RSA");
ArrayList<String> list = new ArrayList();
list.add(appId);
list.add(timeStamp);
list.add(nonceStr);
list.add(packageStr);
PrivateKey privateKey = convertToPrivateKey("");
String packageSign = encryptByPrivateKey(buildSignMessage(list), privateKey);
packageParams.put("paySign", packageSign);
return packageParams;
}
public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return StrUtil.str(Base64.encode(signed));
}
/**
* 构建参数
*
* @param signMessage
* @return
*/
public static String buildSignMessage(List<String> signMessage) {
if (signMessage != null && signMessage.size() > 0) {
StringBuilder sbf = new StringBuilder();
Iterator var2 = signMessage.iterator();
while (var2.hasNext()) {
String str = (String) var2.next();
sbf.append(str).append("\n");
}
return sbf.toString();
} else {
return null;
}
}
public static PrivateKey convertToPrivateKey(String privateKeyString) throws Exception {
// 移除PEM格式的私钥中的首尾标识,例如 -----BEGIN PRIVATE KEY-----
privateKeyString = privateKeyString.replaceAll("-----\\w+ PRIVATE KEY-----", "");
// 去除换行符、空格等字符
privateKeyString = privateKeyString.replaceAll("\\s", "");
// 将Base64编码的私钥字符串解码为字节数组
byte[] privateKeyBytes = Base64.decode(privateKeyString);
// 构造PKCS8EncodedKeySpec对象
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
// 获取RSA密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 生成私钥对象
return keyFactory.generatePrivate(keySpec);
}
}
注意一定要用@Controller,jump接口一定要写@ResponseBody 不然会报错template might not exist or might not be accessible by any of the configured
虽然代码执行没啥关系,但是报错看着很烦