<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
package xyz.kingsilk.qh.service.service
import grails.converters.JSON
import grails.transaction.Transactional
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.Charset
/**
* 微信第三方平台
*/
@Transactional
class WeixinComponentService {
public static final Charset CHARSET = Charset.forName("utf-8");
RestTemplate wxRestTemplate
/** API URL : 获取第三方平台component_access_token */
public static final String COMPONENT_TOKEN = "https://api.weixin.qq.com/cgi-bin/component/api_component_token"
/** API URL : 获取预授权码pre_auth_code */
public static final String CREATE_PREAUTHCODE = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode"
/** API URL : 使用授权码换取公众号的接口调用凭据和授权信息 */
public static final String QUERY_AUTH = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth"
/** API URL : 使用授权码换取公众号的接口调用凭据和授权信息 */
public static final String AUTHORIZER_TOKEN = "https://api.weixin.qq.com /cgi-bin/component/api_authorizer_token"
/** API URL : 获取授权方的公众号帐号基本信息 */
public static
final String GET_AUTHORIZER_INFO = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info"
/** API URL : 获取授权方的选项设置信息 */
public static
final String GET_AUTHORIZER_OPTION = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option"
/** API URL : 设置授权方的选项设置信息 */
public static
final String SET_AUTHORIZER_OPTION = "https://api.weixin.qq.com/cgi-bin/component/ api_set_authorizer_option"
/**
* 获取第三方平台component_access_token
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台方appid
* @param component_appsecret ;第三方平台appsecret
* @param component_verify_ticket ;微信后台推送的ticket,此ticket会定时推送,具体请见本页的推送说明
* @return
返回结果示例{
"component_access_token":"61W3mEpU66027wgNZ_MhGHNQDHnFATkDa9-2llqrMBjUwxRSNPbVsMmyD-yq8wZETSoE5NQgecigDrSHkPtIYA",
"expires_in":7200
}
*/
public String api_component_token(String component_appid, String component_appsecret, String component_verify_ticket) {
URI uri = UriComponentsBuilder.fromHttpUrl(COMPONENT_TOKEN)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
component_appsecret : component_appsecret,
component_verify_ticket: component_verify_ticket
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 获取预授权码pre_auth_code
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台appid
* @param component_access_token
* @return{
"pre_auth_code":"Cx_Dk6qiBE0Dmx4EmlT3oRfArPvwSQ-oa3NL_fwHM7VI08r52wazoZX2Rhpz1dEw",
"expires_in":600
}
*/
public String api_create_preauthcode(String component_access_token, String component_appid) {
URI uri = UriComponentsBuilder.fromHttpUrl(CREATE_PREAUTHCODE)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid: component_appid,
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 使用授权码换取公众号的接口调用凭据和授权信息
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
* @param component_access_token
* @param authorization_code ;授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
* @return{{
"authorization_info": {
"authorizer_appid": "wxf8b4f85f3a794e77",
"authorizer_access_token": "QXjUqNqfYVH0yBE1iI_7vuN_9gQbpjfK7hYwJ3P7xOa88a89-Aga5x1NMYJyB8G2yKt1KCl0nPC3W9GJzw0Zzq_dBxc8pxIGUNi_bFes0qM",
"expires_in": 7200,
"authorizer_refresh_token": "dTo-YCXPL4llX-u1W1pPpnp8Hgm4wpJtlR6iV0doKdY",
"func_info": [{
"funcscope_category": {
"id": 1
}
},{
"funcscope_category": {
"id": 2
}
},{
"funcscope_category": {
"id": 3
}
}]
}}
*/
public String api_query_auth(String component_access_token, String component_appid, String authorization_code) {
URI uri = UriComponentsBuilder.fromHttpUrl(QUERY_AUTH)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
authorization_code: authorization_code
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 获取(刷新)授权公众号的接口调用凭据(令牌)
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台appid
* @param component_access_token
* @param authorizer_appid ;授权方appid
* @param authorizer_refresh_token ;授权方的刷新令牌
* @return {
"authorizer_access_token": "aaUl5s6kAByLwgV0BhXNuIFFUqfrR8vTATsoSHukcIGqJgrc4KmMJ-JlKoC_-NKCLBvuU1cWPv4vDcLN8Z0pn5I45mpATruU0b51hzeT1f8",
"expires_in": 7200,
"authorizer_refresh_token": "BstnRqgTJBXb9N2aJq6L5hzfJwP406tpfahQeLNxX0w"
}
*/
public String api_authorizer_token(String component_access_token, String component_appid, String authorizer_appid, String authorizer_refresh_token) {
URI uri = UriComponentsBuilder.fromHttpUrl(AUTHORIZER_TOKEN)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
authorizer_appid : authorizer_appid,
authorizer_refresh_token: authorizer_refresh_token
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 获取授权方的公众号帐号基本信息
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台appid
* @param component_access_token
* @param authorizer_appid ;授权方appid
* @return{
"authorizer_info": {
"nick_name": "微信SDK Demo Special",
"head_img": "http://wx.qlogo.cn/mmopen/GPyw0pGicibl5Eda4GmSSbTguhjg9LZjumHmVjybjiaQXnE9XrXEts6ny9Uv4Fk6hOScWRDibq1fI0WOkSaAjaecNTict3n6EjJaC/0",
"service_type_info": { "id": 2 },
"verify_type_info": { "id": 0 },
"user_name":"gh_eb5e3a772040",
"business_info": {"open_store": 0, "open_scan": 0, "open_pay": 0, "open_card": 0, "open_shake": 0},
"alias":"paytest01"
},
"qrcode_url":"URL",
"authorization_info": {
"appid": "wxf8b4f85f3a794e77",
"func_info": [{ "funcscope_category": { "id": 1 }},{ "funcscope_category": { "id": 2 }},{ "funcscope_category": { "id": 3 }}]
}
}
*/
public String api_get_authorizer_info(String component_access_token, String component_appid, String authorizer_appid) {
URI uri = UriComponentsBuilder.fromHttpUrl(GET_AUTHORIZER_INFO)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
authorizer_appid: authorizer_appid,
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 获取授权方的选项设置信息
* 该API用于获取授权方的公众号的选项设置信息,如:地理位置上报,语音识别开关,多客服开关。注意,获取各项选项设置信息,需要有授权方的授权
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台appid
* @param component_access_token
* @param authorizer_appid ;授权方appid
* @param option_name ;选项名称
* @return {
"authorizer_appid":"wx7bc5ba58cabd00f4",
"option_name":"voice_recognize",
"option_value":"1"
}
*/
public String api_get_authorizer_option(String component_access_token, String component_appid, String authorizer_appid, String option_name) {
URI uri = UriComponentsBuilder.fromHttpUrl(GET_AUTHORIZER_OPTION)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
authorizer_appid: authorizer_appid,
option_name : option_name
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 设置授权方的选项信息
* 该API用于获取授权方的公众号的选项设置信息,如:地理位置上报,语音识别开关,多客服开关。注意,获取各项选项设置信息,需要有授权方的授权
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=c921fd115eecdb869ad572e9f0e2668b29e4f495&lang=zh_CN
* @param component_appid ;第三方平台appid
* @param component_access_token
* @param authorizer_appid ;授权方appid
* @param option_name ;选项名称
* @param option_value ;设置的选项值
* @return {
"errcode":0,
"errmsg":"ok"
}
*/
public String api_set_authorizer_option(String component_access_token, String component_appid, String authorizer_appid, String option_name, String option_value) {
URI uri = UriComponentsBuilder.fromHttpUrl(SET_AUTHORIZER_OPTION)
.queryParam("component_access_token", component_access_token)
.build()
.encode("UTF-8")
.toUri()
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
def json = ([
component_appid : component_appid,
authorizer_appid: authorizer_appid,
option_name : option_name,
option_value : option_value
] as JSON)
HttpEntity<String> reqEntity = new HttpEntity<String>(json.toString(), headers)
ResponseEntity respEntity = wxRestTemplate.exchange(uri, HttpMethod.POST, reqEntity, String.class)
return respEntity?.body
}
/**
* 消息解密
* @param content //加密的内容
* @param encodingAesKey //aes的key值
* @param result //存放 公众号来自的appid 和 消息解密后的明文 msg_decrypt
* @return
*/
void decrypt(String content, String encodingAesKey, def result) {
byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=")
byte[] original;
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.decodeBase64(content);
// 解密
original = cipher.doFinal(encrypted);
// 去除补位字符
int pad = (int) original[original.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
byte[] bytes = Arrays.copyOfRange(original, 0, original.length - pad);
// 分离16位随机字符串,网络字节序和AppId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
//// 还原4个字节的网络字节序
int sourceNumber = 0;
for (int i = 0; i < 4; i++) {
sourceNumber <<= 8;
sourceNumber |= networkOrder[i] & 0xff;
}
int xmlLength = sourceNumber;
String xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), Charset.forName("utf-8"));
String from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), Charset.forName("utf-8"));
log.debug("from_appid:" + from_appid)
// appid不相同的情况
if (!from_appid.equals(result.appid)) {
log.debug("appid不相同")
result.appid = from_appid
}
result.msg_decrypt = xmlContent
}
/**
* 对明文进行加密.
* @param text 要加密的明文
* @param targetAppid //appid
* @param encodingAesKey //aes加密的key
* @return
* 加密的字符串
*/
String encrypt(String text, String targetAppid, String encodingAesKey) {
String randomStr = getRandomStr();
byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=")
ArrayList<Byte> byteCollector = new ArrayList<Byte>();
byte[] randomStrBytes = randomStr.getBytes(CHARSET);
byte[] textBytes = text.getBytes(CHARSET);
byte[] orderBytes = new byte[4];
// 生成4个字节的网络字节序
orderBytes[3] = (byte) (textBytes.length & 0xFF);
orderBytes[2] = (byte) (textBytes.length >> 8 & 0xFF);
orderBytes[1] = (byte) (textBytes.length >> 16 & 0xFF);
orderBytes[0] = (byte) (textBytes.length >> 24 & 0xFF);
byte[] networkBytesOrder = orderBytes
byte[] appidBytes = targetAppid.getBytes(CHARSET);
// randomStr + networkBytesOrder + text + appid
for (byte b : randomStrBytes) {
byteCollector.add(b);
}
for (byte b : networkBytesOrder) {
byteCollector.add(b);
}
for (byte b : textBytes) {
byteCollector.add(b);
}
for (byte b : appidBytes) {
byteCollector.add(b);
}
// ... + pad: 使用自定义的填充方式对明文进行补位填充
int amountToPad = 32 - (byteCollector.size() % 32);
if (amountToPad == 0) {
amountToPad = 32;
}
// 获得补位所用的字符
byte target = (byte) (amountToPad & 0xFF);
char padChr = (char) target
String tmp = new String();
for (int index = 0; index < amountToPad; index++) {
tmp += padChr;
}
byte[] padBytes = tmp.getBytes(CHARSET);
for (byte b : padBytes) {
byteCollector.add(b);
}
// 获得最终的字节流, 未加密
byte[] bytes = new byte[byteCollector.size()];
log.debug(byteCollector.size())
for (int i = 0; i < byteCollector.size(); i++) {
bytes[i] = byteCollector.get(i);
}
byte[] unencrypted = bytes;
// 设置加密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 加密
byte[] encrypted = cipher.doFinal(unencrypted);
// 使用BASE64对加密后的字符串进行编码
String base64Encrypted = new Base64().encodeToString(encrypted);
return base64Encrypted;
}
// 随机生成16位字符串
String getRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 16; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
/**
* 验证签名/消息验证
* @param params
* @return
*/
public boolean checkSignature(GrailsParameterMap params, String token) throws Exception {
String newSignature = getSignature(params, token)
if (params.msg_encrypt && params.msg_signature) {
return newSignature == params.msg_signature
} else {
return newSignature == params.signature
}
}
/**
* 验证签名/消息验证
* @param params
* @return
*/
public String getSignature(GrailsParameterMap params, String token) throws Exception {
String signature = params.signature
String msg_signature = params.msg_signature
String timestamp = params.timestamp
String nonce = params.nonce
//1)将token、timestamp、nonce三个参数进行字典序排序
ArrayList<String> list = new ArrayList<String>();
/**消息加密**/
if (params.msg_encrypt && params.msg_signature) {
list.add(token);
list.add(timestamp);
list.add(nonce);
list.add(params.msg_encrypt);
} else {
//签名验证
list.add(nonce);
list.add(timestamp);
list.add(token);
}
Collections.sort(list);
//2)将三个参数字符串拼接成一个字符串进行sha1加密
String newSignature
if (list.size() == 3) {
newSignature = DigestUtils.shaHex(list.get(0) + list.get(1) + list.get(2))
//3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
return newSignature
} else {
newSignature = DigestUtils.shaHex(list.get(0) + list.get(1) + list.get(2) + list.get(3))
//3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
return newSignature
}
}
}