介绍

微信公众号开发文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html

微信公众号管理后台界面:https://mp.weixin.qq.com/

微信公众号测试号管理后台界面:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index

微信开发者工具下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

微信开发者工具使用

微信开发者工具

内网穿透工具

使用 cploar ,官网地址:https://www.cpolar.com/

什么是内网穿透工具:

  • 在微信开发中我们必须将代码部署在公网服务器上才能调试(说白了就是需要微信端能够调用到你的服务器)
  • 内网穿透工具能够将本地端口映射到它提供的一个公网域名上,通过访问这个域名来实现本地方法的调用

公众号测试号配置

  1. URL 和 Token 配置
1
2
3
4
5
6
url:
必须以http://或https://开头,分别支持80端口和443端口
URL规范:比如项目的基础目录为 www.abc.com/wechat/,则URL地址为 www.abc.com/wechat

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

URL和Token配置

  1. 配置 JS接口安全域名
1
2
3
JS接口安全域名:
就是当我们调用微信开发的JS接口时,使用的域名必须与配置的 JS接口安全域名一致
这个域名可以不与上面的URL一致

JS接口安全域名

  1. 配置 授权回调页面域名
1
2
3
4
授权回调页面域名:
当我们在跳转的页面中需要调用微信公众号的接口,就必须配置这个域名地址
这个域名可以不与上面的URL一致
具体使用细节,参考:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

授权回调页面域名

  1. 在当前测试号页面可以看到 appId 和 secret ,配置完后就可以在项目中使用微信公众号测试号了。

appId和secret

公众号配置

  1. 在公众号中配置 URL 和 token,在【基本配置】中配置

基本配置

  1. 配置 JS域名地址 和 授权回调域名地址,在【公众号设置】-【功能设置】中配置

功能设置

  1. 在配置 JS域名地址 和 授权回调域名地址还需要在服务上配置一个txt文件,然后通过 配置的域名 + xxx.txt 能访问到。通常这个可以使用 nginx 转发实现。

域名验证

  1. nginx 配置。将文件放置在 d:/webapps/wx/ 目录下
1
2
3
4
location /MP_verify_n5GSFAcyM6FNnI9L.txt {
alias d:/webapps/wx/;
index MP_verify_n5GSFAcyM6FNnI9L.txt;
}

微信认证接口

在配置 URL 对应的 controller 下需要添加微信认证,认证接口是一个 get 请求且路径只能配置为 /

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
/**
* 微信认证
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@GetMapping("/")
@ResponseBody
public String authWechat(String signature, String timestamp, String nonce, String echostr) {
log.info("微信请求参数:[signature={}, timestamp={}, nonce={}, echostr={}]", signature, timestamp, nonce, echostr);
if (CommonUtil.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new RuntimeException("请求参数非法!");
}

if (!checkSignature(signature, timestamp, nonce)) {
throw new RuntimeException("请求参数非法!");
}

return echostr;
}

/**
* 检查signature
*
* @param timestamp
* @param nonce
* @return
*/
public boolean checkSignature(String signature, String timestamp, String nonce) {
StringBuilder stringBuilder = new StringBuilder();
// 自然排序,字符串拼接
Stream.of(token, timestamp, nonce)
.sorted()
.forEach(stringBuilder::append);

String encryptSHA1 = CommonUtil.encryptSHA1(stringBuilder.toString());

return Objects.equals(encryptSHA1, signature);
}
  • CommonUtil工具类
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
public class CommonUtil {

/**
* 是否存在空白的数据
*
* @param params
* @return
*/
public static boolean isAnyBlank(String... params) {
for (String param : params) {
if (!StringUtils.hasText(param)) {
return true;
}
}
return false;
}

/**
* sha1 加密
*
* @param str
* @return
*/
public static String encryptSHA1(String str) {
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] hashedBytes = sha1.digest(str.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : hashedBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}

微信事件处理接口

在配置 URL 对应的 controller 下需要添加微信事件处理,认证接口是一个 post 请求且路径只能配置为 /

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
/**
* 接收普通消息/事件
*
* @param request
* @return
*/
@PostMapping("/")
@ResponseBody
public String receiveMessage(HttpServletRequest request) {
Map<String, String> map = new HashMap<>();

try (ServletInputStream inputStream = request.getInputStream()) {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
// 获取root
Element root = document.getRootElement();
// 获得所有子节点
List<Element> elements = root.elements();
for (Element element : elements) {
map.put(element.getName(), element.getStringValue());
}
} catch (IOException | DocumentException e) {
throw new RuntimeException(e);
}
System.out.println(map);
// 回复消息
return replyMessage(map);
}

/**
* 回复消息
*
* @param map
* @return
*/
private String replyMessage(Map<String, String> map) {
String msgType = map.get("MsgType");
String message = "";

switch (msgType) {
case "text":
// TODO 消息
break;
case "event":
// TODO 事件
break;
default:
message = commonReply(map, "欢迎你使用公众号!");
break;
}
return message;
}
  • 解析 xml 使用到了 xstream , maven依赖
1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.20</version>
</dependency>
  • xstream拓展,除了解析 xml ,它还提供了java对象转 xml,代码如下:
    • 代转 xml 的对象
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
@Data
@XStreamAlias("xml")
public class TextMessage {
/**
* 接收方账号
*/
@XStreamAlias("ToUserName")
private String toUserName;
/**
* 开发者微信号
*/
@XStreamAlias("FromUserName")
private String fromUserName;
/**
* 消息创建时间 (整型)
*/
@XStreamAlias("CreateTime")
private Long createTime;
/**
* 消息类型
*/
@XStreamAlias("MsgType")
private String msgType;
/**
* 回复的消息内容
*/
@XStreamAlias("Content")
private String content;
}
  • 在实际代码中转 xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String commonReply(Map<String, String> map, String content) {
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
textMessage.setCreateTime(System.currentTimeMillis() / 1000);
textMessage.setContent(content);

// 将java对象转换成xml对象
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);

return xStream.toXML(textMessage);
}

