微信服务平台快速开发小程序

需求

根据一个小程序,发布一个和其一模一样的小程序。(可能不只一个)

如果按常规的做法每个小程序都需要经历这样的步骤:在公众平台申请appid——>拉代码到自己的仓库——>打开编译器绑定自己的appId——>上传代码到公众平台——>再提交审核——>在公众平台调整开发设置。

如果用微信开放平台的服务平台开发的步骤:在微信开放平台申请第三方服务平台——>上传小程序模板(只有第一次有)——>调接口上传小程序代码

——>调接口提交审核——>调接口发布小程序。

上传小程序模板的步骤:在公众平台申请appid——>在微信开放平台的第三方服务平台中添加开发小程序——>上传小程序代码(此时会直接上传到了开放平台的草稿箱)——>将草稿箱的代码作为普通模板

这就是服务平台开发平台的便捷,只需上传一次小程序代码,就可以快速的发布小程序。

实现

步骤:获取component_verify_ticket——>获取component_access_token——>获取authorizer_access_token——>调接口发布小程序

1、获取component_verify_ticket

官方文档:验证票据 | 微信开放文档 (qq.com)

因为这个验证票据是微信官方主动推送的,所以需要在第三方平台配置一波

image-20220127142943253

controller

1
2
3
4
5
6
7
8
@ApiOperation(value = "微信开放平台:授权事件接收URL,验证票据", notes = "zhuguangliang")
@AnonymousPostMapping("/pushTicket")
public String wechatPlatformEvent(@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("msg_signature") String msgSignature,
@RequestBody String postData) {
return openPlatformUtil.parseRequest(timestamp, nonce, msgSignature, postData);
}

实现类

关于存储component_verify_ticket方案,我这里是mysql和redis各存一份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public String parseRequest(String timeStamp, String nonce, String msgSignature, String postData) {
try {
if (redisUtils.hasKey(CacheKey.OPEN_PLATFORM_TICKET)) {
return "success";
}
//这个类是微信官网提供的解密类,需要用到消息校验Token 消息加密Key和服务平台appid
WXBizMsgCrypt pc = new WXBizMsgCrypt(Token, Key, APPID);
String xml = pc.decryptMsg(msgSignature, timeStamp, nonce, postData);
Map<String, String> result = WXXmlToMapUtil.xmlToMap(xml);// 将xml转为map
String componentVerifyTicket = result.get("ComponentVerifyTicket");
if (StringUtils.isNotEmpty(componentVerifyTicket)) {
// 存储平台授权票据,保存ticket
SpiritOpenPlatform spiritOpenPlatform = new SpiritOpenPlatform();
if (platformMapper.selectByAppId(APPID) == 0) {
spiritOpenPlatform.setAppid(APPID);
spiritOpenPlatform.setAppSecret(AppSecret);
spiritOpenPlatform.setPlatformKey(Key);
spiritOpenPlatform.setToken(Token);
spiritOpenPlatform.setComponentVerifyTicket(componentVerifyTicket);
platformMapper.insert(spiritOpenPlatform);
} else {
spiritOpenPlatform.setComponentVerifyTicket(componentVerifyTicket);
LambdaUpdateWrapper<SpiritOpenPlatform> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SpiritOpenPlatform::getAppid, APPID);
platformMapper.update(spiritOpenPlatform, updateWrapper);
}
redisUtils.set(CacheKey.OPEN_PLATFORM_TICKET, componentVerifyTicket, 60 * 60 * 12);
log.info("微信开放平台,第三方平台获取【验证票据】成功");
} else {
log.error("微信开放平台,第三方平台获取【验证票据】失败");
}
} catch (AesException e) {
log.error("微信开放平台,第三方平台获取【验证票据】失败,异常信息:" + e.getMessage());
}
return "success";
}

官方提供的解密类:WXBizMsgCrypt.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;

