@@ -19,3 +19,4 @@ target | |||
sw-pom.xml | |||
*.iml | |||
test-config.xml | |||
.idea |
@@ -1,5 +1,7 @@ | |||
package me.chanjar.weixin.common.util.crypto; | |||
import org.apache.commons.codec.digest.DigestUtils; | |||
import java.security.MessageDigest; | |||
import java.security.NoSuchAlgorithmException; | |||
import java.util.Arrays; | |||
@@ -21,7 +23,7 @@ public class SHA1 { | |||
for (String a : arr) { | |||
sb.append(a); | |||
} | |||
return genStr(sb.toString()); | |||
return DigestUtils.sha1Hex(sb.toString()); | |||
} | |||
/** | |||
@@ -40,25 +42,6 @@ public class SHA1 { | |||
sb.append('&'); | |||
} | |||
} | |||
return genStr(sb.toString()); | |||
} | |||
public static String genStr(String str) throws NoSuchAlgorithmException { | |||
MessageDigest sha1 = MessageDigest.getInstance("SHA1"); | |||
sha1.update(str.getBytes()); | |||
byte[] output = sha1.digest(); | |||
return bytesToHex(output); | |||
return DigestUtils.sha1Hex(sb.toString()); | |||
} | |||
protected static String bytesToHex(byte[] b) { | |||
char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', | |||
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; | |||
StringBuffer buf = new StringBuffer(); | |||
for (int j = 0; j < b.length; j++) { | |||
buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | |||
buf.append(hexDigit[b[j] & 0x0f]); | |||
} | |||
return buf.toString(); | |||
} | |||
} | |||
} |
@@ -14,6 +14,7 @@ | |||
package me.chanjar.weixin.common.util.crypto; | |||
import org.apache.commons.codec.binary.Base64; | |||
import org.apache.commons.codec.digest.DigestUtils; | |||
import org.w3c.dom.Document; | |||
import org.w3c.dom.Element; | |||
import org.xml.sax.InputSource; | |||
@@ -27,8 +28,7 @@ import javax.xml.parsers.ParserConfigurationException; | |||
import java.io.StringReader; | |||
import java.nio.charset.Charset; | |||
import java.security.NoSuchAlgorithmException; | |||
import java.util.Arrays; | |||
import java.util.Random; | |||
import java.util.*; | |||
public class WxCryptUtil { | |||
@@ -224,6 +224,36 @@ public class WxCryptUtil { | |||
} | |||
/** | |||
* 微信公众号支付签名算法(详见:http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=4_3) | |||
* @param packageParams 原始参数 | |||
* @param signKey 加密Key(即 商户Key) | |||
* @param charset 编码 | |||
* @return 签名字符串 | |||
*/ | |||
public static String createSign(Map<String, String> packageParams, String signKey) { | |||
SortedMap<String, String> sortedMap = new TreeMap<String, String>(); | |||
sortedMap.putAll(packageParams); | |||
List<String> keys = new ArrayList<String>(packageParams.keySet()); | |||
Collections.sort(keys); | |||
StringBuffer toSign = new StringBuffer(); | |||
for (String key : keys) { | |||
String value = packageParams.get(key); | |||
if (null != value && !"".equals(value) && !"sign".equals(key) | |||
&& !"key".equals(key)) { | |||
toSign.append(key + "=" + value + "&"); | |||
} | |||
} | |||
toSign.append("key=" + signKey); | |||
System.out.println(toSign.toString()); | |||
String sign = DigestUtils.md5Hex(toSign.toString()) | |||
.toUpperCase(); | |||
return sign; | |||
} | |||
/** | |||
* 将一个数字转换成生成4个字节的网络字节序bytes数组 | |||
* | |||
@@ -50,6 +50,10 @@ public interface WxMpConfigStorage { | |||
public String getSecret(); | |||
public String getPartnerId(); | |||
public String getPartnerKey(); | |||
public String getToken(); | |||
public String getAesKey(); | |||
@@ -11,6 +11,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
protected volatile String appId; | |||
protected volatile String secret; | |||
protected volatile String partnerId; | |||
protected volatile String partnerKey; | |||
protected volatile String token; | |||
protected volatile String accessToken; | |||
protected volatile String aesKey; | |||
@@ -168,6 +170,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
"appId='" + appId + '\'' + | |||
", secret='" + secret + '\'' + | |||
", token='" + token + '\'' + | |||
", partnerId='" + partnerId + '\'' + | |||
", partnerKey='" + partnerKey + '\'' + | |||
", accessToken='" + accessToken + '\'' + | |||
", aesKey='" + aesKey + '\'' + | |||
", expiresTime=" + expiresTime + | |||
@@ -179,4 +183,22 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + | |||
'}'; | |||
} | |||
@Override | |||
public String getPartnerId() { | |||
return partnerId; | |||
} | |||
public void setPartnerId(String partnerId) { | |||
this.partnerId = partnerId; | |||
} | |||
@Override | |||
public String getPartnerKey() { | |||
return partnerKey; | |||
} | |||
public void setPartnerKey(String partnerKey) { | |||
this.partnerKey = partnerKey; | |||
} | |||
} |
@@ -11,9 +11,11 @@ import me.chanjar.weixin.mp.bean.result.*; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.math.BigDecimal; | |||
import java.text.SimpleDateFormat; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* 微信API的Service | |||
@@ -332,6 +334,18 @@ public interface WxMpService { | |||
*/ | |||
public WxMpQrCodeTicket qrCodeCreateLastTicket(int scene_id) throws WxErrorException; | |||
/** | |||
* <pre> | |||
* 换取永久字符串二维码ticket | |||
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=生成带参数的二维码 | |||
* </pre> | |||
* | |||
* @param scene_str 参数。字符串类型长度现在为1到64 | |||
* @return | |||
* @throws WxErrorException | |||
*/ | |||
public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException; | |||
/** | |||
* <pre> | |||
* 换取二维码图片文件,jpg格式 | |||
@@ -528,4 +542,32 @@ public interface WxMpService { | |||
*/ | |||
void setMaxRetryTimes(int maxRetryTimes); | |||
/** | |||
* 统一下单(详见http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=9_1) | |||
* 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识" | |||
* @param openId 支付人openId | |||
* @param outTradeNo 商户端对应订单号 | |||
* @param amt 金额(单位元) | |||
* @param body 商品描述 | |||
* @param tradeType 交易类型 JSAPI,NATIVE,APP,WAP | |||
* @param ip 发起支付的客户端IP | |||
* @param notifyUrl 通知地址 | |||
* @return | |||
*/ | |||
WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl); | |||
/** | |||
* 该接口调用“统一下单”接口,并拼装JSSDK发起支付请求需要的参数 | |||
* 详见http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E5.8F.91.E8.B5.B7.E4.B8.80.E4.B8.AA.E5.BE.AE.E4.BF.A1.E6.94.AF.E4.BB.98.E8.AF.B7.E6.B1.82 | |||
* @param openId 支付人openId | |||
* @param outTradeNo 商户端对应订单号 | |||
* @param amt 金额(单位元) | |||
* @param body 商品描述 | |||
* @param tradeType 交易类型 JSAPI,NATIVE,APP,WAP | |||
* @param ip 发起支付的客户端IP | |||
* @param notifyUrl 通知地址 | |||
* @return | |||
*/ | |||
Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl); | |||
} |
@@ -6,6 +6,7 @@ import com.google.gson.JsonObject; | |||
import com.google.gson.internal.Streams; | |||
import com.google.gson.reflect.TypeToken; | |||
import com.google.gson.stream.JsonReader; | |||
import com.thoughtworks.xstream.XStream; | |||
import me.chanjar.weixin.common.bean.WxAccessToken; | |||
import me.chanjar.weixin.common.bean.WxMenu; | |||
import me.chanjar.weixin.common.bean.WxJsapiSignature; | |||
@@ -17,13 +18,16 @@ import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.common.util.RandomUtils; | |||
import me.chanjar.weixin.common.util.StringUtils; | |||
import me.chanjar.weixin.common.util.crypto.SHA1; | |||
import me.chanjar.weixin.common.util.crypto.WxCryptUtil; | |||
import me.chanjar.weixin.common.util.fs.FileUtils; | |||
import me.chanjar.weixin.common.util.http.*; | |||
import me.chanjar.weixin.common.util.json.GsonHelper; | |||
import me.chanjar.weixin.common.util.xml.XStreamInitializer; | |||
import me.chanjar.weixin.mp.bean.*; | |||
import me.chanjar.weixin.mp.bean.result.*; | |||
import me.chanjar.weixin.mp.util.http.QrCodeRequestExecutor; | |||
import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
import org.apache.http.Consts; | |||
import org.apache.http.HttpHost; | |||
import org.apache.http.auth.AuthScope; | |||
import org.apache.http.auth.UsernamePasswordCredentials; | |||
@@ -32,6 +36,8 @@ import org.apache.http.client.CredentialsProvider; | |||
import org.apache.http.client.config.RequestConfig; | |||
import org.apache.http.client.methods.CloseableHttpResponse; | |||
import org.apache.http.client.methods.HttpGet; | |||
import org.apache.http.client.methods.HttpPost; | |||
import org.apache.http.entity.StringEntity; | |||
import org.apache.http.impl.client.BasicCredentialsProvider; | |||
import org.apache.http.impl.client.BasicResponseHandler; | |||
import org.apache.http.impl.client.CloseableHttpClient; | |||
@@ -43,10 +49,9 @@ import java.io.File; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.StringReader; | |||
import java.math.BigDecimal; | |||
import java.security.NoSuchAlgorithmException; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.UUID; | |||
import java.util.*; | |||
public class WxMpServiceImpl implements WxMpService { | |||
@@ -332,6 +337,19 @@ public class WxMpServiceImpl implements WxMpService { | |||
String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString()); | |||
return WxMpQrCodeTicket.fromJson(responseContent); | |||
} | |||
public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException { | |||
String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create"; | |||
JsonObject json = new JsonObject(); | |||
json.addProperty("action_name", "QR_LIMIT_STR_SCENE"); | |||
JsonObject actionInfo = new JsonObject(); | |||
JsonObject scene = new JsonObject(); | |||
scene.addProperty("scene_str", scene_str); | |||
actionInfo.add("scene", scene); | |||
json.add("action_info", actionInfo); | |||
String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString()); | |||
return WxMpQrCodeTicket.fromJson(responseContent); | |||
} | |||
public File qrCodePicture(WxMpQrCodeTicket ticket) throws WxErrorException { | |||
String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode"; | |||
@@ -613,4 +631,77 @@ public class WxMpServiceImpl implements WxMpService { | |||
this.maxRetryTimes = maxRetryTimes; | |||
} | |||
@Override | |||
public WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) { | |||
String nonce_str = System.currentTimeMillis() + ""; | |||
SortedMap<String, String> packageParams = new TreeMap<String, String>(); | |||
packageParams.put("appid", wxMpConfigStorage.getAppId()); | |||
packageParams.put("mch_id", wxMpConfigStorage.getPartnerId()); | |||
packageParams.put("nonce_str", nonce_str); | |||
packageParams.put("body", body); | |||
packageParams.put("out_trade_no", outTradeNo); | |||
packageParams.put("total_fee", (int)(amt*100) + ""); | |||
packageParams.put("spbill_create_ip", ip); | |||
packageParams.put("notify_url", callbackUrl); | |||
packageParams.put("trade_type", tradeType); | |||
packageParams.put("openid", openId); | |||
String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey()); | |||
String xml = "<xml>" + | |||
"<appid>" + wxMpConfigStorage.getAppId() + "</appid>" + | |||
"<mch_id>" + wxMpConfigStorage.getPartnerId() + "</mch_id>" + | |||
"<nonce_str>" + nonce_str + "</nonce_str>" + | |||
"<sign>" + sign + "</sign>" + | |||
"<body><![CDATA[" + body + "]]></body>" + | |||
"<out_trade_no>" + outTradeNo + "</out_trade_no>" + | |||
"<total_fee>" + packageParams.get("total_fee") + "</total_fee>" + | |||
"<spbill_create_ip>" + ip + "</spbill_create_ip>" + | |||
"<notify_url>" + callbackUrl + "</notify_url>" + | |||
"<trade_type>" + tradeType + "</trade_type>" + | |||
"<openid>" + openId + "</openid>" + | |||
"</xml>"; | |||
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder"); | |||
if (httpProxy != null) { | |||
RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
httpPost.setConfig(config); | |||
} | |||
StringEntity entity = new StringEntity(xml, Consts.UTF_8); | |||
httpPost.setEntity(entity); | |||
try { | |||
CloseableHttpResponse response = httpClient.execute(httpPost); | |||
String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||
XStream xstream = XStreamInitializer.getInstance(); | |||
xstream.alias("xml", WxMpPrepayIdResult.class); | |||
WxMpPrepayIdResult wxMpPrepayIdResult = (WxMpPrepayIdResult) xstream.fromXML(responseContent); | |||
return wxMpPrepayIdResult; | |||
} catch (IOException e) { | |||
e.printStackTrace(); | |||
} | |||
return new WxMpPrepayIdResult(); | |||
} | |||
@Override | |||
public Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) { | |||
WxMpPrepayIdResult wxMpPrepayIdResult = getPrepayId(openId, outTradeNo, amt, body, tradeType, ip, callbackUrl); | |||
String prepayId = wxMpPrepayIdResult.getPrepay_id(); | |||
if (prepayId == null || prepayId.equals("")) { | |||
throw new RuntimeException("get prepayid error"); | |||
} | |||
Map<String, String> payInfo = new HashMap<String, String>(); | |||
payInfo.put("appId", wxMpConfigStorage.getAppId()); | |||
// 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 | |||
payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); | |||
payInfo.put("nonceStr", System.currentTimeMillis() + ""); | |||
payInfo.put("package", "prepay_id=" + prepayId); | |||
payInfo.put("signType", "MD5"); | |||
String finalSign = WxCryptUtil.createSign(payInfo, wxMpConfigStorage.getPartnerKey()); | |||
payInfo.put("sign", finalSign); | |||
return payInfo; | |||
} | |||
} |
@@ -0,0 +1,98 @@ | |||
package me.chanjar.weixin.mp.bean.result; | |||
import java.io.Serializable; | |||
/** | |||
* <pre> | |||
* 群发消息一发送就返回的结果 | |||
* | |||
* 真正的群发消息是否发送成功要看 | |||
* http://mp.weixin.qq.com/wiki/index.php?title=高级群发接口#.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81.E7.BE.A4.E5.8F.91.E7.BB.93.E6.9E.9C | |||
* | |||
* </pre> | |||
* | |||
* @author chanjarster | |||
*/ | |||
public class WxMpPrepayIdResult implements Serializable { | |||
private String return_code; | |||
private String return_msg; | |||
private String appid; | |||
private String mch_id; | |||
private String nonce_str; | |||
private String sign; | |||
private String result_code; | |||
private String prepay_id; | |||
private String trade_type; | |||
public String getReturn_code() { | |||
return return_code; | |||
} | |||
public void setReturn_code(String return_code) { | |||
this.return_code = return_code; | |||
} | |||
public String getReturn_msg() { | |||
return return_msg; | |||
} | |||
public void setReturn_msg(String return_msg) { | |||
this.return_msg = return_msg; | |||
} | |||
public String getAppid() { | |||
return appid; | |||
} | |||
public void setAppid(String appid) { | |||
this.appid = appid; | |||
} | |||
public String getMch_id() { | |||
return mch_id; | |||
} | |||
public void setMch_id(String mch_id) { | |||
this.mch_id = mch_id; | |||
} | |||
public String getNonce_str() { | |||
return nonce_str; | |||
} | |||
public void setNonce_str(String nonce_str) { | |||
this.nonce_str = nonce_str; | |||
} | |||
public String getSign() { | |||
return sign; | |||
} | |||
public void setSign(String sign) { | |||
this.sign = sign; | |||
} | |||
public String getResult_code() { | |||
return result_code; | |||
} | |||
public void setResult_code(String result_code) { | |||
this.result_code = result_code; | |||
} | |||
public String getPrepay_id() { | |||
return prepay_id; | |||
} | |||
public void setPrepay_id(String prepay_id) { | |||
this.prepay_id = prepay_id; | |||
} | |||
public String getTrade_type() { | |||
return trade_type; | |||
} | |||
public void setTrade_type(String trade_type) { | |||
this.trade_type = trade_type; | |||
} | |||
} |