接上一篇,领导让我帮忙对接一下微信支付,接到文档之后我一脸懵逼,看了半天之后发现与银行对接大同小异,于是根据微信API要求进行了编码。
先贴上源码:微信支付Demo 本工程是用java8编写
注:必须要在微信小程序控制台申请APPID,KEY,商户号等
所用技术: Maven 3.x,IDEA2017,Mysql5.7.x,SpringBoot 2.x,Lombok1.16.x
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
</dependency>
<dependency>
<groupId>xmlpull</groupId>
<artifactId>xmlpull</artifactId>
<version>1.1.3.1</version>
</dependency>
<dependency>
<groupId>net.sf.kxml</groupId>
<artifactId>kxml2</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
maven依赖加入完成之后,就开始进行项目的封装了
先从统一下单开始:
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author ChoviWu
* @date 2019/1/3
* Description :
*/
public class OrderUtils {
private static final AtomicInteger NUMBER = new AtomicInteger(1);
private static final AtomicBoolean BOOLEAN = new AtomicBoolean(false);
private static Random rnd = new Random();
/**
* 随机串 生成签名
*/
public static final String RANDOM_NONECE = "abcdefghijklmnopqrstuvwxyz0123456789";
/**
* 针对微信支付生成商户订单号,为了避免微信商户订单号重复(下单单位支付),
*
* @return
*/
public static String generateOrderSN() {
StringBuffer orderSNBuffer = new StringBuffer();
orderSNBuffer.append(System.currentTimeMillis());
orderSNBuffer.append(getRandomStringByLength(7));
return orderSNBuffer.toString();
}
/**
* 生成随机数
* @param length
* @return
*/
public static String getRandomStringByLength(int length) {
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(RANDOM_NONECE.length());
sb.append(RANDOM_NONECE.charAt(number));
}
return sb.toString();
}
}
’这个类为商户生成的32位随机串,这里就不介绍了
接下来是需要生成唯一的签名Sign(这里的算法我们用MD5加密),如果不严格进行加签,请求到微信会报签名错误
下面我来贴一下工具类: 因为要进行拼接,我利用反射对传入的字段进行取值并拼接,然后进行签名加密
import com.example.demo.core.annotation.Sign;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.*;
public class ReflectUtils {
private static final Logger LOG = LoggerFactory.getLogger(ReflectUtils.class);
public static <T> Map<String,Object> reflectBean(T bean){
List<Field> list = ClassUtils.getClassFields(bean);
if(CollectionUtils.isEmpty(list)){
return null;
}
Map map = new HashMap(16);
for (Field field : list){
field.setAccessible(true);
try {
Object value = field.get(bean);
map.put(field.getName(),value);
} catch (IllegalAccessException e) {
e.printStackTrace();
LOG.error("error map : {}",e);
continue;
}
}
LOG.info("获取Bean : {}",map);
return map;
}
public static <T> T generatorBean(T bean) {
List<Field> list = ClassUtils.getClassFields(bean);
if (CollectionUtils.isEmpty(list)) {
return null;
}
//ASCII字母排序C
list.sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
//必有一个签名字段
String type = Constants.MD5;
//签名字段
Field signField = null;
//拼接的StringBuilder
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < list.size(); i++) {
Field field = list.get(i);
field.setAccessible(true);
try {
Object value = field.get(bean);
if (field.isAnnotationPresent(Sign.class)) {
//这里可能还需要定义一个忽略的字段,用来标记,但不传输
Sign sign = field.getAnnotation(Sign.class);
type = sign.type();
signField = field;
}
// 签名类型
if(Constants.SIGN_TYPE.equals(field.getName())){
field.set(bean,type);
}
if (StringUtils.isEmpty(sb)) {
sb.append(field.getName()).append("=").append(value);
} else {
//如果是拼接到最后一个元素,进行签名
if (i == list.size() - 1) {
try {
if (!StringUtils.isEmpty(value)) {
sb.append("&").append(field.getName()).append("=").append(value);
}
signField.set(bean, toSign(sb, type));
break;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
//过滤为null的value,不参与拼接
if (StringUtils.isEmpty(value)) {
continue;
}
sb.append("&").append(field.getName()).append("=").append(value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
LOG.error("error map : {}", e);
continue;
}
}
return bean;
}
/**
* 生成签名
* MD5
*
* @param str
* @return
*/
public static String toSign(StringBuilder str, String signType) {
StringBuilder sb = str.append("&key=").append(WxUtils.KEY);
String sign = null;
sign = new String(sb.toString());
LOG.info("生成对象成功: {}", sign);
if (Constants.MD5.equals(signType)) {
return MD5Utils.MD5Encode(sign).toUpperCase();
}
return ShA256Utils.sha256_HMAC(sign, WxUtils.KEY).toUpperCase();
}
}
`
上面的加签过程依赖了两个工具类,这里贴过来:
import com.yixuan.domain.wxpay.BaseRequest;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ClassUtils {
/**
* 继承 反射获取字段
* @param bean
* @param <T>
* @return
*/
public static <T> List<Field> getClassFields(T bean){
return sortField(bean.getClass(),new ArrayList<>());
}
/**
* 递归获取字段
* @param tClass 轮询到的当前类
* @param list 字段list
* @return 返回字段的List
*/
private static <T> List<Field> sortField(Class<T> tClass,List<Field> list){
if(StringUtils.isEmpty(tClass)){
return list;
}
Field[] fields = tClass.getDeclaredFields();
for (Field field : fields){
list.add(field);
}
Class clazz = tClass.getSuperclass();
if(clazz==null){
return list;
}
return sortField(clazz.getSuperclass(),list);
}
}
以及MD5加密的(网上有)
在这里注意,我在调试过程中总是出现签名错误,经调试,Md5加密与微信支付的加密后的不对,这里可以在微信官方进行调试: 微信公众平台支付接口调试工具
**注:在SHA256加密算法中,如果你的参数里有了中文等字符时,需要在加密前转为UTF-8,否则会 报 “签名错误” **
<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[签名错误]]></return_msg>
</xml>
在这我先贴上SHA256工具类:
package com.yixuan.utils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class ShA256Utils {
/**
* 将加密后的字节数组转换成字符串
*
* @param b 字节数组
* @return 字符串
*/
private static String byteArrayToHexString(byte[] b) {
StringBuilder hs = new StringBuilder();
String stmp;
for (int n = 0; b!=null && n < b.length; n++) {
stmp = Integer.toHexString(b[n] & 0XFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toLowerCase();
}
/**
* sha256_HMAC加密
* @param message 消息
* @param secret 秘钥
* @return 加密后字符串
*/
public static String sha256_HMAC(String message, String secret) {
String hash = "";
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
//重点 : *这里需要进行UTF-8转码,为了防止中文字符匹配签名失败**
byte[] bytes = sha256_HMAC.doFinal(message.getBytes("UTF-8"));
hash = byteArrayToHexString(bytes);
System.out.println(hash);
} catch (Exception e) {
System.out.println("Error HmacSHA256 ===========" + e.getMessage());
}
System.out.println("Sha256 生成结果:" + hash);
return hash;
}
}
import java.security.MessageDigest;
/**
* @author ChoviWu
* @date 2019/1/4
* Description :
*/
public class MD5Utils {
private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};
/**
* 转换字节数组为16进制字串
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (byte aB : b) {
resultSb.append(byteToHexString(aB));
}
return resultSb.toString();
}
/**
* 转换byte到16进制
* @param b 要转换的byte
* @return 16进制格式
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* MD5编码
* @param origin 原始字符串
* @return 经过MD5加密之后的结果
*/
public static String MD5Encode(String origin) {
String resultString = null;
try {
resultString = origin;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(resultString.getBytes("UTF-8"));
resultString = byteArrayToHexString(md.digest());
} catch (Exception e) {
e.printStackTrace();
}
return resultString;
}
}
到这一步,我们传入的对象基本已经可以拼接好并生成签名串了,由于微信所需要的是发送xml格式的,所以,我们需要对bean进行格式转化: 这里我就不贴代码了
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
public class XmlUtils {
private static final Logger LOG = LoggerFactory.getLogger(XmlUtils.class);
public static String beanToXml(Object obj){
XStream xstream = new XStream(new DomDriver("UTF-8"));
// 识别obj类中的注解
xstream.processAnnotations(obj.getClass());
// 以格式化的方式输出XML
String xml = xstream.toXML(obj);
//在这里,xstream会把bean的单下划线转化为双下划綫,这里要进行替换字符串
xml = xml.replace("__","_");
LOG.info("请求参数 : \n" + xml);
return xml;
}
//微信返回我们的数据,转化为bean类
public static <T> T toBean(String xmlStr, Class<T> cls) {
T t = null;
try {
JAXBContext context = JAXBContext.newInstance(cls);
Unmarshaller unmarshaller = context.createUnmarshaller();
t = (T)unmarshaller.unmarshal(new StringReader(xmlStr));
} catch (JAXBException e) {
e.printStackTrace();
}
return t;
}
}
至此,一切都准备就绪了,我们可以请求微信的下单接口了:
/**
* 下单
* @param unifiedOrder 自己封装的下单的bean类,与微信方保持一致
* @return
*/
public static UnifiedOrderReturn unifiedOrder(UnifiedOrder unifiedOrder){
System.out.println(JsonUtil.toJson(unifiedOrder));
//http请求微信接口,这里的参数是上面我们封装的反射调用和转化xml的类
String response = HttpUtil.sendHttpPostXml(UNIFIEDORDER,XmlUtils.beanToXml(ReflectUtils.appendToString(unifiedOrder)));
//微信返回xml 我们对其进行转化
UnifiedOrderReturn unifiedOrderReturn =XmlUtils.toBean(response,UnifiedOrderReturn.class);
//处理逻辑
if(Constants.FAIL.equals(unifiedOrderReturn.getReturn_code())){
//TODO 失败
System.out.println(JsonUtils.toJson(unifiedOrderReturn));
}
return unifiedOrderReturn;
}
至此,本篇文章结束,该部分只以下单为例,其余的也适用。