美文网首页工作生活
Java版本微信公众号接入(附详细代码流程)

Java版本微信公众号接入(附详细代码流程)

作者: yuan_dongj | 来源:发表于2019-06-30 11:24 被阅读0次

    前言

    首先要明白公众号号与代公众号的区别。微信提供了微信公众平台与微信开放平台,微信公众平台就是我们常用的公众号,而微信开放平台就是指的代公众号,为第三方平台服务。

    所谓的代公众号指的就是将公众号授权给第三方平台进行管理,第三方平台拥有全部或部分的该公众号的接口权限,可以帮助管理运营公众号。


    微信公众平台-公众号.png 微信开放平台-代公众号.png

    本文非第三方平台接入,楼主在开发公众号与代公众号过程中遇到了不少坑,写这篇文章的目的也是为后人提供一些踩坑指南,文中代码均已经过测试,可以放心食用。

    公众平台配置

    公众平台配置.png

    消息加解密方式选择安全模式。启用服务器配置时,微信需要验证所填服务器地址url是否可用,开发时使用内网穿透外网工具。url的接口开发完成后才能保存成功。

    • IP白名单不要忘记配置,多个IP换行分隔。如果不知道本机外网IP,请百度关键词IP。
    • 如果启用了服务器配置,微信公众平台将不再提供自定义菜单、自动回复等基础功能,平台上原有的菜单将会失效。
    image.png

    场景:公众号由运营人员运营,开发人员需要在自家的产品上嵌入公众号。很尴尬的情况出现了,运营人员不能放弃使用微信公众平台,开发人员需要接入公众号。对于这种情况,可以在公众号关闭服务器配置,将公众号授权给第三方平台,微信会将公众号的事件推送给第三方平台,以第三方平台的方式进行开发。

    开发前准备

    开发配置

    • 所需依赖
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <!--上文打包好的微信加解密工具-->
            <dependency>
                <groupId>com.hualala</groupId>
                <artifactId>aes-util</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.4</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.29</version>
            </dependency>
            <dependency>
                <groupId>dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>1.6</version>
            </dependency>
    
    • 项目配置
    server:
      port: 8080
    
    spring:
      redis:
        host: 127.0.0.1
        port: 6379
        database: 0
        timeout: 10000
    
    wechat:
      appID: #公众号AppID
      secret: #公众平台配置的APPSecret
      token: #公众平台配置的Token
      encodingAESKey: #公众平台配置的EncodingAESKey
    
    
    /**
     * @author YuanChong
     * @create 2019-06-26 22:20
     * @desc
     */
    @Data
    @Component
    @ConfigurationProperties(prefix = "wechat")
    public class WXConfig implements InitializingBean {
    
        private String appID;
        private String secret;
        private String token;
        private String encodingAESKey;
    
        /**
         * 微信加解密工具
         */
        private WXBizMsgCrypt wxBizMsgCrypt;
    
        /**
         * 创建全局唯一的微信加解密工具
         * @throws Exception
         */
        @Override
        public void afterPropertiesSet() throws Exception {
            wxBizMsgCrypt = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appID);
        }
    }
    
    /**
     * @author YuanChong
     * @create 2018-07-04 18:56
     * @desc  与微信交互有大量的URL,建议统一保存管理
     */
    public class WXConstant {
    
        /**
         * 微信公众号access_token_key 用于保存在redis中的key
         */
        public static final String ACCESS_TOKEN_KEY = "wechat:accessToken:%s";
    
        /**
         * 获取微信公众号的access_token
         */
        public static final String WX_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    
    }
    
    

    注意:公众号AppID在项目代码的任何地方都不要写死,保证公众号灵活随时可配置更换

    开发

    • 获取公众号AccessToken
      AccessToken是公众号的唯一凭证,附部分微信文档。楼主把AccessToken存储在了redis中,对外只暴露redis。
    1. 建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
    2. 目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器可对外继续输出的老access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;
    3. access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。
    /**
     * @author YuanChong
     * @create 2019-06-26 22:47
     * @desc
     */
    @Component
    public class RefreshToken implements InitializingBean {
    
        @Autowired
        private WXService wxService;
    
        /**
         * 刷新token的定时线程
         */
        private ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("refresh-wx-access-token-%d").daemon(true).build());
    
    
        @Override
        public void afterPropertiesSet() {
            scheduledPool.scheduleAtFixedRate(() -> wxService.refreshToken(),0, 7000, TimeUnit.SECONDS);
        }
    }
    
    /**
     * @author YuanChong
     * @create 2019-06-26 22:51
     * @desc
     */
    @Log4j2
    @Service
    public class WXService {
    
        @Autowired
        private WXConfig wxConfig;
    
        /**
         * 刷新微信公众号的access_token
         * https请求:
         *       https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
         * 微信返回数据:
         *       {"access_token":"ACCESS_TOKEN","expires_in":7200}
         * @return
         */
        public String refreshToken() {
            String redisKey = String.format(WXConstant.ACCESS_TOKEN_KEY,wxConfig.getAppID());
            String url = String.format(WXConstant.WX_ACCESS_TOKEN_URL,wxConfig.getAppID(),wxConfig.getSecret());
            //HttpClient工具根据项目自行修改
            HttpClientUtil.HttpResult result = HttpClientUtil.getInstance().post(url,"");
            log.info("获取微信公众号的access_token: {}", result.getContent());
            String accessToken = JSON.parseObject(result.getContent()).getString("access_token");
            //redis工具根据项目自行修改
            CacheUtils.set(redisKey, accessToken, 7200);
            return accessToken;
        }
    }
    
    • 服务器url(微信事件推送)

    首先初步接入,保存公众平台上配置的服务器URL。本机开启内网穿透,微信会尝试对接口发送参数echostr,原样返回即可接入成功。

      /**
     * @author YuanChong
     * @create 2019-06-26 21:51
     * @desc
     */
    @Log4j2
    @RestController
    @RequestMapping("/notice")
    public class NoticeController {
    
    
        /**
         * 公众号消息和事件推送
         *
         * @param timestamp    时间戳
         * @param nonce        随机数
         * @param msgSignature 消息体签名
         * @param echostr 初次接入配置所需
         * @param postData 消息体
         * @return
         */
        @ResponseBody
        @RequestMapping(value = "/event")
        public Object official(@RequestParam("timestamp") String timestamp,
                               @RequestParam("nonce") String nonce,
                               @RequestParam(value = "msg_signature",required = false) String msgSignature,
                               @RequestParam(value = "echostr", required = false) String echostr,
                               @RequestBody(required = false) String postData) throws Exception {
            return echostr;
        }
    }
    

    微信给推送与接收的数据格式均为xml,简单些一个xml与map的转换工具

    /**
     * @author YuanChong
     * @create 2019-06-26 20:48
     * @desc xml转换工具
     */
    public class XMLParse {
    
    
        /**
         * @param xml 要转换的xml字符串
         * @return 转换成map后返回结果
         * @throws Exception
         */
        public static Map<String, String> xmlToMap(String xml) throws Exception {
            Map<String, String> respMap = new HashMap<String, String>();
            SAXReader reader = new SAXReader();
            Document doc = reader.read(new ByteArrayInputStream(xml.getBytes("utf-8")));
            Element root = doc.getRootElement();
            xmlToMap(root, respMap);
            return respMap;
        }
    
        /**
         * map对象转行成xml
         *
         * @param map
         * @return
         * @throws IOException
         */
        public static String mapToXml(Map<String, Object> map) throws IOException {
            Document d = DocumentHelper.createDocument();
            Element root = d.addElement("xml");
            mapToXml(root, map);
            StringWriter sw = new StringWriter();
            XMLWriter xw = new XMLWriter(sw);
            xw.setEscapeText(false);
            xw.write(d);
            return sw.toString();
        }
    
    
        /**
         * 递归转换
         *
         * @param root
         * @param map
         * @return
         * @throws IOException
         */
        private static Element mapToXml(Element root, Map<String, Object> map) throws IOException {
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (entry.getValue() instanceof Map) {
                    Element element = root.addElement(entry.getKey());
                    mapToXml(element, (Map<String, Object>) entry.getValue());
                } else {
                    root.addElement(entry.getKey()).addText(entry.getValue().toString());
                }
            }
            return root;
        }
    
    
        /**
         * 递归转换
         *
         * @param tmpElement
         * @param respMap
         * @return
         */
        private static Map<String, String> xmlToMap(Element tmpElement, Map<String, String> respMap) {
            if (tmpElement.isTextOnly()) {
                respMap.put(tmpElement.getName(), tmpElement.getText());
                return respMap;
            }
            Iterator<Element> eItor = tmpElement.elementIterator();
            while (eItor.hasNext()) {
                Element element = eItor.next();
                xmlToMap(element, respMap);
            }
            return respMap;
        }
    
    
    }
    
    

    公众平台上所配置的服务器url是一个非常重要的接口地址,用户关注、取关、自定义菜单点击事件、接收用户消息并作出被动回复等等都会推送到这个接口上,微信以不同的type来区分不同的事件类型,建议以策略模式来写这个接口。

    策略上层抽象接口

    /**
     * @author YuanChong
     * @create 2018-07-06 16:21
     * @desc 微信事件推送策略的最上层抽象
     */
    public interface WeChatNotify {
    
    
        /**
         * 上层事件推送策略抽象接口
         *
         * @param xmlMap 微信推送的参数数据
         * @return  返回给微信的回复信息 如:接收到用户发消息事件,我们给他返回“我收到啦”
         * @throws Exception
         */
        String weChatNotify(Map<String, String> xmlMap) throws Exception;
    
    }
    

    策略枚举

    /**
     * @author YuanChong
     * @create 2018-07-31 15:27
     * @desc 微信的推送类型枚举
     */
    public enum NotifyEnum {
    
        //菜单点击事件
        CLICK("event", "CLICK"),
        //关注
        SUBSCRIBE("event", "subscribe"),
        //取关
        UNSUBSCRIBE("event", "unsubscribe"),
        //已关注时的扫码事件
        SCAN("event", "SCAN"),
        //文字消息回复
        TEXT("text", null);
    
    
        private String msgType;
        private String event;
    
        NotifyEnum(String msgType, String event) {
            this.msgType = msgType;
            this.event = event;
        }
    
        public String getMsgType() {
            return this.msgType;
        }
    
        public String getEvent() {
            return this.event;
        }
    
        /**
         * 解析事件类型
         *
         * @param msgType
         * @param event
         * @return
         */
        public static NotifyEnum resolveEvent(String msgType, String event) {
            for (NotifyEnum notifyEnum : NotifyEnum.values()) {
                if (Objects.equals(msgType, notifyEnum.getMsgType()) && Objects.equals(event, notifyEnum.getEvent())) {
                    return notifyEnum;
                }
            }
            return null;
        }
    }
    

    策略注解

    /**
     * @author YuanChong
     * @create 2018-07-31 15:27
     * @desc  微信的推送类型注解
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Component
    public @interface NotifyType {
    
        /**
         * 事件推送的类型 支持枚举多个事件
         * @return
         */
        NotifyEnum[] value();
    }
    

    策略具体实现,针对于不同的事件推送,以微信文档为主

        /**
         * @author YuanChong
         * @create 2018-07-06 17:12
         * @desc 策略实现 用户取消关注
         */
    
        @Log4j2
        @NotifyType(NotifyEnum.UNSUBSCRIBE)
        public static class UnSubscribe implements WeChatNotify {
    
            @Autowired
            private WXService wxService;
    
            @Override
            public String weChatNotify(Map<String, String> xmlMap) throws Exception {
                return "";
            }
        }
    
    
        /**
         * @author YuanChong
         * @create 2019-01-15 17:12
         * @desc 策略实现 用户关注事件
         */
        @Log4j2
        @NotifyType(NotifyEnum.SUBSCRIBE)
        public static class Subscribe implements WeChatNotify {
    
            @Autowired
            private WXService wxService;
    
            @Override
            public String weChatNotify(Map<String, String> xmlMap) throws Exception {
                return "";
            }
        }
    

    策略工厂

    /**
     * @author YuanChong
     * @create 2018-07-31 15:43
     * @desc
     */
    @Component
    public class NotifyFactory implements ApplicationContextAware {
    
        /**
         * 策略列表
         */
        private Map<NotifyEnum, WeChatNotify> notifyMap = new HashMap<>();
    
        /**
         * 工厂获取事件执行策略对象
         *
         * @param notifyType
         * @return
         */
        public WeChatNotify loadWeChatNotify(NotifyEnum notifyType) {
            WeChatNotify notify = notifyMap.get(notifyType);
            //对于没配置的策略 返回一个默认的空实现即可
            return Optional.ofNullable(notify).orElse((xmlMap) -> this.defaultNotify(xmlMap));
        }
    
        /**
         * 工厂提供默认空实现
         *
         * @param xmlMap
         * @return
         */
        public String defaultNotify(Map<String, String> xmlMap) {
            return "success";
        }
    
    
        /**
         * 扫描带有NotifyType注解的bean组装成map
         * 新加策略时 在类上加入注解@NotifyType(...)即可
         * 支持枚举多个策略事件
         *
         * @param applicationContext
         * @throws BeansException
         */
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            Map<String, Object> notifyBeanMap = applicationContext.getBeansWithAnnotation(NotifyType.class);
            Map<NotifyEnum[], WeChatNotify> annoValueBeanMap = notifyBeanMap.values().stream()
                    .filter(obj -> ArrayUtils.contains(obj.getClass().getInterfaces(), WeChatNotify.class))
                    .map(obj -> (WeChatNotify) obj)
                    .collect(Collectors.toMap(obj -> obj.getClass().getAnnotation(NotifyType.class).value(), Function.identity()));
    
            annoValueBeanMap.entrySet().stream().forEach(enrty -> Arrays.stream(enrty.getKey()).forEach(type -> notifyMap.put(type, enrty.getValue())));
        }
    }
    

    完善controller接口

    /**
     * @author YuanChong
     * @create 2019-06-26 21:51
     * @desc
     */
    @Log4j2
    @RestController
    @RequestMapping("/notice")
    public class NoticeController {
    
        @Autowired
        private WXConfig wxConfig;
    
        @Autowired
        private NotifyFactory notifyFactory;
    
        /**
         * 公众号消息和事件推送
         *
         * @param timestamp    时间戳
         * @param nonce        随机数
         * @param msgSignature 消息体签名
         * @param echostr 初次接入配置所需
         * @param postData 消息体
         * @return
         */
        @ResponseBody
        @RequestMapping(value = "/event")
        public Object official(@RequestParam("timestamp") String timestamp,
                               @RequestParam("nonce") String nonce,
                               @RequestParam(value = "msg_signature",required = false) String msgSignature,
                               @RequestParam(value = "echostr", required = false) String echostr,
                               @RequestBody(required = false) String postData) throws Exception {
            log.info("Msg接收到的POST请求: signature={}, timestamp={}, nonce={}, echostr={} postData={}", msgSignature, timestamp, nonce, echostr,postData);
            if(StringUtils.isEmpty(postData)) {
                //此处用于公众平台配置的初步接入
                return echostr;
            }
            WXBizMsgCrypt pc = wxConfig.getWxBizMsgCrypt();
            //签名校验 数据解密
            String decryptXml = pc.decryptMsg(msgSignature, timestamp, nonce, postData);
            Map<String, String> decryptMap = XMLParse.xmlToMap(decryptXml);
            //获取推送事件类型  可以拿到的事件: 1 关注/取消关注事件  2:扫描带参数二维码事件 3: 用户已经关注公众号 扫描带参数二维码事件 ...等等
            NotifyEnum notifyEnum = NotifyEnum.resolveEvent(decryptMap.get("MsgType"), decryptMap.get("Event"));
            WeChatNotify infoType = notifyFactory.loadWeChatNotify(notifyEnum);
            //执行具体的策略 得到给微信的响应信息 微信有重试机制  需要考虑幂等性
            String result = infoType.weChatNotify(decryptMap);
            log.info("Msg响应的POST结果: 授权策略对象: [{}] 解密后信息: [{}] 返回给微信的信息: [{}]", infoType.getClass().getSimpleName(), decryptMap, result);
            return result;
        }
    }
    

    加解密异常java.security.InvalidKeyException:illegal Key Size解决方案

    微信针对这个异常提供的文档说明
    在官方网站下载JCE无限制权限策略文件(JDK7的下载地址
    下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt,如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件;如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件

    相关文章

      网友评论

        本文标题:Java版本微信公众号接入(附详细代码流程)

        本文链接:https://www.haomeiwen.com/subject/tjgvcctx.html