需求
实现
- 步骤:获取component_verify_ticket——>获取component_access_token——>获取authorizer_access_token——>调接口发布小程序
- 1、获取component_verify_ticket
- controller
- 实现类
- 2、获取component_access_token
- 实现类
- 3、获取authorizer_access_token
- 步骤:
- 获取预授权码pre_auth_code——>微信公众平台管理员授权——>获取authorizer_access_token&authorizer_refresh_token
- ①获取预授权码pre_auth_code
- ②微信公众平台管理员授权
- ③获取authorizer_access_token&authorizer_refresh_token
- 4、调接口发布小程序
- 提交代码接口:[上传小程序代码并生成体验版 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/commit.html)
- 设置用户隐私接口:[配置小程序用户隐私保护指引 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/privacy_config/set_privacy_setting.html)
- 提交审核接口:[提交审核 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/submit_audit.html)
- 查询审核结果接口:[查询最新一次提交的审核状态 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_latest_auditstatus.html)
- 最后
需求
根据现有的平台小程序,快速发布一个相同类型的小程序
如果按常规的做法每个小程序都需要经历这样的步骤:在公众平台申请appid——>拉代码到自己的仓库——>打开编译器绑定自己的appId——>上传代码到公众平台——>再提交审核——>在公众平台调整开发设置。
如果用微信开放平台的服务平台开发的步骤:在微信开放平台申请第三方服务平台——>上传小程序模板(只有第一次有)——>调接口上传小程序代码
——>调接口提交审核——>调接口发布小程序。
上传小程序模板的步骤:在公众平台申请appid——>在微信开放平台的第三方服务平台中添加开发小程序——>上传小程序代码(此时会直接上传到了开放平台的草稿箱)——>将草稿箱的代码作为普通模板
这就是服务平台开发平台的便捷,只需上传一次小程序代码,就可以快速的发布小程序。
实现
步骤:获取component_verify_ticket——>获取component_access_token——>获取authorizer_access_token——>调接口发布小程序
1、获取component_verify_ticket
官方文档:验证票据 | 微信开放文档 (qq.com)
因为这个验证票据是微信官方主动推送的,所以需要在第三方平台配置一波
controller
@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各存一份。
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
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
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
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
@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)
实现类
@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
@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标签打开新标签页,管理员进行扫码授权
@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携带到这个接口
@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,下面是实现代码:
@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)
- 项目中可以采用定时任务的策略调用该接口
最后
总结:微信第三方平台开发的流程,涉及的接口有点多,微信的文档其实也写的很详细了,但是在开发过程中,免不了遇见莫名其妙的问题,这个时候可以在微信开放社区里找找或者搜下博客。