UnionID、OpenID、AppID区别

  • UnionID:在微信公众平台和微信开放平台中,对于同一个微信用户,无论其通过多少个不同的应用(App)或公众号登录,都会对应一个唯一的 UnionID
  • OpenID:每个应用(App)或公众号下,同一个微信用户的 OpenID 是不同的。这意味着即使同一个用户在不同的应用或公众号中登录,每个应用或公众号会为该用户分配一个独特的 OpenID
  • AppID:每个微信应用(包括小程序、公众号等)都有一个唯一的 appID,用于标识该应用

遇到的问题

前后端部署在两个服务器存在跨域问题

当前后端部署在两个服务器中,会存在跨域问题,这个时候可以在 controller 上面配置 @CrossOrigin 注解,从而允许来自该域名的跨域请求

1
2
3
4
@RestController
@RequestMapping("/wechat/")
@CrossOrigin(origins = "http://net.abc.com") // 允许来自该域名的跨域请求
public class WxBaseConfigController { ... }

微信扫描生成的二维码

微信扫描生成的二维码有两种情况:

  • 已经关注了公众号,这时候扫描会进入【微信事件处理接口】,条件为
1
2
3
4
5
// xxx 表示生成二维码携带的内容
if ("subscribe".equals(map.get("Event"))
&& Objects.equals(map.get("EventKey"), "xxx")) {
// TODO 业务处理
}
  • 未关注公众号,这个时候扫描也会进入【微信事件处理接口】,但条件变为
1
2
3
4
5
// 此时 EventKey 的内容等于 qrscene_ + xxx,其中xxx为二维码携带的内容
if ("SCAN".equals(map.get("Event"))
&& Objects.equals(map.get("EventKey"), "qrscene_xxx")) {
// TODO 业务处理
}

微信公众号将当前页面转成图片并下载

前置条件:微信公众号前端使用的是 vue3 写的

我使用的是 vue 的 html2canvas 实现的,部分代码如下:

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
<script setup lang="ts">

import html2canvas from "html2canvas";

const btnClickDownload = () => {

var content = document.getElementById("flexGird");
if (content) {
app.data.showPic = true;
html2canvas(content).then(canvas => {
//添加属性
canvas.setAttribute('id', 'thecanvas');
var base64Image = document.getElementById('base64Img');
const img = canvas.toDataURL('image/png'); // 将canvas转换为base64图片格式
if (base64Image) {
base64Image.setAttribute("src", img);
}
});
}
};

</script>
<template>
<div>
<div class="container" id="flexGird">
<div>需要生成图片的页面</div>
</div>

<div class="btn-box">
<div class="button" @click="btnClickDownload">下载图片</div>
</div>
</div>
</template>

<style scoped>
</style>

IP不在白名单中

在对接微信公众号时调用接口报错:"errmsg": "**invalid ip xxx.xxx.xxx.xxx** ipv6 ::ffff:xxx.xxx.xxx.xxx, not in whitelist rid: 6280cf55-5sccscb42-7cscc91f0"

  1. 第一种情况:配置了错误的 appId 和 secret,这种情况也可能报这种错误
1
解决方案:修正 appId 和 secret 即可
  1. 第二种情况:该问题是由于微信公众需要调用方的IP在白名单中,才可以正常调用
    1. 解决方案:打开微信公众号,在【基础配置】中的 【IP白名单】中的配置报错的IP即可
    2. 注意事项:马上配置完,可能还会报同样的错误,这个时候是因为微信公众号更新没有实时的原因,可以稍微等一下再试试

IP白名单配置