/**
* 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
* <ol> * <li>第三方回复加密消息给公众平台</li> * <li>第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。</li>
* </ol>
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
* <ol>
* <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: *
* http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
* <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
* <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
* <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
*
* </ol>
*/
public class WXBizMsgCrypt {
static Charset CHARSET = Charset.forName("utf-8");
Base64 base64 = new Base64();
byte[] aesKey;
String token;
String appId;

/**
* 构造函数
*
* @param token 公众平台上,开发者设置的token
* @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
* @param appId 公众平台appid
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException {
if (encodingAesKey.length() != 43) {
throw new AesException(AesException.IllegalAesKey);
}

this.token = token;
this.appId = appId;
aesKey = Base64.decodeBase64(encodingAesKey + "=");
}

// 还原4个字节的网络字节序
int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
for (int i = 0; i < 4; i++) {
sourceNumber <<= 8;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}

/**
* 对密文进行解密.
*
* @param text 需要解密的密文
* @return 解密得到的明文
* @throws AesException aes解密失败
*/
String decrypt(String text) throws AesException {
byte[] original;
try {
// 设置解密模式为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(text);

// 解密
original = cipher.doFinal(encrypted);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.DecryptAESError);
}

String xmlContent, from_appid;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);

// 分离16位随机字符串,网络字节序和AppId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

int xmlLength = recoverNetworkBytesOrder(networkOrder);

xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
from_appid =
new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.IllegalBuffer);
}

// appid不相同的情况
if (!from_appid.equals(appId)) {
throw new AesException(AesException.ValidateSignatureError);
}
return xmlContent;

}

/**
* * 检验消息的真实性,并且获取解密后的明文.
* <ol>
* <li>利用收到的密文生成安全签名,进行签名验证</li>
* <li>若验证通过,则提取xml中的加密消息</li>
* <li>对消息进行解密</li>
* </ol>
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param postData 密文,对应POST请求的数据
* @return 解密后的原文
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
throws AesException {

// 密钥,公众账号的app secret
// 提取密文
Object[] encrypt = extract(postData);

// 验证安全签名
String signature = getSHA1(token, timeStamp, nonce, encrypt[1].toString());

// 和URL中的签名比较是否相等
// System.out.println("第三方收到URL中的签名:" + msg_sign);
// System.out.println("第三方校验签名:" + signature);
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}

// 解密
String result = decrypt(encrypt[1].toString());
return result;
}

/**
* 提取出xml数据包中的加密消息
*
* @param xmltext 待提取的xml字符串
* @return 提取出的加密消息字符串
* @throws AesException
*/
public static Object[] extract(String xmltext) throws AesException {
Object[] result = new Object[3];
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(xmltext);
InputSource is = new InputSource(sr);
Document document = db.parse(is);

Element root = document.getDocumentElement();
NodeList nodelist1 = root.getElementsByTagName("Encrypt");
NodeList nodelist2 = root.getElementsByTagName("ToUserName");
result[0] = 0;
result[1] = nodelist1.item(0).getTextContent();

//注意这里,获取ticket中的xml里面没有ToUserName这个元素,官网原示例代码在这里会报空
//空指针,所以需要处理一下
if (nodelist2 != null) {
if (nodelist2.item(0) != null) {
result[2] = nodelist2.item(0).getTextContent();
}
}
return result;
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ParseXmlError);
}
}

