This commit is contained in:
地平线
2015-08-17 10:51:34 +08:00
parent 70e61dffda
commit ec7b1d7570
4 changed files with 605 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wentch.redkale.service.weixin;
import com.wentch.redkale.convert.json.*;
import com.wentch.redkale.service.*;
import static com.wentch.redkale.service.weixin.WeiXinQYService.MAPTYPE;
import com.wentch.redkale.util.*;
import static com.wentch.redkale.util.Utility.getHttpContent;
import java.io.*;
import java.security.*;
import java.util.*;
import java.util.logging.*;
import javax.annotation.*;
/**
* 微信服务号Service
*
* @author zhangjx
*/
public class WeiXinMPService {
protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName());
private final boolean finest = logger.isLoggable(Level.FINEST);
private final boolean finer = logger.isLoggable(Level.FINER);
protected final Map<String, String> mpsecrets = new HashMap<>();
@Resource
protected JsonConvert convert;
// http://m.xxx.com/pipes/wx/verifymp
@Resource(name = "property.wxmp.token")
protected String mptoken = "";
@Resource(name = "property.wxmp.corpid")
protected String mpcorpid = "wxYYYYYYYYYYYYYY";
@Resource(name = "property.wxmp.aeskey")
protected String mpaeskey = "";
public WeiXinMPService() {
// mpsecrets.put("wxYYYYYYYYYYYYYYYYYY", "xxxxxxxxxxxxxxxxxxxxxxxxxxx");
}
//-----------------------------------微信服务号接口----------------------------------------------------------
public RetResult<String> getMPWxunionid(String appid, String code) {
try {
Map<String, String> wxmap = getMPUserToken(appid, code);
final String unionid = wxmap.get("unionid");
if (unionid != null && !unionid.isEmpty()) return new RetResult<>(unionid);
return new RetResult<>(1011002);
} catch (IOException e) {
return new RetResult<>(1011001);
}
}
public Map<String, String> getMPUserToken(String appid, String code) throws IOException {
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid + "&secret=" + mpsecrets.get(appid) + "&code=" + code + "&grant_type=authorization_code";
String json = getHttpContent(url);
if (finest) logger.finest(url + "--->" + json);
Map<String, String> jsonmap = convert.convertFrom(MAPTYPE, json);
url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + jsonmap.get("access_token") + "&openid=" + jsonmap.get("openid");
json = getHttpContent(url);
if (finest) logger.finest(url + "--->" + json);
jsonmap = convert.convertFrom(MAPTYPE, json.replaceFirst("\\[.*\\]", "null"));
return jsonmap;
}
public String verifyMPURL(String msgSignature, String timeStamp, String nonce, String echoStr) {
String signature = sha1(mptoken, timeStamp, nonce);
if (!signature.equals(msgSignature)) throw new RuntimeException("signature verification error");
return echoStr;
}
/**
* 用SHA1算法生成安全签名
* <p>
* @param strings
* @return 安全签名
*/
protected static String sha1(String... strings) {
try {
Arrays.sort(strings);
MessageDigest md = MessageDigest.getInstance("SHA-1");
for (String s : strings) md.update(s.getBytes());
return Utility.binToHexString(md.digest());
} catch (Exception e) {
throw new RuntimeException("SHA encryption to generate signature failure", e);
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wentch.redkale.service.weixin;
import com.wentch.redkale.convert.json.*;
import java.util.*;
/**
* 微信企业号Service
*
* @author zhangjx
*/
public class WeiXinQYMessage {
private String agentid = "1";
private String msgtype = "text";
private Map<String, String> text;
private String touser = "@all";
private String toparty;
private String totag;
private String safe;
public WeiXinQYMessage() {
}
public WeiXinQYMessage(String agentid, String text) {
this.agentid = agentid;
setTextMessage(text);
}
public final void setTextMessage(String content) {
if (text == null) text = new HashMap<>();
text.put("content", content);
}
public String getAgentid() {
return agentid;
}
public void setAgentid(String agentid) {
this.agentid = agentid;
}
public String getMsgtype() {
return msgtype;
}
public void setMsgtype(String msgtype) {
this.msgtype = msgtype;
}
public Map<String, String> getText() {
return text;
}
public void setText(Map<String, String> text) {
this.text = text;
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public String getToparty() {
return toparty;
}
public void setToparty(String toparty) {
this.toparty = toparty;
}
public String getTotag() {
return totag;
}
public void setTotag(String totag) {
this.totag = totag;
}
public String getSafe() {
return safe;
}
public void setSafe(String safe) {
this.safe = safe;
}
@Override
public String toString() {
return JsonFactory.root().getConvert().convertTo(this);
}
}

View File

@@ -0,0 +1,309 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wentch.redkale.service.weixin;
import com.wentch.redkale.boot.*;
import com.wentch.redkale.convert.json.*;
import com.wentch.redkale.net.*;
import com.wentch.redkale.service.*;
import com.wentch.redkale.util.*;
import static com.wentch.redkale.util.Utility.*;
import java.io.*;
import java.lang.reflect.*;
import java.nio.charset.*;
import java.security.*;
import java.util.*;
import java.util.logging.*;
import javax.annotation.*;
import javax.crypto.*;
import javax.crypto.spec.*;
/**
*
* @author zhangjx
*/
public class WeiXinQYService implements Service {
protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName());
private final boolean finest = logger.isLoggable(Level.FINEST);
private final boolean finer = logger.isLoggable(Level.FINER);
private static class Token {
public String token;
public long expires = 7100000;
public long accesstime;
}
private static final String BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final Charset CHARSET = Charset.forName("UTF-8");
private static final Random RANDOM = new Random();
protected static final Type MAPTYPE = new TypeToken<Map<String, String>>() {
}.getType();
@Resource
protected JsonConvert convert;
//------------------------------------------------------------------------------------------------------
// http://oa.xxxx.com/pipes/wx/verifyqy
@Resource(name = "property.wxqy.token")
protected String qytoken = "";
@Resource(name = "property.wxqy.corpid")
protected String qycorpid = "wxYYYYYYYYYYYYYYYY";
@Resource(name = "property.wxqy.aeskey")
protected String qyaeskey = "";
@Resource(name = "property.wxqy.secret")
private String qysecret = "#########################";
private SecretKeySpec qykeyspec;
private IvParameterSpec qyivspec;
private final Token qyAccessToken = new Token();
//------------------------------------------------------------------------------------------------------
public WeiXinQYService() {
}
public static void main(String[] args) throws Exception {
WeiXinQYService service = Application.singleton(WeiXinQYService.class);
WeiXinQYMessage message = new WeiXinQYMessage();
message.setTextMessage("【测试】duang");
message.setAgentid("2");
service.sendQYMessage(message);
}
//-----------------------------------微信企业号接口----------------------------------------------------------
public Map<String, String> getQYUserCode(String code, String agentid) throws IOException {
String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=" + getQYAccessToken() + "&code=" + code + "&agentid=" + agentid;
String json = getHttpContent(url);
if (finest) logger.finest(url + "--->" + json);
return convert.convertFrom(MAPTYPE, json);
}
public void sendQYTextMessage(String agentid, String message) {
sendQYMessage(new WeiXinQYMessage(agentid, message));
}
public void sendQYMessage(WeiXinQYMessage message) {
submit(() -> {
String result = null;
try {
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + getQYAccessToken();
result = postHttpContent(url, convert.convertTo(message));
if (finest) logger.finest("sendQYMessage ok: " + message + " -> " + result);
} catch (Exception e) {
logger.log(Level.WARNING, "sendQYMessage error: " + message + " -> " + result, e);
}
});
}
public String verifyQYURL(String msgSignature, String timeStamp, String nonce, String echoStr) {
String signature = sha1(qytoken, timeStamp, nonce, echoStr);
if (!signature.equals(msgSignature)) throw new RuntimeException("signature verification error");
return decryptQY(echoStr);
}
protected String getQYAccessToken() throws IOException {
if (qyAccessToken.accesstime < System.currentTimeMillis() - qyAccessToken.expires) qyAccessToken.token = null;
if (qyAccessToken.token == null) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + qycorpid + "&corpsecret=" + qysecret;
String json = getHttpContent(url);
if (finest) logger.finest(url + "--->" + json);
Map<String, String> jsonmap = convert.convertFrom(MAPTYPE, json);
qyAccessToken.accesstime = System.currentTimeMillis();
qyAccessToken.token = jsonmap.get("access_token");
String exp = jsonmap.get("expires_in");
if (exp != null) qyAccessToken.expires = (Integer.parseInt(exp) - 100) * 1000;
}
return qyAccessToken.token;
}
/**
* 将公众平台回复用户的消息加密打包.
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
* <p>
* @param replyMsg 公众平台待回复用户的消息xml格式的字符串
* @param timeStamp 时间戳可以自己生成也可以用URL参数的timestamp
* @param nonce 随机串可以自己生成也可以用URL参数的nonce
* <p>
* @return 加密后的可以直接回复用户的密文包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
*/
protected String encryptQYMessage(String replyMsg, String timeStamp, String nonce) {
// 加密
String encrypt = encryptQY(random16String(), replyMsg);
// 生成安全签名
if (timeStamp == null || timeStamp.isEmpty()) timeStamp = Long.toString(System.currentTimeMillis());
String signature = sha1(qytoken, timeStamp, nonce, encrypt);
// System.out.println("发送给平台的签名是: " + signature[1].toString());
// 生成发送的xml
return "<xml>\n<Encrypt><![CDATA[" + encrypt + "]]></Encrypt>\n"
+ "<MsgSignature><![CDATA[" + signature + "]]></MsgSignature>\n"
+ "<TimeStamp>" + timeStamp + "</TimeStamp>\n"
+ "<Nonce><![CDATA[" + nonce + "]]></Nonce>\n</xml>";
}
protected String decryptQYMessage(String msgSignature, String timeStamp, String nonce, String postData) {
// 密钥公众账号的app secret
// 提取密文
String encrypt = postData.substring(postData.indexOf("<Encrypt><![CDATA[") + "<Encrypt><![CDATA[".length(), postData.indexOf("]]></Encrypt>"));
// 验证安全签名
if (!sha1(qytoken, timeStamp, nonce, encrypt).equals(msgSignature)) throw new RuntimeException("signature verification error");
return decryptQY(encrypt);
}
/**
* 对明文进行加密.
* <p>
* @param randomStr
* @param text 需要加密的明文
* @return 加密后base64编码的字符串
*/
protected String encryptQY(String randomStr, String text) {
ByteArray bytes = new ByteArray();
byte[] randomStrBytes = randomStr.getBytes(CHARSET);
byte[] textBytes = text.getBytes(CHARSET);
byte[] corpidBytes = qycorpid.getBytes(CHARSET);
// randomStr + networkBytesOrder + text + qycorpid
bytes.add(randomStrBytes);
bytes.addInt(textBytes.length);
bytes.add(textBytes);
bytes.add(corpidBytes);
// ... + pad: 使用自定义的填充方式对明文进行补位填充
byte[] padBytes = encodePKCS7(bytes.count());
bytes.add(padBytes);
// 获得最终的字节流, 未加密
try {
// 加密
byte[] encrypted = createQYCipher(Cipher.ENCRYPT_MODE).doFinal(bytes.directBytes(), 0, bytes.count());
// 使用BASE64对加密后的字符串进行编码
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("AES加密失败", e);
}
}
protected String decryptQY(String text) {
byte[] original;
try {
// 使用BASE64对密文进行解码
original = createQYCipher(Cipher.DECRYPT_MODE).doFinal(Base64.getDecoder().decode(text));
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
try {
// 去除补位字符
byte[] bytes = decodePKCS7(original);
// 分离16位随机字符串,网络字节序和corpid
int xmlLength = (bytes[16] & 0xFF) << 24 | (bytes[17] & 0xFF) << 16 | (bytes[18] & 0xFF) << 8 | bytes[19] & 0xFF;
if (!qycorpid.equals(new String(bytes, 20 + xmlLength, bytes.length - 20 - xmlLength, CHARSET))) {
throw new RuntimeException("corpid校验失败");
}
return new String(bytes, 20, xmlLength, CHARSET);
} catch (RuntimeException e) {
if (e.getMessage().contains("corpid")) throw e;
throw new RuntimeException("解密后得到的buffer非法", e);
}
}
protected Cipher createQYCipher(int mode) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
if (qykeyspec == null) {
byte[] aeskeyBytes = Base64.getDecoder().decode(qyaeskey + "=");
qykeyspec = new SecretKeySpec(aeskeyBytes, "AES");
qyivspec = new IvParameterSpec(aeskeyBytes, 0, 16);
}
cipher.init(mode, qykeyspec, qyivspec);
return cipher;
}
protected void submit(Runnable runner) {
Thread thread = Thread.currentThread();
if (thread instanceof WorkThread) {
((WorkThread) thread).submit(runner);
return;
}
runner.run();
}
//-----------------------------------通用接口----------------------------------------------------------
// 随机生成16位字符串
protected static String random16String() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++) {
sb.append(BASE.charAt(RANDOM.nextInt(BASE.length())));
}
return sb.toString();
}
/**
* 用SHA1算法生成安全签名
* <p>
* @param strings
* @return 安全签名
*/
protected static String sha1(String... strings) {
try {
Arrays.sort(strings);
MessageDigest md = MessageDigest.getInstance("SHA-1");
for (String s : strings) md.update(s.getBytes());
return Utility.binToHexString(md.digest());
} catch (Exception e) {
throw new RuntimeException("SHA encryption to generate signature failure", e);
}
}
/**
* 获得对明文进行补位填充的字节.
* <p>
* @param count 需要进行填充补位操作的明文字节个数
* @return 补齐用的字节数组
*/
private static byte[] encodePKCS7(int count) {
// 计算需要填充的位数
int amountToPad = 32 - (count % 32);
if (amountToPad == 0) amountToPad = 32;
// 获得补位所用的字符
char padChr = (char) (byte) (amountToPad & 0xFF);
StringBuilder tmp = new StringBuilder();
for (int index = 0; index < amountToPad; index++) {
tmp.append(padChr);
}
return tmp.toString().getBytes(CHARSET);
}
/**
* 删除解密后明文的补位字符
* <p>
* @param decrypted 解密后的明文
* @return 删除补位字符后的明文
*/
private static byte[] decodePKCS7(byte[] decrypted) {
int pad = (int) decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) pad = 0;
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
}

