微信公众号开发准备

** 通讯机制 **

20200423163057

** 接入步骤 **
1、填写服务器配置

2、验证服务器地址的有效性

3、依据接口文档实现业务逻辑

第1步中服务器配置包含服务器地址(URL)、令牌(Token) 和 消息加解密密钥(EncodingAESKey)。 ​ 可在开发–>基本配置–>服务器配置中配置,测试号没有EncodingAESKey

​ 服务器地址即公众号后台提供业务逻辑的入口地址,目前只支持80端口,之后包括接入验证以及任何其它的操作的请求(例如消息的发送、菜单管理、素材管理等)都要从这个地址进入。接入验证和其它请求的区别就是,接入验证时是get请求,其它时候是post请求;

Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性);

EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。本例中全部以未加密的明文消息方式,不涉及此配置项。

20200423163107

第2步 开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

1
2
3
4
5
参数	描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串\

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1)将token、timestamp、nonce三个参数进行字典序排序

2)将三个参数字符串拼接成一个字符串进行sha1加密

3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

Spring Boot实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebServlet("weixin.do")
public class WeixinServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String signature = req.getParameter("signature");
String timestamp = req.getParameter("timestamp");
String nonce = req.getParameter("nonce");
String echostr = req.getParameter("echostr");

PrintWriter out = resp.getWriter();
if (CheckUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr);
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
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
package com.hu.utils;

import java.security.MessageDigest;
import java.util.Arrays;

/**
* @Description TODO
* @Author huyunshun 2019/9/11
*/
public class CheckUtil {
/*
* 自定义token, 用作生成签名,从而验证安全性
* */
private static final String token = "demodemo";//token是在验证的时候进行配置的

public static boolean checkSignature(String signature,String timestamp,String nonce){

/**
* 接收微信服务器发送请求时传递过来的参数
*/
String[] arr = new String[]{token,timestamp,nonce};
/**
* 将token、timestamp、nonce三个参数进行字典序排序
* 并拼接为一个字符串
*/
Arrays.sort(arr);
//生成字符串
StringBuffer content = new StringBuffer();
for(int i=0;i<arr.length;i++){
content.append(arr[i]);
}
//sha1加密
String temp = getSha1(content.toString());
//校验微信服务器传递过来的签名 和 加密后的字符串是否一致, 若一致则签名通过
return temp.equals(signature);
}
public static String getSha1(String str){
if (null == str || 0 == str.length()){
return null;
}
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));

byte[] md = mdTemp.digest();
int j = md.length;
char[] buf = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
}

启动后保证在本地访问没问题,之后使用内网映射工具(如Ngrok)到公网。然后,在测试号中配置:

20200423163124

配置成功后说明,公众号应用已经能够和微信服务器正常通信,公众号已经接入到微信公众平台了。

** access_token **

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

接口调用上限每天2000次,所以不能调用太频繁。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。

调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。

接口调用请求说明

https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

1
2
3
4
参数	是否必须	说明
grant_type 是 获取access_token填写client_credential
appid 是 第三方用户唯一凭证
secret 是 第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

1
{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数说明

1
2
3
4
参数	说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
1
{"errcode":40013,"errmsg":"invalid appid"}

返回码说明

1
2
3
4
5
6
返回码	说明
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
40002 请确保grant_type字段值为client_credential
40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。(小程序及小游戏调用不要求IP地址在白名单内。)

代码实现获取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
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
@Data
public class AccessToken {
private String tokenName; //获取到的凭证
private int expireSecond; //凭证有效时间 单位:秒

}

public class AccessTokenInfo {
public static AccessToken accessToken = null;
}

RestController
@RequestMapping("/weixin")

@Slf4j
public class AccessTokenController {

@Autowired
private RestTemplate restTemplate;

@RequestMapping("getweixintoken")
public void doWeixin() throws Exception{

System.out.println("-----启动AccessTokenController-----");

final String appId = "wxe1fe5190eeb82893";//request.getParameter("appId");
final String appSecret = "22150a2fd88b02b9ae23dad993dd6f7c";//request.getParameter("appSecret");

new Thread(()->{
while (true) {
try {
//获取accessToken
AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
//获取成功
if (AccessTokenInfo.accessToken != null) {
//获取到access_token 休眠7000秒,大约2个小时左右
Thread.sleep(7000 * 1000);
} else {
//获取失败
Thread.sleep(1000 * 3); //获取的access_token为空 休眠3秒
}
} catch (Exception e) {
log.error("发生异常:" + e.getMessage());
e.printStackTrace();
try {
Thread.sleep(1000 * 10); //发生异常休眠1秒
} catch (Exception e1) {
e.printStackTrace();
}
}
}
}).start();
}

private AccessToken getAccessToken(String appId, String appSecret) {
/**
* 接口地址为https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
*
* grant_type固定写为client_credential即可。
*/
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
//此请求为https的get请求,返回的数据格式为{"access_token":"ACCESS_TOKEN","expires_in":7200}
JSONObject jsonObject = restTemplate.getForObject(url,JSONObject.class);
log.info("获取到的access_token="+jsonObject.getString("access_token"));

AccessToken token = new AccessToken();
token.setTokenName(jsonObject.getString("access_token"));
token.setExpireSecond(jsonObject.getInteger("expires_in"));
return token;
}
}

启动项目,浏览器访问,控制台:

1
2
----启动AccessTokenController-----
2019-09-12 10:25:59.899 INFO 12136 --- [ Thread-10] com.hu.config.AccessTokenController : 获取到的access_token=25_yKsFws5iSmFPTQiHBv9r97cDzEXsjY37skMbzdFzSh73q3mkzaSdCP4WUSICQ1DYMPOcxhk8mk4Ynow3koQ-1C-A53jzuY8VsNfNnNccomvBfDZryZzd5RmyR2OSWQph3uroE258k-n9WWPNHJJgAFARTT

OK!