微信公众号开发(接收公众号消息)
场景:工作中的一个小机能,想要通过公众号实现微信的openid与公司员数据绑定,方便后续给员工推送一些消息
(公司员工向公众号发送指定数据,包含员工的工号,后台解析出OpenId和员工的工号,写入到数据库)
参考开发文档: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
一、配置公众号
这里使用的自己的公众号,公众号的申请就不再记录
URL 配置自己的域名(这里使用的花生壳)
Token 随意指定即可
EncodingAESKey 随机生成
消息加解密方式 这里并没有必要(后续如果有用到再记录)
二、微信公众号链接认证
三、定义接口和实体类
- 接口
@Api(tags = "微信公众号处理类", description = "微信公众号消息处理类")
@FeignClient(value = "XXXX")
public interface WxMessageApi {
/**
* 公众号设置中设定接收消息的地址时,会发送一个GET请求进行验证
* signature 微信加密签名
* timestamp 时间戳
* nonce 随机数
* echostr 随机字符串
*/
@ApiOperation(value = "微信公众号请求校验")
@RequestMapping(value = "/access", method = RequestMethod.GET)
void wxInterface(@RequestParam("signature") String signature,@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr,HttpServletResponse response) throws IOException;
/**
* 接收公众号消息的接口(同上面的接口一样,不同的是POST请求)
*
* 由于微信发送的消息是xml形式,所以这里就需要解析xml进行处理
*
* produces 指定处理请求的提交内容类型
* consumes 指定返回值类型和返回值编码
*/
@ApiOperation(value = "微信公众号消息处理")
@RequestMapping(value = "/access", method = RequestMethod.POST,
consumes = "text/xml; charset=UTF-8")
String parseMessage(HttpServletRequest request, HttpServletResponse response);
}
- 消息实体基类(包含公共的属性)
这里使用了@XmlElement @XmlTransient @XmlRootElement
等标签结合 consumes 属性
,Controller返回时会将其转换成XML返回
/**
* 消息基类
* @author fyang
* @version V1.0
* @date 2020/9/2 16:34
*/
public class BaseMessage {
/**
* 开发者微信号
*/
private String toUserName;
/**
* 发送方帐号(一个OpenID)
*/
private String fromUserName;
/**
* 消息创建时间 (整型)
*/
private Long createTime;
/**
* 消息类型,文本为text
*/
private String msgType;
/**
*消息id,64位整型
*/
private Long msgId;
@XmlElement(name = "ToUserName")
public String getToUserName() {
return toUserName;
}
public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}
@XmlElement(name = "FromUserName")
public String getFromUserName() {
return fromUserName;
}
public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}
@XmlElement(name = "CreateTime")
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
@XmlElement(name = "MsgType")
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
@XmlElement(name = "MsgId")
public Long getMsgId() {
return msgId;
}
public void setMsgId(Long msgId) {
this.msgId = msgId;
}
}
- 文本消息实体类
其他类型实体类类似的写法
/**
* 普通文本消息
* @author fyang
* @version V1.0
* @date 2020/9/2 16:42
*/
@XmlRootElement(name = "xml")
public class TextMessage extends BaseMessage{
/**
* 文本消息内容
*/
private String content;
@XmlElement(name = "Content")
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
四、验证的实现
- 验证接口接收验证的数据,并返回echostr:
@RequestMapping("/wechat")
@RestController
public class WxMessageApiImpl implements WxMessageApi{
// 这里的token和公众号中配置的一样,我是将其写在了SpringBoot的配置文件中
@Value("${wx.token}")
private String token;
// 用于处理消息的Server
@Autowired
private WxMessageService wxMessageService;
/**
* 微信公众号链接校验
*
* signature 微信加密签名
* timestamp 时间戳
* nonce 随机数
* echostr 随机字符串
*/
@RequestMapping(value = "/access", method = RequestMethod.GET)
public void wxInterface(@RequestParam("signature") String signature,@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr,HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
// 验证
if (StringUtils.isNoneBlank(signature, timestamp, nonce)
&& WeChatUtil.checkSignature(signature, timestamp, nonce, token)){
writer.print(echostr);
}
writer.close();
writer = null;
}
}
小记
这里wxInterface方法一定不能直接返回字符串,直接返回字符串会一直出现token认证失败
下面的写法是错误的
@RequestMapping(value = "/access", method = RequestMethod.GET)
public String wxInterface(@RequestParam("signature") String signature,@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce, @RequestParam("echostr") String echostr,HttpServletResponse response) {
PrintWriter writer = response.getWriter();
// 验证
if (StringUtils.isNoneBlank(signature, timestamp, nonce)
&& WeChatUtil.checkSignature(signature, timestamp, nonce, token)){
return echostr;
}
return null;
}
- 工具类,实现连接数据的验证:
// 1)将token、timestamp、nonce三个参数进行字典序排序
// 2)将三个参数字符串拼接成一个字符串进行sha1加密
// 3)开发者获得加密后的字符串可与signature对比
public class WeChatUtil {
/**
* 验证签名
* @param signature 微信加密签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce, String token) {
String[] arr = new String[] { token, timestamp, nonce };
// 1)将token、timestamp、nonce三个参数进行字典序排序
Arrays.sort(arr);
// 2)将三个参数字符串拼接成一个字符串
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 2)进行 sha1 加密
byte[] digest = md.digest(content.toString().getBytes());
// 转换成字符串
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
/****************************上面是用来做链接验证的************************************/
/****************************下面是用来处理消息的************************************/
/**
* 解析微信发来的请求(XML)
*
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> xml2Map(HttpServletRequest request) {
// 将解析结果存储在HashMap中
Map<String, String> map = Maps.newHashMap();
// 从request中取得输入流
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList){
map.put(e.getName(), e.getText());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(inputStream!=null){
// 释放资源
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
inputStream = null;
}
}
return map;
}
/**
* 将java对象转换成xml方法(空属性会被过滤)
*
* */
public static String beanToXml(Object obj, Class<?> load) {
String xmlStr = null;
try {
JAXBContext context = JAXBContext.newInstance(load);
Marshaller marshaller = context.createMarshaller();
// 去掉生成xml的默认报文头
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
StringWriter writer = new StringWriter();
marshaller.marshal(obj, writer);
xmlStr = writer.toString();
} catch (JAXBException e) {
e.printStackTrace();
}
return xmlStr;
}
}
五、接收消息方法
- 文本消息格式:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>
ToUserName 开发者微信号(也就是我们公众号的id)
FromUserName 发送方帐号(一个OpenID,唯一的标识,指明消息是谁发来的)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 文本消息内容
MsgId 消息id,64位整型
在WxMessageApiImpl中追加接收消息的接口
/**
* 接收公众号消息的接口(同上面的接口一样,不同的是POST请求)
*
* 由于微信发送的消息是xml形式,所以这里就需要解析xml进行处理
*
* consumes 指定处理请求的提交内容类型
* produces 指定返回值类型和返回值编码
*/
@Override
@RequestMapping(value = "/access", method = RequestMethod.POST,
consumes = "text/xml; charset=UTF-8")
public String parseMessage(HttpServletRequest request, HttpServletResponse response) {
//处理消息,获取返回数据
TextMessage textMessage = wxMessageService.wxMessageHandleCoreService(WeChatUtil.xml2Map(request));
// 将数据转换成xml格式返回
return textMessage == null ? null : WeChatUtil.beanToXml(textMessage, TextMessage.class);
}
六、处理消息数据
/**
* 解析公众号的消息,将员工的openid插入到数据库
* @param msgMap 微信文本消息数据
* @return
*/
@Override
public TextMessage wxMessageHandleCoreService(Map<String,String> msgMap) {
TextMessage result = new TextMessage();
Optional.ofNullable(msgMap)
// 判断数据有否存在
//.filter(map -> StringUtils.isNoneBlank(map.get("FromUserName"), map.get("ToUserName") , map.get("MsgType"), map.get("Content")))
// 判断消息是text文本消息
// 这里是一个枚举类,其实就是 text 字符串
.filter(map -> WxMessageTypeEnum.REQ_MESSAGE_TYPE_TEXT.getValue().equals(map.get("MsgType")))
// 判断文本消息是否是指定的格式 XXXX# + 员工工号
.filter(map -> Pattern.compile("^XXXX#.+$").matcher(map.get("Content")).matches())
// 截取工号
.map(map -> map.get("Content").substring(5))
.ifPresent(jobNo -> {
// 根据员工工号查询员工信息
UserDTO emp = userApi.getUserByJobId(jobNo, Long.parseLong("1"));
if (Optional.ofNullable(emp).isPresent() && Optional.ofNullable(emp.getId()).isPresent()) {
// 将openid写入到数据库
Boolean updateResult = userApi.updateOpenId(emp.getId().longValue()
, emp.getCompanyId()
, msgMap.get("FromUserName"));
result.setContent((updateResult!=null && updateResult) ? "绑定成功":"绑定失败,检查员工编号是否正确");
}else{
result.setContent("绑定失败,检查员工编号是否正确");
}
// 回传消息,所以讲fromuser和toUser交换
result.setFromUserName(msgMap.get("ToUserName"));
result.setToUserName(msgMap.get("FromUserName"));
// text
result.setMsgType(WxMessageTypeEnum.REQ_MESSAGE_TYPE_TEXT.getValue());
result.setCreateTime(new Date().getTime());
});
return StringUtils.isBlank(result.getContent())?null:result;
}
小白一枚,可能代码逻辑/风格不够完善,有不足之处请大神多多指出,感激不尽,谢谢!!!
- 踩到的坑
- 返回给微信的数据如果缺少东西会出现异常
(例如最早没有返回消息的时候,我的返回内容是空的) - 这里的bean转成xml 会将null 数据过滤掉(注意)
- 验证的方法一定要将返回值写入到response中,不能直接返回