/**
* 用SHA1算法生成安全签名
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
* @throws AesException
*/
public static String getSHA1(String token, String timestamp, String nonce, String encrypt)
throws AesException {
try {
String[] array = new String[]{token, timestamp, nonce, encrypt};
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < 4; i++) {
sb.append(array[i]);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();

StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
}

PKCS7Encode.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.nio.charset.Charset;
import java.util.Arrays;

/**
* 提供基于PKCS7算法的加解密接口.
*/
public class PKCS7Encoder {
static Charset CHARSET = Charset.forName("utf-8");
static int BLOCK_SIZE = 32;

/**
* 获得对明文进行补位填充的字节.
*
* @param count 需要进行填充补位操作的明文字节个数
* @return 补齐用的字节数组
*/
static byte[] encode(int count) {
// 计算需要填充的位数
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
if (amountToPad == 0) {
amountToPad = BLOCK_SIZE;
}
// 获得补位所用的字符
char padChr = chr(amountToPad);
String tmp = new String();
for (int index = 0; index < amountToPad; index++) {
tmp += padChr;
}
return tmp.getBytes(CHARSET);
}

/**
* 删除解密后明文的补位字符
*
* @param decrypted 解密后的明文
* @return 删除补位字符后的明文
*/
static byte[] decode(byte[] decrypted) {
int pad = (int) decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}

/**
* 将数字转化成ASCII码对应的字符,用于对明文进行补码
*
* @param a 需要转化的数字
* @return 转化得到的字符
*/
static char chr(int a) {
byte target = (byte) (a & 0xFF);
return (char) target;
}
}

WXXmlToMapUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WXXmlToMapUtil {

private static final Logger logger = LoggerFactory.getLogger(WXXmlToMapUtil.class);

/**
* XML格式字符串转换为Map
*
* @param xml XML字符串
* @return XML数据转换后的Map
*/
public static Map<String, String> xmlToMap(String xml) {
try {
Map<String, String> data = new HashMap<>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(xml.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());
}
}
stream.close();
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
*/
public static String mapToXml(Map<String, String> data) throws Exception {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key : data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
writer.close();
return output;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* (多层)xml格式字符串转换为map
*
* @param xml xml字符串
* @return 第一个为Root节点,Root节点之后为Root的元素,如果为多层,可以通过key获取下一层Map
*/
public static Map<String, Object> multilayerXmlToMap(String xml) {
Document doc = null;
try {
doc = DocumentHelper.parseText(xml);
} catch (DocumentException e) {
logger.error("xml字符串解析,失败 --> {}", e);
}
Map<String, Object> map = new HashMap<>();
if (null == doc) {
return map;
}
// 获取根元素
Element rootElement = doc.getRootElement();
recursionXmlToMap(rootElement, map);
return map;
}

/**
* multilayerXmlToMap核心方法,递归调用
*
* @param element 节点元素
* @param outmap 用于存储xml数据的map
*/
private static void recursionXmlToMap(Element element, Map<String, Object> outmap) {
// 得到根元素下的子元素列表
List<Element> list = element.elements();
int size = list.size();
if (size == 0) {
// 如果没有子元素,则将其存储进map中
outmap.put(element.getName(), element.getTextTrim());
} else {
// innermap用于存储子元素的属性名和属性值
Map<String, Object> innermap = new HashMap<>();
// 遍历子元素
list.forEach(childElement -> recursionXmlToMap(childElement, innermap));
outmap.put(element.getName(), innermap);
}
}

/**
* (多层)map转换为xml格式字符串
*
* @param map 需要转换为xml的map
* @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
* @return xml字符串
*/
public static String multilayerMapToXml(Map<String, Object> map, boolean isCDATA) {
String parentName = "xml";
Document doc = DocumentHelper.createDocument();
doc.addElement(parentName);
String xml = recursionMapToXml(doc.getRootElement(), parentName, map, isCDATA);
return formatXML(xml);
}

/**
* multilayerMapToXml核心方法,递归调用
*
* @param element 节点元素
* @param parentName 根元素属性名
* @param map 需要转换为xml的map
* @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
* @return xml字符串
*/
private static String recursionMapToXml(Element element, String parentName, Map<String, Object> map, boolean isCDATA) {
Element xmlElement = element.addElement(parentName);
map.keySet().forEach(key -> {
Object obj = map.get(key);
if (obj instanceof Map) {
recursionMapToXml(xmlElement, key, (Map<String, Object>) obj, isCDATA);
} else {
String value = obj == null ? "" : obj.toString();
if (isCDATA) {
xmlElement.addElement(key).addCDATA(value);
} else {
xmlElement.addElement(key).addText(value);
}
}
});
return xmlElement.asXML();
}

/**
* 格式化xml,显示为容易看的XML格式
*
* @param xml 需要格式化的xml字符串
*/
public static String formatXML(String xml) {
String requestXML = null;
try {
// 拿取解析器
SAXReader reader = new SAXReader();
Document document = reader.read(new StringReader(xml));
if (null != document) {
StringWriter stringWriter = new StringWriter();
// 格式化,每一级前的空格
OutputFormat format = new OutputFormat(" ", true);
// xml声明与内容是否添加空行
format.setNewLineAfterDeclaration(false);
// 是否设置xml声明头部
format.setSuppressDeclaration(false);
// 是否分行
format.setNewlines(true);
XMLWriter writer = new XMLWriter(stringWriter, format);
writer.write(document);
writer.flush();
writer.close();
requestXML = stringWriter.getBuffer().toString();
}
return requestXML;
} catch (Exception e) {
logger.error("格式化xml,失败 --> {}", e);
return null;
}
}
}

AesException.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@SuppressWarnings("serial")
public class AesException extends Exception {

public final static int OK = 0;
public final static int ValidateSignatureError = -40001;
public final static int ParseXmlError = -40002;
public final static int ComputeSignatureError = -40003;
public final static int IllegalAesKey = -40004;
public final static int ValidateCorpidError = -40005;
public final static int EncryptAESError = -40006;
public final static int DecryptAESError = -40007;
public final static int IllegalBuffer = -40008;

private int code;

private static String getMessage(int code) {
switch (code) {
case ValidateSignatureError:
return "签名验证错误";
case ParseXmlError:
return "xml解析失败";
case ComputeSignatureError:
return "sha加密生成签名失败";
case IllegalAesKey:
return "SymmetricKey非法";
case ValidateCorpidError:
return "corpid校验失败";
case EncryptAESError:
return "aes加密失败";
case DecryptAESError:
return "aes解密失败";
case IllegalBuffer:
return "解密后得到的buffer非法";
default:
return null; // cannot be
}
}

public int getCode() {
return code;
}

AesException(int code) {
super(getMessage(code));
this.code = code;
}

}

2、获取component_access_token

官方文档:令牌 | 微信开放文档 (qq.com)

实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
public void getComponentAccessToken() {
String componentAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/component/api_component_token";
if (redisUtils.hasKey(CacheKey.COMPONENT_ACCESS_TOKEN)) {
if (redisUtils.getExpire(CacheKey.COMPONENT_ACCESS_TOKEN) > 10 * 60) {
return;
}
}
JSONObject params = new JSONObject();
//第三方平台的appid、appsecret
params.put("component_appid", APPID);
params.put("component_appsecret", AppSecret);
String component_verify_ticket;
if (redisUtils.hasKey(CacheKey.OPEN_PLATFORM_TICKET)) {
component_verify_ticket = (String) redisUtils.get(CacheKey.OPEN_PLATFORM_TICKET);
} else {
//查询数据库中的component_verify_ticket
component_verify_ticket = platformMapper.selectTicket();
redisUtils.set(CacheKey.OPEN_PLATFORM_TICKET, component_verify_ticket, 60 * 60 * 12);
}
if (StringUtils.isBlank(component_verify_ticket)) {
throw new BadRequestException("微信开放平台,第三方平台获取【验证票据】失败");
}
params.put("component_verify_ticket", component_verify_ticket);
JSONObject data = JSONObject.parseObject(HttpUtil.post(componentAccessTokenUrl, JSON.toJSONString(params)));
if (data.containsKey("component_access_token")) {
String componentAccessToken = data.getString("component_access_token");
//将component_access_token存入redis中,并设置2个小时的过期时间
redisUtils.set(CacheKey.COMPONENT_ACCESS_TOKEN, componentAccessToken, 60 * 60 * 2);
return;
}
redisUtils.del(CacheKey.COMPONENT_ACCESS_TOKEN);
redisUtils.del(CacheKey.OPEN_PLATFORM_TICKET);
throw new BadRequestException("获取component_access_token失败,请重试,接口返回信息:" + data.toJSONString());
}

3、获取authorizer_access_token

步骤:

获取预授权码pre_auth_code——>微信公众平台管理员授权——>获取authorizer_access_token&authorizer_refresh_token

官方文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html

①获取预授权码pre_auth_code

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public String getPreAuthCode() {
//先判断component_access_token是否过期,如果过期重新获取
getComponentAccessToken();
String preAuthCodeUrl = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=" + redisUtils.get(CacheKey.COMPONENT_ACCESS_TOKEN);
JSONObject params = new JSONObject();
params.put("component_appid", APPID);
JSONObject data = JSONObject.parseObject(HttpUtil.post(preAuthCodeUrl, JSON.toJSONString(params)));
if (data.containsKey("pre_auth_code")) {
return data.getString("pre_auth_code");
}
throw new BadRequestException("获取预授权失败,接口返回信息:" + data.toJSONString());
}

②微信公众平台管理员授权

生成授权链接,可以在前端用a标签填入这个接口地址,点击a标签打开新标签页,管理员进行扫码授权

1
2
3
4
5
6
7
8
@Override
public void toAuthorization(HttpServletResponse response) throws IOException {
String authUrl = "https://mp.weixin.qq.com/cgi-bin/componentloginpagecomponent_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=1";
String redirectUrl = "https://服务端地址/callback";
String preAuthCode = getPreAuthCode();
response.setStatus(301);
response.sendRedirect(String.format(authUrl, APPID, preAuthCode, redirectUrl));
}

授权之后,会回调:“https://服务端地址/callback”,并将授权码auth_code携带到这个接口

1
2
3
4
5
6
@ApiOperation(value = "微信开放平台:用户授权后,回调地址", notes = "zhuguangliang")
@AnonymousGetMapping("/callback")
@Notice("2393194918@qq.com")
public void callback(String auth_code) {
// openPlatformService.getAuthorizationInfo(auth_code);
}

③获取authorizer_access_token&authorizer_refresh_token

官方文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html

说明:authorization_code就是授权之后,返回的授权码

如果你使用微信的第三方平台管理工具,可以在数据库中直接获取authorizer_refresh_token,然后根据:

获取/刷新接口调用令牌 | 微信开放文档 (qq.com)这个接口进行刷新authorizer_access_token,下面是实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override//authorizerAppId为授权的appid
public void getAuthorizationInfo(String authorizerAppId) {

String key = CacheKey.AUTH_ACCESS_TOKEN + authorizerAppId;
//这一步是为如果发现这个token快过期,则刷新
if (redisUtils.hasKey(key) && redisUtils.getExpire(key) > 5 * 60) {
return;
}
getComponentAccessToken();
String refreshTokenUrl = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=" + redisUtils.get(CacheKey.COMPONENT_ACCESS_TOKEN);
JSONObject param = new JSONObject();
param.put("component_appid", APPID);
param.put("authorizer_appid", authorizerAppId);
String refreshToken;
if (redisUtils.hasKey(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId)) {
refreshToken = (String) redisUtils.get(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId);
} else {
//从微信第三方管理工具的数据库中查询出authorizer_refresh_token
refreshToken = platformMapper.selectRefreshTokenByAppId(authorizerAppId);
if (StringUtils.isBlank(refreshToken)) {
throw new BadRequestException("小程序管理员未授权,请让小程序管理员重新授权");
}
//计算刷新令牌的过期时间
//从微信第三方管理工具的数据库中查询出authorizer_refresh_token的过期时间
LocalDateTime authTime = platformMapper.selectAuthTimeByAppId(authorizerAppId).toLocalDateTime();
Duration duration = Duration.between(authTime, LocalDateTime.now());
redisUtils.set(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId, refreshToken, 60 * 60 * 24 * 30 - (duration.toMillis() / 1000 + 60));
}
param.put("authorizer_refresh_token", refreshToken);
JSONObject tokenData = JSONObject.parseObject(HttpUtil.post(refreshTokenUrl, JSON.toJSONString(param)));
if (tokenData.containsKey("authorizer_access_token")) {
redisUtils.set(CacheKey.AUTH_ACCESS_TOKEN + authorizerAppId, tokenData.getString("authorizer_access_token"), 2 * 60 * 60);
LambdaUpdateWrapper<SpiritOpenPlatform> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(SpiritOpenPlatform::getAppid, authorizerAppId);
SpiritOpenPlatform openPlatform = new SpiritOpenPlatform();
openPlatform.setAuthorizerRefreshToken(refreshToken);
openPlatform.setAuthorizerAccessToken(tokenData.getString("authorizer_access_token"));
platformMapper.update(openPlatform, updateWrapper);
return;
}
throw new BadRequestException("获取authorizer_access_token失败,接口返回信息:" + tokenData.toJSONString());
}

到此,微信第三方平台开发中的关键token,authorizer_access_token已经成功获取

4、调接口发布小程序

提交代码接口:上传小程序代码并生成体验版 | 微信开放文档 (qq.com)

设置用户隐私接口:配置小程序用户隐私保护指引 | 微信开放文档 (qq.com)

提交审核接口:提交审核 | 微信开放文档 (qq.com)

查询审核结果接口:查询最新一次提交的审核状态 | 微信开放文档 (qq.com)

  • 项目中可以采用定时任务的策略调用该接口

最后

总结:微信第三方平台开发的流程,涉及的接口有点多,微信的文档其实也写的很详细了,但是在开发过程中,免不了遇见莫名其妙的问题,这个时候可以在微信开放社区里找找或者搜下博客。