公众号登录后 -> 公众号设置 -> 功能设置:JS接口安全域名需要填写准备使用微信支付的域名
添加或修改
这里有4点要注意的:
1.需要填写域名或路径,不支持ip地址,端口号,短链域名,一般直接用域名就行了
2.域名需要经过备案
3.将其中下载的txt文件放在服务器域名映射的根目录,假如域名是:abc.de.com,那就要确保:abc.de.com/MP_verify_nT4jEtfHhaE7IceO.txt能访问到该txt文件。
公众号的设置就完成了
首先进入:账户中心 -> 操作证书,在电脑安装了证书之后进入:API安全
接着继续进入:产品中心 -> 开发配置 -> 支付配置 -> 公众号支付 -> 支付授权目录
这里需要注意的是:
授权目录一定要填写到调起支付页面的上一级目录,加入支付页是:abc.de.com/demo/pay.jsp,那么需要填写:abc.de.com/demo/
商户平台的配置也完成了,接下来就是编码了
先定义一部分常量:
WxConst.java
package com.demo.common;
/**
* @author xyd
* @version V1.0
* @Package com.demo.common
* @Description:
* @date 2018/8/8 10:45
*/
public class WxConst {
/**
* acesstoken
*/
public static String ACCESSTOKEN = "";
/**
* 公众号appId
*/
public static String APPID = "";
/**
* 公众号secret
*/
public static String SECRET = "";
/**
* 公众号jsapi ticket
*/
public static String JSAPITICKET = "";
/**
* 签名类型
*/
public enum SignType {
MD5, HMACSHA256
}
/**
* 商户支付密码
*/
public static String PAYAPISECRET = "";
/**
* 商户号
*/
public static String MCHID = "";
/**
* 商户密钥
*/
public static String MCHKEY = "";
/**
* 签名字段名称
*/
public static final String FIELD_SIGN = "sign";
/**
* 服务商退款证书路径
*/
public static String CERTIFICATE_PATH = "/usr/local/cert/apiclient_cert.p12";
/**
* 下单地址
*/
public static final String WXPAY_UNIFIEDORDER_GATEWAY = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 退款地址
*/
public static final String WXPAY_REFUND_GATEWAY = "https://api.mch.weixin.qq.com/secapi/pay/refund";
/**
* 下单回调地址
*/
public static final String ORDER_NOTIFYURL = "";
/**
* 回复微信的消息
*/
public static final String NOTIFY_RESPONSE_BODY = "\n" +
" \n" +
" \n" +
"";
/**
* 失败
*/
public static final String FAIL = "FAIL";
/**
* 成功
*/
public static final String SUCCESS = "SUCCESS";
}
参考微信支付开发文档可知,需要先获取accesstoken和jsapiticket
JS-SDK文档:JS-SDK
微信统一下单文档:统一下单
首先注入依赖:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.demogroupId>
<artifactId>wechatpaydemoartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>warpackaging>
<name>wechatpaydemo Maven Webappname>
<url>http://www.example.comurl>
<properties>
<spring.version>4.3.3.RELEASEspring.version>
<mybatis.version>3.3.1mybatis.version>
<slf4j.version>1.7.7slf4j.version>
<log4j.version>1.2.17log4j.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>${log4j.version}version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>${slf4j.version}version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>${slf4j.version}version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-annotationsartifactId>
<version>2.5.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-coreartifactId>
<version>2.5.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.5.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.12version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.5.5version>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-validatorartifactId>
<version>5.1.3.Finalversion>
dependency>
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>1.1.0.Finalversion>
dependency>
<dependency>
<groupId>com.thoughtworks.xstreamgroupId>
<artifactId>xstreamartifactId>
<version>1.4.10version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.5version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.11version>
<scope>testscope>
dependency>
dependencies>
<build>
<finalName>wechatpaydemofinalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-pluginartifactId>
<version>3.0.0version>
plugin>
<plugin>
<artifactId>maven-resources-pluginartifactId>
<version>3.0.2version>
plugin>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>3.7.0version>
plugin>
<plugin>
<artifactId>maven-surefire-pluginartifactId>
<version>2.20.1version>
plugin>
<plugin>
<artifactId>maven-war-pluginartifactId>
<version>3.2.0version>
plugin>
<plugin>
<artifactId>maven-install-pluginartifactId>
<version>2.5.2version>
plugin>
<plugin>
<artifactId>maven-deploy-pluginartifactId>
<version>2.8.2version>
plugin>
plugins>
pluginManagement>
build>
project>
使用spring的spring schedule定时任务来获取accesstoken和jsapiticket,将获取的代码定在service层
spring.xml配置如下:
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd ">
<context:component-scan base-package="com.demo.service"/>
<task:scheduled-tasks>
<task:scheduled ref="timerService" initial-delay="2000" method="getAccessTokenAndTicket" fixed-delay="7200000" />
task:scheduled-tasks>
beans>
TimerService.java
package com.demo.service;
import com.demo.common.WxConst;
import com.demo.util.WXUtil;
import org.springframework.stereotype.Service;
/**
* @author xyd
* @version V1.0
* @Package com.demo.service
* @Description:
* @date 2018/8/8 10:53
*/
@Service
public class TimerService {
/**
* 自动获取accessToken和jsapiticket
* @throws Exception
*/
public void getAccessTokenAndTicket() throws Exception {
System.out.println("开始获取token和ticket");
WxConst.ACCESSTOKEN = WXUtil.getAccess_token(WxConst.APPID,WxConst.SECRET);
WxConst.JSAPITICKET = WXUtil.getJsapiTicket(WxConst.ACCESSTOKEN);
}
}
WXUtil.java
package com.demo.util;
import com.alibaba.fastjson.JSON;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.util.Map;
/**
* @author xyd
* @version V1.0
* @Package com.demo.WXUtil
* @Description:
* @date 2018/8/8 10:53
*/
public class WXUtil {
/**
* 两个小时的过期 JsapiTicket
* @param AccessToken
* @return
* @throws Exception
*/
public static String getJsapiTicket(String AccessToken)throws Exception{
HttpClient httpclient = HttpClients.createDefault();
String smsUrl="https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + AccessToken + "&type=jsapi";
HttpGet httpGet = new HttpGet(smsUrl);
String strResult = "";
HttpResponse response = httpclient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
/*读返回数据*/
strResult = EntityUtils.toString(response
.getEntity());
}
Map resultMap = (Map) JSON.parse(strResult);
return (String)resultMap.get("ticket");
}
/**
* 两个小时的过期 ACCESSTOKEN
* @param appid
* @param secret
* @return
* @throws Exception
*/
public static String getAccess_token(String appid, String secret) throws Exception{
HttpClient httpclient = HttpClients.createDefault();
String smsUrl="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&SECRET=" + secret;
HttpGet httpGet = new HttpGet(smsUrl);
String strResult = "";
HttpResponse response = httpclient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
/*读返回数据*/
strResult = EntityUtils.toString(response
.getEntity());
}
Map resultMap = (Map) JSON.parse(strResult);
String AccessToken = (String)resultMap.get("access_token");
System.out.println(AccessToken);
return AccessToken;
}
}
spring-mvc.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd">
<mvc:annotation-driven />
<context:component-scan base-package="com.demo.controller" />
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"
>
<property name="order" value="10">property>
bean>
<bean id="exceptionJson"
class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
bean>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
bean>
<bean
class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
bean>
beans>
目前目录结构如下:
先测试一下是否获取到accesstoken和jsapiticket
DemoController.java
package com.demo.controller;
import com.demo.common.WxConst;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xyd
* @version V1.0
* @Package com.demo.controller
* @Description:
* @date 2018/8/6 17:19
*/
@RestController("demo")
@RequestMapping("/wx")
public class DemoController {
@GetMapping("/get")
public String get(){
System.out.println(WxConst.ACCESSTOKEN);
System.out.println(WxConst.JSAPITICKET);
return "token: " + WxConst.ACCESSTOKEN + "\n ticket: " + WxConst.JSAPITICKET;
}
}
结果:
页面输出:
控制台输出:
问题:
有些人可能获取不到ticket和token,可能需要在公众号设置ip白名单:
查看开发文档第一步是经过统一下单获取prepay_id。
文档:统一下单
大致流程:
第1和第2步在demo中没有逻辑,所以跳过。
第3步:将发起统一下单需要的信息使用java Bean来填充
定义统一下单的bean:
WxPayUnifiedorderRequest.java
package com.demo.bean;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
* @author xyd
* @version V1.0
* @Package com.demo.bean
* @Description:
* @date 2018/8/8 12:45
*/
// xstream标记,后面使用xstream包转成xml格式数据
@XStreamAlias("xml")
public class WxPayUnifiedorderRequest {
//公众账号ID 必填
private String appid;
//商户号 必填
// 转化成xml需要改变的名称
@XStreamAlias("mch_id")
private String mchId;
//设备号
@XStreamAlias("device_info")
private String deviceInfo;
//随机字符串 必填
@XStreamAlias("nonce_str")
private String nonceStr;
//签名 必填
private String sign;
//签名类型
private String sign_type;
//商品描述 必填
private String body;
//商品详情
private String detail;
//附加数据
private String attach;
//商户订单号 必填
@XStreamAlias("out_trade_no")
private String outTradeNo;
//标价币种
@XStreamAlias("fee_type")
private String feeType;
//标价金额 必填
@XStreamAlias("total_fee")
private Integer totalFee;
//终端IP 必填
@XStreamAlias("spbill_create_ip")
private String spbillCreateIp;
//交易起始时间
@XStreamAlias("time_start")
private String timeStart;
//交易结束时间
@XStreamAlias("time_expire")
private String timeExpire;
//订单优惠标记
@XStreamAlias("goods_tag")
private String goodsTag;
//通知地址 必填
@XStreamAlias("notify_url")
private String notifyUrl;
//交易类型 必填
@XStreamAlias("trade_type")
private String tradeType;
//商品ID
@XStreamAlias("product_id")
private String productId;
//指定支付方式
@XStreamAlias("limit_pay")
private String limitPay;
//用户标识
private String openid;
//场景信息
@XStreamAlias("scene_info")
private String sceneInfo;
public String getAppid() {
return appid;
}
/**
* 公众账号ID
* 必填: 是
* @param appid
*/
public void setAppid(String appid) {
this.appid = appid;
}
public String getMchId() {
return mchId;
}
/**
* 商户号
* 必填: 是
* @param mchId
*/
public void setMchId(String mchId) {
this.mchId = mchId;
}
public String getDeviceInfo() {
return deviceInfo;
}
/**
* 设备号
* 必填: 否
* @param deviceInfo
*/
public void setDeviceInfo(String deviceInfo) {
this.deviceInfo = deviceInfo;
}
public String getNonceStr() {
return nonceStr;
}
/**
* 随机字符串
* 必填: 是
* @param nonceStr
*/
public void setNonceStr(String nonceStr) {
this.nonceStr = nonceStr;
}
public String getSign() {
return sign;
}
/**
* 签名
* 必填: 是
* @param sign
*/
public void setSign(String sign) {
this.sign = sign;
}
public String getAttach() {
return attach;
}
/**
* 附加数据
* 必填: 否
* @param attach
*/
public void setAttach(String attach) {
this.attach = attach;
}
public String getBody() {
return body;
}
/**
* 商品描述
* 必填: 是
* @param body
*/
public void setBody(String body) {
this.body = body;
}
public String getDetail() {
return detail;
}
/**
* 商品详情
* 必填: 否
* @param detail
*/
public void setDetail(String detail) {
this.detail = detail;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getOutTradeNo() {
return outTradeNo;
}
/**
* 商户订单号
* 必填: 是
* @param outTradeNo
*/
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo;
}
public String getSpbillCreateIp() {
return spbillCreateIp;
}
public void setSpbillCreateIp(String spbillCreateIp) {
this.spbillCreateIp = spbillCreateIp;
}
public Integer getTotalFee() {
return totalFee;
}
public void setTotalFee(Integer totalFee) {
this.totalFee = totalFee;
}
public String getTradeType() {
return tradeType;
}
public void setTradeType(String tradeType) {
this.tradeType = tradeType;
}
public String getSign_type() {
return sign_type;
}
public void setSign_type(String sign_type) {
this.sign_type = sign_type;
}
public String getFeeType() {
return feeType;
}
public void setFeeType(String feeType) {
this.feeType = feeType;
}
public String getTimeStart() {
return timeStart;
}
public void setTimeStart(String timeStart) {
this.timeStart = timeStart;
}
public String getTimeExpire() {
return timeExpire;
}
public void setTimeExpire(String timeExpire) {
this.timeExpire = timeExpire;
}
public String getGoodsTag() {
return goodsTag;
}
public void setGoodsTag(String goodsTag) {
this.goodsTag = goodsTag;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getLimitPay() {
return limitPay;
}
public void setLimitPay(String limitPay) {
this.limitPay = limitPay;
}
public String getSceneInfo() {
return sceneInfo;
}
/**
* 场景信息
* 必填:是
* 格式:{"h5_info": {"type":"Wap","wap_url": "https://pay.qq.com","wap_name": "腾讯充值"}}
* @param sceneInfo
*/
public void setSceneInfo(String sceneInfo) {
this.sceneInfo = sceneInfo;
}
}
在DemoController.java中填充信息:
@GetMapping("/order")
public Map order() throws Exception {
// 获取5分钟后的时间
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.MINUTE, 5);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timeExpire = sdf.format(calendar.getTime());
// 统一下单Bean
WxPayUnifiedorderRequest wxRequest = new WxPayUnifiedorderRequest();
wxRequest.setAppid(WxConst.APPID);
wxRequest.setMchId(WxConst.MCHID);
// 随机串
wxRequest.setNonceStr(RandomUtil.getRandomStr());
wxRequest.setBody("TEST");
// 订单号
wxRequest.setOutTradeNo(WXUtil.getOutTradeNo());
// 订单价格
wxRequest.setTotalFee(1);
// ip,当发起的是微信公众号支付时,微信对ip不进行校验
wxRequest.setSpbillCreateIp("8.8.8.8");
// 异步通知
wxRequest.setNotifyUrl(WxConst.ORDER_NOTIFYURL);
wxRequest.setTradeType("JSAPI");
// 微信OpenId
wxRequest.setOpenid("oEjT4vy21gy6m96WlGkV6AvhgG-E");
// 订单过期时间
wxRequest.setTimeExpire(timeExpire);
// 签名
wxRequest.setSign(sign(xmlToMap(XmlUtil.toXMl(wxRequest)), WxConst.MCHKEY));
// WxConst.JS_URL表示支付页面的地址
Map jsMap = WxPayUtil.pay(wxRequest, WxConst.JS_URL);
return jsMap;
}
随机串的生成算法:
RandomUtil.java
package com.demo.util;
public class RandomUtil {
// 参与算法的字符
private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final java.util.Random RANDOM = new java.util.Random();
public static String getRandomStr() {
StringBuilder sb = new StringBuilder();
// 从参与算法的字符中随机选出一个字符,循环16次,16为随机数
for (int i = 0; i < 16; i++) {
sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length())));
}
return sb.toString();
}
}
WXUtil.getOutTradeNo() 来动态获取订单号:
/**
*生成18位随机订单号
* 时间+5位随机数
*
*/
public static String getOutTradeNo(){
Date date=new Date();
// 日期精确到秒加4位随机数
DateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
String time=format.format(date);
String radom = String.valueOf(new Random(7).nextInt(10000));
String outTradeNo = time + radom ;
return outTradeNo;
}
XmlUtil.toXMl(WxPayUnifiedorderRequest wxRequest);
工具类将bean转成xml
private static XStream createxStream(){
return new XStream(new XppDriver(new NoNameCoder()) {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;
@Override
@SuppressWarnings("rawtypes")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
////当对象属性带下划线时,XStream会转换成双下划线,
// 重写这个方法,不再像XppDriver那样调用nameCoder来进行编译,而是直接返回节点名称,避免双下划线出现
@Override
public String encodeNode(String name) {
return name;
}
@Override
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write(");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});
}
/**
* 对象转xml
* @param obj
* @return
*/
public static String toXMl(Object obj) {
//使用注解设置别名必须在使用之前加上注解类才有作用
XStream xStream = createxStream();
xStream.processAnnotations(obj.getClass());
return xStream.toXML(obj);
}
XmlUtil.xmlToMap(String xmlString);
将xml类型数据转化成map,这里先将bean转xml再转map是想去掉多余的参数字段,因为后面需要加密
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map xmlToMap(String strXML) throws Exception {
try {
Map data = new HashMap();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
WxPaySignatureUtil.sign(Map
params, String signKey);
微信签名介绍:
假设传送的参数如下:
appid: wxd930ea5d5a258f4f
mch_id: 10000100
device_info: 1000
body: test
nonce_str: ibuaiVcKdpRxkhJA
第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:
stringA=”appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA”;
第二步:拼接API密钥:
stringSignTemp=stringA+”&key=192006250b4c09247ec02edce69f6a2d” //注:key为商户平台设置的密钥key
sign=MD5(stringSignTemp).toUpperCase()=”9A0A8659F005D6984697E2CA0A9CF3B7” //注:MD5签名方式
sign=hash_hmac(“sha256”,stringSignTemp,key).toUpperCase()=”6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6” //注:HMAC-SHA256签名方式
最终得到最终发送的数据:
<xml>
<appid>wxd930ea5d5a258f4fappid>
<mch_id>10000100mch_id>
<device_info>1000device_info>
<body>testbody>
<nonce_str>ibuaiVcKdpRxkhJAnonce_str>
<sign>9A0A8659F005D6984697E2CA0A9CF3B7sign>
xml>
签名代码:
/**
* 签名
* MD5
* @param params
* @param signKey
* @return
*/
public static String sign(Map params, String signKey) {
// 将Map中的字段都进行按字母顺序排序
SortedMap sortedMap = new TreeMap<>(params);
StringBuilder toSign = new StringBuilder();
for (String key : sortedMap.keySet()) {
String value = params.get(key);
// 当值不为空并且sign字段和key字段不参与字段连接
if (StringUtils.isNotEmpty(value) && !"sign".equals(key) && !"key".equals(key)) {
toSign.append(key).append("=").append(value).append("&");
}
}
// 最后加上key字段,内容是商户密钥
toSign.append("key=").append(signKey);
// 使用apache的DigestUtils来进行md5加密,返回加密后的字符串
return DigestUtils.md5Hex(toSign.toString()).toUpperCase();
}
将sign字段加入bean中后,使用
WxPayUtil.pay(WxPayUnifiedorderRequest wxRequest, String url);
来向微信服务器发起请求:
/**
* 微信支付
* @param jsUrl
* @return
* @throws Exception
*/
public static Map pay(WxPayUnifiedorderRequest wxRequest, String jsUrl) throws Exception{
String xml = XmlUtil.toXMl(wxRequest);
String result = HttpClientUtil.post(WxConst.WXPAY_UNIFIEDORDER_GATEWAY,xml, "XML");
Map map = handleReturnMessage( result, jsUrl);
return map;
}
有个难点就是需要处理微信返回的数据,如果数据合格就需要进行签名并提取JS-SDK中wx.config()中的字段
JS-SDK的签名算法:
注意事项
1.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
2.签名用的url必须是调用JS接口页面的完整URL。
3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
4.最坑的一点,注意页面的noncestr和参与签名的nonceStr大小写区别。和后面部分调起支付的timestamp与参与签名的timeStamp大小写。
public static Map
handleReturnMessage(String returnMessage,String jsUrl)
/**
* 最后返回给前端的数据
* @param returnMessage
* @param jsUrl
* @return
* @throws Exception
*/
public static Map<String, String> handleReturnMessage(String returnMessage,String jsUrl) throws Exception{
// 将微信返回的数据转成map
Map returnMap = xmlToMap(returnMessage);
Map<String,String> resultMap = new HashMap<String, String>();
// 获取微信的两种代码
String return_code = (String)returnMap.get("return_code");
String result_code = (String)returnMap.get("result_code");
System.out.println(returnMap);
// 当return_code返回失败时,将错误信息返回前端
if(return_code.equals(WxConst.FAIL)){
resultMap.put("error", (String)returnMap.get("return_msg"));
return resultMap;
}
// result_code返回失败时,返回错误信息前端
if(result_code.equals(WxConst.FAIL)){
resultMap.put("error", (String)returnMap.get("err_code_des"));
return resultMap;
}
String tradeType = (String)returnMap.get("trade_type");
// 返回的公众号支付,进行JS-SDK的wx.config()中的字段获取
if(tradeType.equals("JSAPI")){
String perpayId = (String)returnMap.get("prepay_id");
String appId = (String)returnMap.get("appid");
String signType = "MD5";
String jsapi_ticket = WxConst.JSAPITICKET;
String nonceStr = UUID.randomUUID().toString();
String timestamp = String.valueOf(WXUtil.getSecondTimestamp());
if(jsUrl == null) {
return null;
};
//微信签名
Map<String, String> JSAPIMap = new HashMap<String,String>();
JSAPIMap.put("jsapi_ticket",jsapi_ticket);
JSAPIMap.put("noncestr",nonceStr);
JSAPIMap.put("timestamp",timestamp);
JSAPIMap.put("url",jsUrl);
String signature = generateJSAPISignature( JSAPIMap);
// 支付签名
Map<String, String> JSAPIPayMap = new HashMap<String,String>();
JSAPIPayMap.put("appId",appId);
JSAPIPayMap.put("timeStamp",timestamp);
JSAPIPayMap.put("nonceStr",nonceStr);
JSAPIPayMap.put("package","prepay_id="+perpayId); //prepay_id=wx201710301549453bd02ae87d0526670466
JSAPIPayMap.put("signType","MD5");
String paySign = sign(JSAPIPayMap, WxConst.MCHKEY);
//返回结果
resultMap.put("appId",appId);
resultMap.put("timestamp",timestamp);
resultMap.put("nonceStr",nonceStr);
resultMap.put("signature",signature);
resultMap.put("package","prepay_id="+perpayId);
resultMap.put("signType","MD5");
resultMap.put("paySign",paySign);
return resultMap;
}
if (tradeType.equals("MWEB")){//返回的是H5支付
resultMap.put("mweb_url",(String)returnMap.get("mweb_url"));
return resultMap;
}
return null;
}
这样后台完成了发起支付的操作了,前端需要将这些数据填充到JS-SDK中发起支付,我们写一个静态jsp页面进行测试:
pay.jsp
<html>
<body>
<meta charset="UTF-8">
<title>Titletitle>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js">script>
<h2>Hello World!h2>
<script>
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: 'wx*******ed032', // 必填,公众号的唯一标识
timestamp: "1526020385", // 必填,生成签名的时间戳
nonceStr: '03ac62ff-7ead-403c-984e-ced5fd8e12cc', // 必填,生成签名的随机串
signature: 'c2724aefc4e1b20048a5934aed4834d761d77abb',// 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
});
wx.ready(function(){
alert("支付");
wx.chooseWXPay({
timestamp: '1526020385', // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '03ac62ff-7ead-403c-984e-ced5fd8e12cc', // 支付签名随机串,不长于 32 位
package: 'prepay_id=wx111***081363443414418cb31285518214', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: 'MD5', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: 'FED7D5D14F5D04628FB82D41B5979BEC', // 支付签名
success: function (res) {
// 支付成功后的回调函数
alert("支付成功");
},
cencel:function(res){
alert('cencel pay');
},
fail: function(res){
alert('pay fail');
alert(JSON.stringify(res));
}
});
wx.error(function(res){
alert(res.err_msg);
});
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});
script>
<input type="button" value="支付1111" onclick="">
body>
html>
测试过程:
异步通知很简单,先看官方文档说明:
地址:异步通知
总结下来就一下几点:
<xml>
<return_code>return_code>
<return_msg>return_msg>
xml>
首先,写一个springmvc方法进行接收微信的请求:
@PostMapping("/notify")
public void notify(HttpServletRequest request, HttpServletResponse response){
//异步通知
Map returnMap = WxPayUtil.asynNotify(request,response);
System.out.println(returnMap);
}
方法
public static Map
asynNotify(HttpServletRequest request, HttpServletResponse response)
如下:
/**
* 异步通知 成功返回Map,失败返回null
* @param request
* @param response
* @return
*/
public static Map asynNotify(HttpServletRequest request, HttpServletResponse response){
ServletInputStream sis = null;
String xmlData = "";
Map returnMap = null;
try {
//编码格式改成UTF-8
response.setCharacterEncoding("UTF-8");
response.setContentType("text/xml;charset=utf-8");
response.setHeader("Cache-control", "no-cache");
sis = request.getInputStream();
// 取HTTP请求流长度
int size = request.getContentLength();
// 用于缓存每次读取的数据
byte[] buffer = new byte[size];
// 用于存放结果的数组
byte[] xmldataByte = new byte[size];
int count = 0;
int rbyte = 0;
// 循环读取
while (count < size) {
// 每次实际读取长度存于rbyte中
rbyte = sis.read(buffer);
for (int i = 0; i < rbyte; i++) {
xmldataByte[count + i] = buffer[i];
}
count += rbyte;
}
xmlData = new String(xmldataByte, "UTF-8");
PrintWriter out = response.getWriter();
out.println(WxConst.NOTIFY_RESPONSE_BODY);
returnMap = xmlToMap(xmlData);
String return_code = (String) returnMap.get("return_code");
if (return_code.equals(WxConst.FAIL)) {//不成功返回结果为空
return null;
}
//验签
if (return_code.equals(WxConst.SUCCESS)){
if(!WxPaySignatureUtil.verify(returnMap,WxConst.MCHKEY)){//验签失败
return null ;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return returnMap;
}
/**
* 校验签名
* @param params
* @param signKey
* @return
*/
public static Boolean verify(Map params, String signKey) {
String sign = sign(params, signKey);
return sign.equals(params.get("sign"));
}
测试结果:
查看日志看到回复的数据:
{
"transaction_id": "4200000144201808085758930691",
"nonce_str": "wLnrwFqTgBvp5FDe",
"bank_type": "CFT",
"openid": "oEjT4vy21gy6m96WlGkV6AvhgG - E",
"sign": "77361 FE27A070271F7EA78E5767A87DA",
"fee_type": "CNY",
"mch_id": "1489877082",
"cash_fee": 1,
"out_trade_no": "201808081646224236",
"appid": "wx4a********0ed032",
"total_fee": 1,
"trade_type": "JSAPI",
"result_code": "SUCCESS",
"time_end": "20180808164817",
"is_subscribe": "Y",
"return_code": "SUCCESS"
}
微信支付和异步通知已经ok了
码云:地址