微信公众号开发(一)
上一篇文章大致解读了官方文档给出的开发概述,本文正式开始开发步骤的记录。
1. 为了配合微信请求只能使用域名的要求,可以使用natapp搭建外网服务器,模拟域名访问,详细的步骤可参考文章:搭建外网传送门。主要就是配置一个免费隧道,并下载对应的natapp插件,按照免费隧道中的authtoken,配置config.ini文件放在natapp根目录下,双击启动即可。
启动natapp见下列这样即说明配置成功,可通过域名访问
域名设置成功就可以进行公众号开发了.
step1 引包
<!--微信封装类-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.2.0</version>
</dependency>
<!--用于进行配置文件的注入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
step2 微信相关配置信息的设置
server: port: 8803 # 测试公众号 hwy.wx.mp: configs[0]: appId: 你的公众号appid secret: 你的公众号appsecret token: 自定义设置一个token,会在公众号配置中使用,要保持一致 aesKey: 公众号中的 template1: alarm: 告警推送信息的模板id
step3 代码开发
① 微信公众号相关配置信息注入到WxMpProperties类中,支持多公众号的注入
package com.iris.wechat.config; import lombok.Data; import lombok.ToString; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.List; /** * 注入微信公众号的配置信息 */ @Data @ConfigurationProperties(prefix = "hwy.wx.mp") public class WxMpProperties { private List<MpConfig> configs; @Data @ToString public static class MpConfig { /** * 设置微信公众号的appid */ private String appId; /** * 设置微信公众号的app secret */ private String secret; /** * 设置微信公众号的token */ private String token; /** * 设置微信公众号的EncodingAESKey */ private String aesKey; } }
② 注入配置文件对象,公众号常见事件的路由层WxMpConfiguration
package com.iris.wechat.config; import com.google.common.collect.Maps; import com.iris.wechat.handler.*; import me.chanjar.weixin.common.api.WxConsts.EventType; import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; import me.chanjar.weixin.common.api.WxConsts.XmlMsgType; import me.chanjar.weixin.mp.api.WxMpInMemoryConfigStorage; import me.chanjar.weixin.mp.api.WxMpMessageRouter; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; import me.chanjar.weixin.mp.constant.WxMpEventConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.util.Map; import java.util.stream.Collectors; /** * wechat mp configuration * * @author Binary Wang(https://github.com/binarywang) */ @Configuration @EnableConfigurationProperties(WxMpProperties.class) public class WxMpConfiguration { private LogHandler logHandler; private NullHandler nullHandler; private KfSessionHandler kfSessionHandler; private StoreCheckNotifyHandler storeCheckNotifyHandler; private LocationHandler locationHandler; private MenuHandler menuHandler; private MsgHandler msgHandler; private UnsubscribeHandler unsubscribeHandler; private SubscribeHandler subscribeHandler; private WxMpProperties properties; private static Map<String, WxMpMessageRouter> routers = Maps.newHashMap(); private static Map<String, WxMpService> mpServices = Maps.newHashMap(); @Autowired public WxMpConfiguration(LogHandler logHandler, NullHandler nullHandler, KfSessionHandler kfSessionHandler, StoreCheckNotifyHandler storeCheckNotifyHandler, LocationHandler locationHandler, MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler, SubscribeHandler subscribeHandler, WxMpProperties properties) { this.logHandler = logHandler; this.nullHandler = nullHandler; this.kfSessionHandler = kfSessionHandler; this.storeCheckNotifyHandler = storeCheckNotifyHandler; this.locationHandler = locationHandler; this.menuHandler = menuHandler; this.msgHandler = msgHandler; this.unsubscribeHandler = unsubscribeHandler; this.subscribeHandler = subscribeHandler; this.properties = properties; } public static Map<String, WxMpMessageRouter> getRouters() { return routers; } @PostConstruct public void init() { services(); } public static Map<String, WxMpService> getMpServices() { return mpServices; } public Object services() { mpServices = this.properties.getConfigs() .stream() .map(a -> { WxMpInMemoryConfigStorage configStorage = new WxMpInMemoryConfigStorage(); configStorage.setAppId(a.getAppId()); configStorage.setSecret(a.getSecret()); configStorage.setToken(a.getToken()); configStorage.setAesKey(a.getAesKey()); WxMpService service = new WxMpServiceImpl(); service.setWxMpConfigStorage(configStorage); routers.put(a.getAppId(), this.newRouter(service)); return service; }).collect(Collectors.toMap(s -> s.getWxMpConfigStorage().getAppId(), a -> a)); return Boolean.TRUE; } private WxMpMessageRouter newRouter(WxMpService wxMpService) { final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService); // 记录所有事件的日志 (异步执行) newRouter.rule().handler(this.logHandler).next(); // 接收客服会话管理事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_CREATE_SESSION) .handler(this.kfSessionHandler).end(); newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_CLOSE_SESSION) .handler(this.kfSessionHandler) .end(); newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(WxMpEventConstants.CustomerService.KF_SWITCH_SESSION) .handler(this.kfSessionHandler).end(); // 门店审核事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(WxMpEventConstants.POI_CHECK_NOTIFY) .handler(this.storeCheckNotifyHandler).end(); // 自定义菜单事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(MenuButtonType.CLICK).handler(this.menuHandler).end(); // 点击菜单连接事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(MenuButtonType.VIEW).handler(this.nullHandler).end(); // 关注事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(EventType.SUBSCRIBE).handler(this.subscribeHandler) .end(); // 取消关注事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(EventType.UNSUBSCRIBE) .handler(this.unsubscribeHandler).end(); // 上报地理位置事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(EventType.LOCATION).handler(this.locationHandler) .end(); // 接收地理位置消息 newRouter.rule().async(false).msgType(XmlMsgType.LOCATION) .handler(this.locationHandler).end(); // 扫码事件 newRouter.rule().async(false).msgType(XmlMsgType.EVENT) .event(EventType.SCAN).handler(this.nullHandler).end(); // 默认 newRouter.rule().async(false).handler(this.msgHandler).end(); return newRouter; } }
③ 根据WxMpConfiguration类中的handler完成各个handler的开发,根据自己的业务场景作相应的开发,
④ 完成验证接口的开发,分两个接口,接口的路径一致只是方法不同,一个get方法:用于用户界面上域名接口验证,一个post方法:用于关注,取关,自定义菜单等事件的触发。
package com.iris.wechat.controller; import com.iris.wechat.config.WxMpConfiguration; import com.iris.wechat.log.XlyLogger; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; /** * 与微信交互的API文件 * @Date 2019-11-22 16:41:00 * @author muruan.lt * 配合华为云公众号绑定 */ @RestController @RequestMapping("/wx/{appid}") public class WechatController { private static Logger log = XlyLogger.get(); // 接口配置信息调用接口(GET) @GetMapping(produces = "text/plain;charset=utf-8") public String doGet(@PathVariable String appid, HttpServletRequest request) { // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr); if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) { log.info("signature " + signature + " timestamp " + timestamp + " nonce " + nonce + " echostr " + echostr); throw new IllegalArgumentException("请求参数非法,请核实!"); } final WxMpService wxService = WxMpConfiguration.getMpServices().get(appid); if (wxService == null) { throw new IllegalArgumentException(String.format("未找到对应appid=[%d]的配置,请核实!", appid)); } // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 if (wxService.checkSignature(timestamp, nonce, signature)) { log.info("weixin get success...."+echostr); return echostr; }else { log.error("weixin get failed...."); return "weixin get failed...."; } } // 关注,取关,客服,菜单等调用接口(POST) @PostMapping(produces = "application/xml; charset=UTF-8") public String doPost(HttpServletRequest request, @PathVariable String appid, @RequestBody String requestBody) { log.debug("weixin login get..."); // 获取微信公众号传输过来的code,通过code可获取access_token,进而获取用户信息 String code = request.getParameter("code"); // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // openid String openid = request.getParameter("openid"); // encType--可空 String encType = request.getParameter("encType"); // msgSignature--可空 String msgSignature = request.getParameter("msgSignature"); final WxMpService wxService = WxMpConfiguration.getMpServices().get(appid); if (!wxService.checkSignature(timestamp, nonce, signature)) { log.info( "\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}]," + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ", openid, signature, encType, msgSignature, timestamp, nonce, requestBody); throw new IllegalArgumentException("非法请求,可能属于伪造的请求!"); } String out = null; if (StringUtils.isBlank(encType)) { // 明文传输的消息 WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody); WxMpXmlOutMessage outMessage = this.route(inMessage, appid); if (outMessage == null) { return ""; } out = outMessage.toXml(); } else if ("aes".equalsIgnoreCase(encType)) { // aes加密的消息 WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(), timestamp, nonce, msgSignature); log.debug("\n消息解密后内容为:\n{} ", inMessage.toString()); WxMpXmlOutMessage outMessage = this.route(inMessage, appid); if (outMessage == null) { return ""; } out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage()); } log.debug("\n组装回复信息:{}", out); return out; } private WxMpXmlOutMessage route(WxMpXmlMessage message, String appid) { try { return WxMpConfiguration.getRouters().get(appid).route(message); } catch (Exception e) { log.error("路由消息时出现异常!", e); } return null; } }
其中doGet方法用于接口配置信息的调用接口,doPost方法用于关注,取关等事件的触发。
开发阶段若服务器,域名已经准备好,则可将代码打包上线,进行下一步开发调试,由于上线的代码并不是很方便调试故而可使用上文中natapp产生的域名代替,进行本地调试,详细配置如下,
① 接口配置信息修改:URL是验证接口,Token是自己定义的,务必与服务器配置文件中的token一致
登陆公众号测试账号,找到如下位置,
将上图中appID,appsecret信息添加到配置文件中,并启动服务。
点击“修改“,将"接口配置信息"接口填在URL处,将上图中的token填在下图的Token处。其中URL的格式为natapp生成的外网访问地址+/wx/{appid},如下图所示
点击提交,在本地如下方法中打断点,发现点击提交会进入下面方法,若没有进入则说明配置URL,token,appId,secret等出现错误,若进入该方法则说明配置没有问题。
// 接口配置信息调用接口(GET) @GetMapping(produces = "text/plain;charset=utf-8") public String doGet(@PathVariable String appid, HttpServletRequest request) { // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr); if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) { log.info("signature " + signature + " timestamp " + timestamp + " nonce " + nonce + " echostr " + echostr); throw new IllegalArgumentException("请求参数非法,请核实!"); } final WxMpService wxService = WxMpConfiguration.getMpServices().get(appid); if (wxService == null) { throw new IllegalArgumentException(String.format("未找到对应appid=[%d]的配置,请核实!", appid)); } // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 if (wxService.checkSignature(timestamp, nonce, signature)) { log.info("weixin get success...."+echostr); return echostr; }else { log.error("weixin get failed...."); return "weixin get failed...."; } }
② 配置JS接口安全域名:点击修改将外网访问地址填好提交就可以了。
③ 检查关注公众号,取消关注是否生效
首先在下面方法上打断点,再扫码关注,会进入该方法,同样操作取关也会进入该方法,再需要自己根据实际需求进行业务代码的开发。
// 关注,取关,客服,菜单等调用接口(POST) @PostMapping(produces = "application/xml; charset=UTF-8") public String doPost(HttpServletRequest request, @PathVariable String appid, @RequestBody String requestBody) { log.debug("weixin login get..."); // 获取微信公众号传输过来的code,通过code可获取access_token,进而获取用户信息 String code = request.getParameter("code"); // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // openid String openid = request.getParameter("openid"); // encType--可空 String encType = request.getParameter("encType"); // msgSignature--可空 String msgSignature = request.getParameter("msgSignature"); final WxMpService wxService = WxMpConfiguration.getMpServices().get(appid); if (!wxService.checkSignature(timestamp, nonce, signature)) { log.info( "\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}]," + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ", openid, signature, encType, msgSignature, timestamp, nonce, requestBody); throw new IllegalArgumentException("非法请求,可能属于伪造的请求!"); } String out = null; if (StringUtils.isBlank(encType)) { // 明文传输的消息 WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody); WxMpXmlOutMessage outMessage = this.route(inMessage, appid); if (outMessage == null) { return ""; } out = outMessage.toXml(); } else if ("aes".equalsIgnoreCase(encType)) { // aes加密的消息 WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(), timestamp, nonce, msgSignature); log.debug("\n消息解密后内容为:\n{} ", inMessage.toString()); WxMpXmlOutMessage outMessage = this.route(inMessage, appid); if (outMessage == null) { return ""; } out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage()); } log.debug("\n组装回复信息:{}", out); return out; }