目录
1. 概念说明:
2. 开发准备:
3. 测试demo(更改配置信息即可使用)
3.1. 服务器配置
3.1.1.配置填写说明
3.1.2.校验服务器有效性:
3.1.3.URL后端接口代码和校验代码(servlet)
3.1.4.配置内网穿透,完成本地调试
3.1.5. 可能存在的问题
3.2 模板消息
3.2.1. 搞定 template_id 即模板消息id:
3.2.2. 搞定 touser 即openid
3.2.3. 从获取openid的请求中我们发现需要access_token:
3.2.4. 发送模板消息的url参数
3.2.5. topcolor
3.2.5. data
3.3. 源码
3.3.1 模板消息DTO
3.3.2. 模板消息内容DTO
3.3.3. access_token缓存类:
3.3.4.http请求工具类:
3.3.5. 最终的Servlet(controller自行转换)(为方便观看所有逻辑都写在这里了,自行优化):
3.4.测试
官方文档:
微信公众平台开发概述 | 微信开放文档
全局返回码文档 :微信开放文档
access_token:是公众号的全局唯一接口调用凭据,公众号调用各接口的必要参数(2小时内有效,过期需要重新获取,但1天内获取次数有限,需自行存储)
OpenID :为了识别用户每个公众号针对,每个用户会产生一个OpenID(用户id:对用户的操作需要用到)
UnionID: 同一开放平台账号下不同公众号或应用下用户的共同id(这里不需要用到)
消息会话(这里用到模板消息)
公众号是以微信用户的一个联系人形式存在的,消息会话是公众号与用户交互的基础。
公众号内主要有这样几类消息服务的类型,分别用于不同的场景:
群发消息:订阅号为每天1次,服务号为每月4次
被动回复消息:在用户给公众号发消息后,公众号可以回复一个消息
客服消息:用户在公众号内发消息/触发特定行为后,公众号可以给用户发消息
模板消息:在需要对用户发送服务通知(如刷卡提醒、服务预约成功通知等)时,公众号可以用特定内容模板,主动向用户发送消息。
https://kf.qq.com/faq/120911VrYVrA150918fMZ77R.html?scene_id=kf3386
个人研究测试:通过手机微信扫描二维码获得测试号
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
这里坑比较多,服务器配置不是随便填一个url就完事了,需要后端接口配合校验。
URL:服务器地址--是开发者用来接收微信消息和事件的接口URL (在提交配置修改时微信会向该URL接口发送请求验证服务器地址的有效性)
Token:任意填写,用作生成签名(微信向上述URL接口发送的请求是携带token的,需要在接口中校验token一致以确保安全性)这个token与上述的access_token不是一回事。这个token只用于验证开发者服务器。
EncodingAESKey: 由开发者手动填写或随机生成,将用作消息体加解密密钥
消息加解密方式 :明文模式、兼容模式和安全模式
这是重点:点击完提交修改后,微信会向url发起一个请求并将token携带过去,这个请求要能正确被你的后端服务器所响应并返回正确的结果,服务器配置才算修改成功
校验请求说明:
signature:微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp:时间戳
nonce :随机数
echostr:随机字符串
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。
@WebServlet(urlPatterns = {
"/wx"})
public class WxServlet extends HttpServlet {
// 服务器配置填写的token
private static final String wxToken = "888888";
@Override
protected void doGET(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGetWx(request, response);
}
/**
* @Description 校验配置URL服务器的合法性
* @date 2023年5月29日下午4:17:40
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void doGetWx(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
// 将微信echostr返回给微信服务器
try (OutputStream os = response.getOutputStream()) {
String sha1 = getSHA1(wxToken, timestamp, nonce, "");
// 和signature进行对比
if (sha1.equals(signature)) {
// 返回echostr给微信
os.write(URLEncoder.encode(echostr, "UTF-8").getBytes());
os.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 用SHA1算法生成安全签名
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
* @throws Exception
*/
public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws Exception {
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();
}
return "";
}
}
项目路径是/xjsrm,因此服务器url地址就是:http://localhost:8080/xjsrm/wx
直接将http://localhost:8080/xjsrm/wx地址填到服务配置的url可以吗?答案是不可以!
3.1.4.1. 内网穿透的必要性:
3.1.4.2. 用到的工具cpolar:
3.1.4.3. cpolar配置内网穿透的教程
3.1.4.4. 获取本地项目的公网路径
配置完cpolar后在在线隧道列表中找到本地项目的地址,我项目是localhost:8080,因此公网地址对应(最好用https协议)https://22717eef.r6.vip.cpolar.cn 、
因此完整的服务器Url就是: https://22717eef.r6.vip.cpolar.cn/xjsrm/wx
浏览器访问该url,看后端是否接受到了请求,如果接收到说明接口没问题,此时将URL填到对应的配置栏中点击提交即可。
如果你在使用点击修改配置的提交发现微信发送的请求根本就没有进到后端的项目中
此时查看sandboxinfo这个包。如果出现 errorcode=-1多半是内网穿透工具的问题。
出现其他错误代码可以去错误大全中根据信息排查
模板消息的官方文档:微信公众平台 (qq.com)
从官方给出的请求示例入手,看需要准备的接口和数据:
POST请求
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
请求包为一个json:
{
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"touser":"OPENID",
"url":"http://weixin.qq.com/download",
"topcolor":"#FF0000",
"data":{
"User": {
"value":"黄先生",
"color":"#173177"
},
"Date":{
"value":"06月07日 19时24分",
"color":"#173177"
},
"CardNumber": {
"value":"0426",
"color":"#173177"
},
"Type":{
"value":"消费",
"color":"#173177"
},
"Money":{
"value":"人民币260.00元",
"color":"#173177"
},
"DeadTime":{
"value":"06月07日19时24分",
"color":"#173177"
},
"Left":{
"value":"6504.09",
"color":"#173177"
}
}
}
新增模板消息(以测试号为例)
模板内容可设置参数(模板标题不可),供接口调用时使用,参数需以{{开头,以.DATA}}结尾(具体传参看后续代码)
查看用户管理的官方文档;微信开放文档 (qq.com),通过官方接口获取
因为我们是公众号,所以选用获取用户列表的接口:微信开放文档 (qq.com)
http请求方式: GET(请使用https协议)
https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
参数 是否必须 说明
access_token 是 调用接口凭证
next_openid 是 第一个拉取的OPENID,不填默认从头开始拉取
返回说明
正确时返回JSON数据包:
{
"total":2,
"count":2,
"data":{
"openid":["OPENID1","OPENID2"]},
"next_openid":"NEXT_OPENID"
}
获取access_token官方文档:微信开放文档 (qq.com)
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
参数说明
参数 是否必须 说明
grant_type 是 获取access_token填写client_credential
appid 是 第三方用户唯一凭证
secret 是 第三方用户唯一凭证密钥,即appsecret
返回说明
正常情况下,微信会返回下述JSON数据包给公众号:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
这个是发送消息后用户点击卡片跳转的地址(自定义)可以不填
消息卡片的顶部颜色(自定义)
消息的内容(了解结构即可,后续代码中会体现)
拷贝完再理解
import java.util.Map;
/**
* @Description 微信公众号模板消息请求对象
* @author isymi
* @version
* @date 2023年5月29日下午4:28:09
*
*/
public class TemplateMessage {
/**
* 发送消息用户的openid
*/
private String touser;
/*
* 模板消息id
*/
private String template_id;
/**
* 点击模板信息跳转地址;置空:则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android)
*/
private String url;
/**
* 卡片顶部颜色
*/
private String topcolor;
/**
* key为模板中参数内容"xx.DATA"的xx,value为参数对应具体的值和颜色
*/
private Map data;
// private String data;
public TemplateMessage() {
}
public TemplateMessage(String touser, String template_id, String url, String topcolor, Map data) {
this.touser = touser;
this.template_id = template_id;
this.url = url;
this.topcolor = topcolor;
this.data = data;
}
public String getTouser() {
return touser;
}
public void setTouser(String touser) {
this.touser = touser;
}
public String gettemplate_id() {
return template_id;
}
public void settemplate_id(String template_id) {
this.template_id = template_id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTopcolor() {
return topcolor;
}
public void setTopcolor(String topcolor) {
this.topcolor = topcolor;
}
public Map getData() {
return data;
}
public void setData(Map data) {
this.data = data;
}
@Override
public String toString() {
return "TemplateMessage [touser=" + touser + ", template_id=" + template_id + ", url=" + url + ", topcolor="
+ topcolor + ", data=" + data + "]";
}
}
import java.io.Serializable;
/**
* @Description 模板消息内容类
* @author isymi
* @version
* @date 2023年5月29日下午4:33:27
*
*/
public class WeChatTemplateMsg implements Serializable{
/**
* 消息实参
*/
private String value;
/**
* 消息颜色
*/
private String color;
public WeChatTemplateMsg(String value) {
this.value = value;
this.color = "#173177";
}
public WeChatTemplateMsg(String value, String color) {
this.value = value;
this.color = color;
}
@Override
public String toString() {
return "WeChatTemplateMsg [value=" + value + ", color=" + color + "]";
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
/**
* @Description access_token缓存类
* @author
* @version
* @date 2023年5月30日上午10:40:08
*
*/
public class AccessToken {
private String accessToken;
//过期时间 当前系统时间+微信传来的过期时间
private Long expiresTime;
public AccessToken(String accessToken, String expiresIn) {
this.accessToken = accessToken;
this.expiresTime = System.currentTimeMillis()+Integer.parseInt(expiresIn)*1000;
}
/**
* 判断token是否过期
* @return
*/
public boolean isExpired(){
return System.currentTimeMillis()>expiresTime;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public Long getExpiresTime() {
return expiresTime;
}
public void setExpiresTime(Long expiresTime) {
this.expiresTime = expiresTime;
}
public AccessToken(String accessToken, Long expiresTime) {
this.accessToken = accessToken;
this.expiresTime = expiresTime;
}
public AccessToken() {
}
}
import java.io.BufferedReader;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xx.xx.pojo.TemplateMessage;
/**
* @Description 微信公众号http请求工具类
* @author isymi
* @version
* @date 2023年5月29日下午4:07:39
*
*/
public class WXPublicAccountHttpUtil {
/**
* @Description 根据请求获取返回结果字符串(根据请求获取accessToken)
* @date 2023年5月29日下午4:04:21
* @param url
* @return
* @throws IOException
*/
public static String get(String url) throws IOException {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL requestUrl = new URL(url);
connection = (HttpURLConnection) requestUrl.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} else {
// Handle error response
System.out.println("HTTP GET request failed with response code: " + responseCode);
return null;
}
} finally {
if (reader != null) {
reader.close();
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* @Description 根据URl获取JSONObject:根据请求获取关注用户列表数据
* @date 2023年5月29日下午4:02:16
* @param url
* @return
* @throws IOException
*/
public static JSONObject getJsonObject(String url) throws IOException {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL urlObj = new URL(url);
connection = (HttpURLConnection) urlObj.openConnection();
connection.setRequestMethod("GET");
StringBuilder response = new StringBuilder();
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
/*
* 正确返回的格式
* {
"total":2,
"count":2,
"data":{
"openid":["OPENID1","OPENID2"]},
"next_openid":"NEXT_OPENID"
}
*/
return JSON.parseObject(response.toString());
} finally {
if (reader != null) {
reader.close();
}
if (connection != null) {
connection.disconnect();
}
}
}
/**
* @Description 获取关注用户的 openid 集合
* @date 2023年5月29日下午4:04:02
* @param url
* @return
* @throws IOException
*/
public static List getOpenidList(String url) throws IOException {
// 获取关注用户列表数据
JSONObject jsonObject = getJsonObject(url);
System.out.println(jsonObject);
// 错误情况
if (jsonObject.containsKey("errcode")) {
int errcode = jsonObject.getIntValue("errcode");
String errmsg = jsonObject.getString("errmsg");
throw new RuntimeException("Failed to get openid list. errcode: " + errcode + ", errmsg: " + errmsg);
}
int total = jsonObject.getIntValue("total");
// 无用户关注 {"total":0,"count":0,"next_openid":""}
if (total == 0) {
throw new RuntimeException("No openid found. Total is 0.");
}
// 有用户关注:
/**
* {"total":1,
* "data":{
* "openid":["o-tgG5-VaQfsgdjerHA-z2PeZFls"]},
* "count":1,
* "next_openid":"o-tgG5-VaQfsgdjerHA-z2PeZFls"}
*/
JSONObject dataObject = jsonObject.getJSONObject("data");
int count = dataObject.getIntValue("count");
System.out.println("关注总人数:"+count);
JSONArray openidArray = dataObject.getJSONArray("openid");
// 将 openid 数组封装为 List 集合
List openidList = new ArrayList<>();
for (int i = 0; i < openidArray.size(); i++) {
String openid = openidArray.getString(i);
openidList.add(openid);
}
return openidList;
}
/**
* @Description 发送消息
* @date 2023年5月29日下午4:58:02
* @param accessToken
* @param templateMessage
* @return
* @throws IOException
*/
public static String sendMessage( String accessToken, TemplateMessage templateMessage) throws IOException {
String requestUrl ="https://api.weixin.qq.com/cgi-bin/message/template/send" + "?access_token=" + accessToken;
URL urlObject = new URL(requestUrl);
HttpURLConnection connection = (HttpURLConnection) urlObject.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
String requestBody = JSON.toJSONString(templateMessage);
byte[] requestBodyBytes = requestBody.getBytes(StandardCharsets.UTF_8);
connection.setRequestProperty("Content-Length", String.valueOf(requestBodyBytes.length));
OutputStream outputStream = connection.getOutputStream();
outputStream.write(requestBodyBytes);
outputStream.close();
int responseCode = connection.getResponseCode();
BufferedReader reader;
if (responseCode >= 200 && responseCode <= 299) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
}
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
connection.disconnect();
System.out.println("Response Code: " + responseCode);
System.out.println("Response Body: " + response.toString());
return response.toString();
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xx.srm.pojo.AccessToken;
import com.xx.xx.pojo.TemplateMessage;
import com.xx.xx.pojo.WeChatTemplateMsg;
import com.xx.xx.utils.WXPublicAccountHttpUtil;
@WebServlet(urlPatterns = {
"/wx",
"/wx/message" })
public class WxServlet extends HttpServlet {
// 必须替换
private static final String wxToken = "xxxxxm8";
// 必须替换
public static final String APPID = "wx3xxxxxx1795fa";
// 必须替换
public static final String SECRET = "57b96fxxxxxxxxeab62bfe3";
// 必须替换
public static final String MESSAGE_TEMPLATE_ID = "N6MyyAF0Ucxxxxxxxxxxxxxxxxp-OGsWnQut_niUAaY";
/**
* 全局AccessToken
*/
private static AccessToken at;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if ("/wx".equals(request.getServletPath())) {
doGetWx(request, response);
} else if ("/wx/message".equals(request.getServletPath())) {
doSendMessage(request, response);
}
}
/**
* @Description 校验配置URL服务器的合法性
* @date 2023年5月29日下午4:17:40
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void doGetWx(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
// 将微信echostr返回给微信服务器
try (OutputStream os = response.getOutputStream()) {
String sha1 = getSHA1(wxToken, timestamp, nonce, "");
// 和signature进行对比
if (sha1.equals(signature)) {
// 返回echostr给微信
os.write(URLEncoder.encode(echostr, "UTF-8").getBytes());
os.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @Description 发送模板消息
* @date 2023年5月30日上午10:57:45
* @param request
* @param response
* @throws IOException
*/
private void doSendMessage(HttpServletRequest request, HttpServletResponse response) throws IOException {
String token = getToken();
String url = "https://api.weixin.qq.com/cgi-bin/user/get?" + "access_token=" + token;
// 获取 openid 数组
List userOpenids = WXPublicAccountHttpUtil.getOpenidList(url);
// 主要的业务逻辑:
for (String openId : userOpenids) {
TemplateMessage templateMessage = new TemplateMessage();
templateMessage.setTouser(openId);
templateMessage.settemplate_id(MESSAGE_TEMPLATE_ID);
templateMessage.setTopcolor("#FF0000");
// key对应创建模板内容中的形参
//{{title.DATA}} {{username.DATA}} {{quote.DATA}} {{date.DATA}}
// WeChatTemplateMsg对应实参和字体颜色
Map data = new HashMap();
data.put("title", new WeChatTemplateMsg("你有一条新的消息", "#173177"));
data.put("username", new WeChatTemplateMsg("黄先生", "#173177"));
data.put("date", new WeChatTemplateMsg("2023年05月29日 16时24分", "#173177"));
data.put("quote", new WeChatTemplateMsg("你好", "#173177"));
templateMessage.setData(data);
System.out.println(templateMessage);
WXPublicAccountHttpUtil.sendMessage(getToken(), templateMessage);
}
}
/**
* @Description 获取token,本地缓存有就直接返回,没有就发送请求获取(wx官方api获取token每天有限制,因此需做缓存)
* @date 2023年5月29日下午4:13:17
* @return
*/
public static String getToken() {
if (at == null || at.isExpired()) {
getAccessToken();
}
return at.getAccessToken();
}
/**
* 给AccessToken赋值
*/
private static void getAccessToken() {
// 发送请求获取token
String token = null;
try {
String url ="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"+ "&appid=" + APPID + "&secret=" + SECRET;
token = WXPublicAccountHttpUtil.get(url);
} catch (Exception e) {
e.printStackTrace();
}
JSONObject jsonObject = JSONObject.parseObject(token);
String accessToken = (String) jsonObject.get("access_token");
Integer expiresIn = (Integer) jsonObject.get("expires_in");
// 创建token对象,并存储
at = new AccessToken(accessToken, String.valueOf(expiresIn));
System.out.println(token);
}
/**
* 用SHA1算法生成安全签名
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
* @throws Exception
*/
public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws Exception {
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();
}
return "";
}
}
关注公众号
浏览器访问 http://22717eef.r6.vip.cpolar.cn/xjsrm/wx/message(替换为自己的)
查看微信消息