java微信公众号支付详细流程
最近在公司做的两个项目中都用到了微信支付,期间也踩了很多的坑,现在抽空整理了一下,一方面是加深自己的印象,另一方面是希望能够帮助到有疑惑的小伙伴们
一、 获取code
每次用户授权带上的code都不一样,code只能使用一次,5分钟未被使用自动过期
String codeUrl =
"https://open.weixin.qq.com/connect/oauth2/authorize?" +
"appid=" + WXPayConstants.APPID +
"&redirect_uri=" + URLEncoder.encode(WXPayConstants.REDIRECTURI+ WXPayConstants.REQUESTURI +
"?renterNo="+request.getRenterNo()+"&thirdType="+request.getThirdType()) +
"&response_type=code" +
"&scope=" + WXPayConstants.SCOPE +
"&state=STATE"+
"#wechat_redirect";
将该链接字符串返回给页面,通过 window.location = codeUrl 即可请求
重点内容
-
redirect_uri (授权回调地址)
当请求 codeUrl,成功获得 code 之后,微信端会回调该地址(该地址必须是外网可以访问的)
设置 redirect_uri
在微信公众平台设置网页授权域名- 此处需要将 MP_verify_xyPkykStkhZUf7ya.txt 文件上传到项目的根目录下,
也就是说通过域名 + /MP_verify_xyPkykStkhZUf7ya.txt 的形式能直接访问到该文件 - 代码中 WXPayConstants.REDIRECTURI 为我设置的域名,例如 www.baidu.com,
需要与微信公众平台设置的授权域名保持一致 - 代码中 WXPayConstants.REQUESTURI 为我设置的接口路径,例如 /pay/get_wx_openid
此外需要对 redirect_uri 进行 URLEncoder.encode() 编码 - 相当于 @requestMapping(“/pay/get_wx_openid”),
也就是说微信端会重定向到该路径 www.baidu.com/pay/get_wx_openid,
在该方法中通过 request.getParameter(“code”),即可获得code
- 此处需要将 MP_verify_xyPkykStkhZUf7ya.txt 文件上传到项目的根目录下,
-
scope(应用授权作用域)
- snsapi_base
不弹出授权页面,直接跳转,只能获取用户openid -
snsapi_userinfo
弹出授权页面,可通过openid拿到昵称、性别、所在地
并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
此处我们设置为 snsapi_userinfo
- snsapi_base
- state(参数,重定向后会带上,将 STATE 替换成所要传递的参数即可)
有一个不足之处就是此处只能携带一个参数,而有时候我们可能会需要传递多个参数,
那么我们在 redirect_uri 后面以?的形式挂上参数即可,
如上 ?renterNo=”+request.getRenterNo()+”&thirdType=”+request.getThirdType()
二、 获取access_token和openid
String getAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
"appid=" + WXPayConstants.APPID +
"&secret=" + WXPayConstants.APPSECRET +
"&code=" + request.getParameter("code") +
"&grant_type=authorization_code";
JSONObject accessTokenObject = HttpUtils.doRequest(getAccessTokenUrl, "GET", null);
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.URL;
public class HttpUtils {
private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);
/** * 发送https请求 * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param data 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String data) {
JSONObject jsonObject = null;
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.connect();
// 当data不为null时向输出流写数据
if (null != data) {
// getOutputStream方法隐藏了connect()方法
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(data.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
inputStream = conn.getInputStream();
inputStreamReader = new InputStreamReader(inputStream, "utf-8");
bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
conn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
return jsonObject;
} catch (Exception e) {
logger.error("发送https请求失败,失败", e);
return null;
} finally {
// 释放资源
try {
if(null != inputStream) {
inputStream.close();
}
if(null != inputStreamReader) {
inputStreamReader.close();
}
if(null != bufferedReader) {
bufferedReader.close();
}
} catch (IOException e) {
logger.error("释放资源失败,失败", e);
}
}
}
}
将 access_token 和 openid 传回页面,以供 ajax 调用
重点内容
-
access_token (网页授权接口调用凭证)
- 通过 accessTokenObject.get(“access_token”) 获取
- 此处获得的 access_token 与基础支撑的 access_token 不同
- 基础支持的 access_token ,每天只能获取2000次,有效期为7200s(两个小时)
- 而此处获得的 access_token 没有获取次数上限,有效期的话我也不太清楚,因为每授权一次,
都会重新获得一个新的 access_token,所以不需要去关心此处 access_token 的有效期
-
openid (用户唯一标识)
- 通过 accessTokenObject.get(“openid “) 获取
-
其余参数
-
通过 accessTokenObject.get(对应字段) 获取
-
通过 accessTokenObject.get(对应字段) 获取
三、 获取用户基本信息
将 access_token 和 openid 传入
String getUserMessageUrl =
"https://api.weixin.qq.com/sns/userinfo?" +
"access_token=" + accessToken +
"&openid=" + openid +
"&lang=zh_CN";
JSONObject userMessageObject = HttpUtils.doRequest(getUserMessageUrl, "GET", null);
重点内容
-
nickname (昵称)
- 通过 userMessageObject .get(“nickname”) 获取
-
其余参数
-
通过 userMessageObject .get(对应字段) 获取
-
通过 userMessageObject .get(对应字段) 获取
三、 统一下单
统一下单生成预支付订单
/** * 统一下单 * @param request */
@RequestMapping(value = "/unifiedOrder")
public Map<String, String> unifiedOrder(HttpServletRequest request) {
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
Map<String, String> map = new HashMap<>();
map.put("appid", WXPayConstants.APPID);
// 此处传入openid
map.put("openid", request.getParameter("openid"));
// 微信支付商户号
map.put("mch_id", WXPayConstants.MCHID);
// 随机字符串,用UUID生成
String nonceStr = WXPayConstants.NONCESTR;
map.put("nonce_str", nonceStr);
// 常量MD5
map.put("sign_type", WXPayConstants.MD5);
// 商品说明
map.put("body", "xxxxxxxxxxxxxx");
// 商品详情
map.put("detail", "xxxxxxxxxxxxxx");
// 自定义订单号,不能超过32个字符
map.put("out_trade_no", request.getParameter("outTradeNo"));
// 标价金额:支付金额单位为【分】,参数值不能带小数
map.put("total_fee", request.getParameter("totalFee"));
// 终端IP:APP和网页支付提交用户端ip(存在反向代理,需获取用户真实ip)
map.put("spbill_create_ip", TerminalUtils.getIpAddr(request));
// 通知地址:此路径为接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
map.put("notify_url", WXPayConstants.REDIRECTURI + WXPayConstants.PAYBACKURI);
// 交易类型:JSAPI 公众号支付 NATIVE 扫码支付 APP APP支付
map.put("trade_type", "JSAPI");
try {
// 下单前签名(微信端会对其进行校验)
map.put("sign", WXPayUtils.generateSignature(map, WXPayConstants.APISECRET));
// 将map转换为xml数据
String xml = WXPayUtils.mapToXml(map);
// 发送post请求,返回xml数据
String respXml = HttpUtils.httpsPostXml(url, xml);
// 将xml数据转换为map
Map<String, String> unifiedOrderMap = WXPayUtils.xmlToMap(respXml);
unifiedOrderMap.put("detail", "xxxxxxxxxxxxxx");
unifiedOrderMap.put("outTradeNo", request.getOutTradeNo());
logger.info("统一下单返回信息 --> {}", JSON.toJSONString(unifiedOrderMap));
return unifiedOrderMap;
} catch (Exception e) {
logger.error("统一下单,失败", e);
return null;
}
}
}
该方法与上文提到的 httpsRequest 方法,同处于 HttpUtils 类中
/** * 发送带xml数据的post请求 * @param urlStr 请求地址 * @param xmlInfo xml数据 * @return */
public static String httpsPostXml(String urlStr, String xmlInfo) {
try {
URL url = new URL(urlStr);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "text/xml;charset=utf-8");
// 在输入流里面进行转码
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), "utf-8");
out.write(xmlInfo);
out.flush();
out.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuffer lines = new StringBuffer();
String line = "";
for(line = br.readLine(); line != null; line = br.readLine()){
lines.append(line);
}
return lines.toString();
} catch(Exception e){
logger.error("发送post请求,失败", e);
return null;
}
}
重点内容
- out_trade_no(自定义订单号),我是在页面生成好订单号,传递给后端
//生成订单号
function formatDate(now) {
var year = now.getFullYear();
var month = ("0"+(now.getMonth()+1)).slice(-2);
var date = ("0"+now.getDate()).slice(-2);
var hour = ("0"+now.getHours()).slice(-2);
var minute = ("0"+now.getMinutes()).slice(-2);
var second = ("0"+now.getSeconds()).slice(-2);
var milliSecond = ("0"+now.getMilliseconds()).slice(-3);
return "pay_" + year + month + date + hour + minute + second + milliSecond;
}
- total_fee(支付金额),单位为分,
例如,商品价格为0.01,此处你需要传入的值为1 - spbill_create_ip(用户真实IP)
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
public class TerminalUtils {
private final static Logger logger = LoggerFactory.getLogger(TerminalUtils.class);
/** * 获取访问者IP * 在一般情况下使用Request.getRemoteAddr()即可,但是经过nginx等反向代理软件后,这个方法会失效 * 本方法先从Header中获取X-Real-IP,如果不存在再从X-Forwarded-For获得第一个IP(用,分割), * 如果还不存在则调用Request .getRemoteAddr() * @param request * @return */
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("X-Forwarded-For");
if (!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
} else {
return request.getRemoteAddr();
}
}
-
notify_url(支付回调地址)
当支付完成后,微信端会回调该地址(该地址必须是外网可以访问的,且不能携带参数)
- 代码中 WXPayConstants.REDIRECTURI 为我设置的域名,例如 www.baidu.com
- 代码中 WXPayConstants.PAYBACKURI 为我设置的接口路径,例如 /pay/pay_back
- 相当于 @requestMapping(“/pay/pay_back”)
也就是说支付完成后微信端会重定向到该路径 www.baidu.com/pay/pay_back -
此外,我们还需要在微信商户平台设置支付授权目录,设置路径:商户平台–>产品中心–>开发配置
怎么设置呢,首先要看你支付的当前页面URL
例如 http://www.xxx.com/pay/pay.jsp,则你应该填写 http://www.xxx.com/pay/
- trade_type(交易类型)
JSAPI:公众号支付 NATIVE:扫码支付 APP:APP支付 - sign(签名)
根据方法 WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 生成签名,
WXPayUtils 我会在最后给出
WXPayConstants.APISECRET 指的是API密钥,你需要在微信商户平台进行设置
三、 用户支付
用户支付方法中,我调用了统一下单的方法,根据其返回值,进行相应的业务逻辑处理
/** * 用户支付 * @param request * @return */
@RequestMapping(value = "/userPay")
@ResponseBody
public Map<String,String> userPay(HttpServletRequest request, HttpServletResponse response) {
// 调用统一下单方法
Map<String, String> unifiedOrderMap = unifiedOrder(req);
String returnCode = unifiedOrderMap.get("return_code");
String resultCode = unifiedOrderMap.get("result_code");
try {
if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
logger.info("微信支付下单成功");
// 进行签名校验
Map<String, String> map = new HashMap<>();
map.put("return_code", unifiedOrderMap.get("return_code"));
map.put("return_msg", unifiedOrderMap.get("return_msg"));
map.put("appid", unifiedOrderMap.get("appid"));
map.put("mch_id", unifiedOrderMap.get("mch_id"));
map.put("nonce_str", unifiedOrderMap.get("nonce_str"));
map.put("result_code", unifiedOrderMap.get("result_code"));
map.put("prepay_id", unifiedOrderMap.get("prepay_id"));
map.put("trade_type", unifiedOrderMap.get("trade_type"));
// 生成签名
String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET);
// 微信返回的签名
String wxSign = unifiedOrderMap.get("sign");
// 需要返回给页面的数据
Map<String,String> returnMap = new HashMap<>();
if (mySign.equals(wxSign)) {
returnMap.put("appId", unifiedOrderMap.get("appid"));
returnMap.put("timeStamp", WXPayUtils.getCurrentTimestamp() + "");
returnMap.put("nonceStr", WXPayConstants.NONCESTR);
returnMap.put("package", "prepay_id=" + unifiedOrderMap.get("prepay_id"));
returnMap.put("signType", WXPayConstants.MD5);
// 此处生成的签名返回给页面作为参数
returnMap.put("paySign", WXPayUtils.generateSignature(returnMap, WXPayConstants.APISECRET));
logger.info("签名校验成功,下单返回信息为 --> {}", JSON.toJSONString(returnMap));
Map<String, Object> storeMap = new HashMap<>();
// 签名校验成功,你可以在此处进行自己业务逻辑的处理
// storeMap可以存储那些你需要存进数据库的信息,可以生成预支付订单
}else {
logger.error("签名校验失败,下单返回信息为 --> {}", JSON.toJSONString(returnMap));
// 签名校验失败,你可以在此处进行校验失败的业务逻辑
}
return returnMap;
}
}catch (Exception e){
logger.error("用户支付,失败", e);
return null;
}
}
重点内容
-
return_code (返回状态码)
- 通过 unifiedOrderMap.get(“return_code”) 获取
-
result_code(业务结果)
- 通过 unifiedOrderMap.get(“result_code”) 获取
-
其余参数
- 通过 unifiedOrderMap.get(对应字段) 获取
统一下单返回的字段有点多,小伙伴们可以上统一下单API中进行查看
- 通过 unifiedOrderMap.get(对应字段) 获取
-
统一下单返回的签名
最最重要的来了,当初在这里踩了不少的坑
return_code,return_msg,appid,mch_id,nonce_str,result_code,prepay_id,trade_type 的顺序来,根据 String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 得到我们自己的签名至于为什么参数顺序一定要这样,是因为微信在统一下单返回的xml数据中,参数顺序就是这样,同时微信端也会返回一个签名,我们通过String wxSign = unifiedOrderMap.get(“sign”) 得到该签名,然后对二者进行比较,若两者相等,则表明验证成功,就可以进行业务逻辑的处理了
-
返回给前端页面的签名
- 大家千万不要把这两个签名给搞混了,返回给页面的签名跟上面那个签名一点关系也没有
- 该签名的参数顺序必须按照 appId,timeStamp,nonceStr,package,signType 的顺序来,详情见微信内H5调起支付,通过 WXPayUtils.generateSignature(returnMap, WXPayConstants.APISECRET) 来生成签名
四、 页面调起支付
走完上面几步,我们终于可以准备在页面上唤起微信支付了
$.ajax({
// 此处填写你后端支付方法的访问路径
url: "www.baidu.com/pay/userPay.do",
type: "POST",
// 填写你业务逻辑需要用到的数据
data: "",
dataType: "json",
success: function (msg) {
// 此处msg为上文userPay方法返回的returnMap
if(msg !== null) {
// WeixinJSBridge:微信浏览器内置对象,在其他浏览器中无效
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": msg.appId,
"timeStamp": msg.timeStamp,
"nonceStr": msg.nonceStr,
"package": msg.package,
"signType": msg.signType,
"paySign": msg.sign
},
function (res) {
// 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,你可以在此处跳转支付成功页面
}else if (res.err_msg === "get_brand_wcpay_request:cancel") {
// 支付取消
} else if (res.err_msg === "get_brand_wcpay_request:fail") {
// 支付失败
}
}
);
}
}
});
五、 支付结果通知
支付完成之后,微信端会回调该地址,该地址为上文所设置好的 notify_url
/** * 支付回调验证 * @param request */
@RequestMapping(value = "/pay_back")
public void payBack(HttpServletRequest request, HttpServletResponse response) {
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
PrintWriter pw = null;
try {
is = request.getInputStream();
isr = new InputStreamReader(is, "utf-8");
br = new BufferedReader(isr);
String str = null;
StringBuffer xml = new StringBuffer();
while ((str = br.readLine()) != null) {
//返回的是xml数据
xml.append(str);
}
//将xml数据转换为map
Map<String, String> payBackMap = WXPayUtils.xmlToMap(xml.toString());
String returnCode = payBackMap.get("return_code"); // 业务码
String resultCode = payBackMap.get("result_code"); // 状态码
Map<String, String> map = new HashMap<>();
if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
logger.info("微信支付返回成功");
//进行签名校验
map.put("appid", payBackMap.get("appid")); // appid
map.put("bank_type", payBackMap.get("bank_type")); // 付款银行
map.put("cash_fee", payBackMap.get("cash_fee")); // 现金支付金额,单位为【分】
map.put("fee_type", payBackMap.get("fee_type")); // 货币种类
map.put("is_subscribe", payBackMap.get("is_subscribe")); // 是否关注公众账号
map.put("mch_id", payBackMap.get("mch_id")); // 商户号
map.put("nonce_str", payBackMap.get("nonce_str")); // 随机字符串
map.put("openid", payBackMap.get("openid"));//openid
map.put("out_trade_no", payBackMap.get("out_trade_no")); // 商户订单号
map.put("result_code", payBackMap.get("result_code")); // 业务码
map.put("return_code", payBackMap.get("return_code")); // 状态码
map.put("time_end", payBackMap.get("time_end")); // 支付完成时间,yyyyMMddHHmmss
map.put("total_fee", payBackMap.get("total_fee")); // 订单总金额,单位为【分】
map.put("trade_type", payBackMap.get("trade_type")); // 交易类型
map.put("transaction_id", payBackMap.get("transaction_id")); // 微信支付订单号
// 生成签名
String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET);
// 微信返回的签名
String wxSign = payBackMap.get("sign");
Map<String,String> returnMap = new HashMap<>();
if (mySign.equals(wxSign)) {
// 返回数据给微信
returnMap.put("return_code", "SUCCESS");
returnMap.put("return_msg", "OK");
logger.info("签名校验成功,回调信息为 --> {}", JSON.toJSONString(returnMap));
// 在此处你可以更新之前存入数据库的预支付订单的信息,处理相关的业务逻辑
}else {
logger.info("签名校验失败");
returnMap.put("return_code", "SUCCESS");
returnMap.put("return_msg", "签名验证失败");
logger.info("签名校验失败,回调信息为 --> {}", JSON.toJSONString(returnMap));
}
//将map转换为xml数据(直接返回即可)
String returnXml = WXPayUtils.mapToXml(returnMap);
pw = response.getWriter();
pw.write(returnXml);
}
} catch (Exception e) {
logger.error("支付回调验证,失败", e);
} finally {
try {
if (null != is) {
is.close();
}
if (null != isr) {
isr.close();
}
if (null != br) {
br.close();
}
if(null != pw){
pw.close();
}
} catch (Exception e) {
logger.error("释放资源,失败", e);
}
}
}
重点内容
- 我们从输入流中获取到微信端返回的xml数据,将其转换为Map,再进行相关的业务逻辑处理
- 跟统一下单返回类似,根据上面参数排列的顺序,通过 String mySign = WXPayUtils.generateSignature(map, WXPayConstants.APISECRET) 生成我们自己的签名,然后跟微信端返回的签名进行比较,若两者相等,则表明验证成功,就可以进行业务逻辑的处理了
- 不管签名验证成功还是失败,我们需要返回一个应答给微信,如果微信没有收到应答,则微信会通过一定的策略定期重新发起通知,通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位为秒
- 具体如何返回应答消息给微信,我已经在上面的代码中给出了,小伙伴们可以参考下
六、 常量工具类
- 文件代码有点多,我把它压缩了,有需要的小伙伴可以留邮箱,我发给你
- 另外,如果要在本地测试微信支付,你可以使用Ngrok内网穿透工具,来映射本地端口,有免费的不过不太稳定,收费的10块一个,还算稳定,小伙伴们可以自行前去购买,我把映射工具也放到压缩包里了
文章有点长,中间难免会有疏忽的地方,小伙伴们可以在评论中指出来,
当然有什么疑问的话,也欢迎提问,我看到了有时间会一一答复的,希望能帮助到有需要的小伙伴们