View File

@@ -0,0 +1,94 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wentch.redkale.service;
import com.wentch.redkale.convert.json.*;
/**
* 通用的结果对象
*
* @author zhangjx
* @param <T>
*/
public class RetResult<T> {
public static final RetResult SUCCESS = new RetResult() {
@Override
public void setRetcode(int retcode) {
}
@Override
public void setRetinfo(String retinfo) {
}
@Override
public void setResult(Object result) {
}
};
protected int retcode;
protected String retinfo;
private T result;
public RetResult() {
}
public RetResult(T result) {
this.result = result;
}
public RetResult(int retcode) {
this.retcode = retcode;
}
public RetResult(int retcode, String retinfo) {
this.retcode = retcode;
this.retinfo = retinfo;
}
public RetResult(int retcode, String retinfo, T result) {
this.retcode = retcode;
this.retinfo = retinfo;
this.result = result;
}
public boolean isSuccess() {
return retcode == 0;
}
public int getRetcode() {
return retcode;
}
public void setRetcode(int retcode) {
this.retcode = retcode;
}
public String getRetinfo() {
return retinfo;
}
public void setRetinfo(String retinfo) {
this.retinfo = retinfo;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
@Override
public String toString() {
return JsonFactory.root().getConvert().convertTo(this);
}
}