企业微信三方开发(一):回调验证及重要参数获取

Git 地址:https://gitee.com/tantantantan/cwp.git
——————————————————————

其他链接

初识微信开发

企业微信三方开发:注册企业微信服务商

企业微信三方开发(一):回调验证及重要参数获取

企业微信三方开发(二):获取access_token

企业微信三方开发(三):网页授权登录

企业微信三方开发(四):发送消息

企业微信三方开发(五):扫码登录

前言

  1. 我们在新建好一个网页应用后,需要填一些基础配置。其中最重要的就是一些回调配置,回调路径指向我们自己的服务器,需要正确接收响应微信服务器的请求。
  2. 微信接口开发无非就是通过一些带token的http请求来实现相关功能,而获取这些token往往需要先获取一些重要参数,这里有两个参数需要先获取:suite_ticketauth_code
  3. 对微信开发不熟,或者没读我之前文章的同学一定要搞清一个概念:在三方应用开发中,应用开发方统称三方服务商,对应id我会称 【服务商corpId】;应用使用方称作 企业,对应id叫 【企业corpId】。请不要搞混了

技术栈及工具

  • 开发框架:spring-boot
  • 开发工具: idea

一、后台配置

应用回调配置

首先我们进入新建的应用:
在这里插入图片描述
其中最主要的配置就是这个回调配置:
在这里插入图片描述

共有四个配置项,分别是:

  • 两个回调URL
  • Token(密钥,不可泄漏)
  • EncodingAESKey(加密消息内容的码)

这里的URL就是我们服务器的响应路径,微信服务器会根据这里的URL向我们服务器发送请求,我们服务器验证方面需要做两件事:

  • 分辨出是否为企业微信来源
  • 分辨出推送消息的内容是否被篡改

在这里插入图片描述

填写好回调URL,并选择自动生成Token和EncodingAESKey

简单一句话概括就是,在某些时点(具体什么时点或访问哪个回调请看下面 “写VerifyController类” 内容),微信服务器会向我们服务器发送验证请求,我们服务器需要通过Token、EncodingAESKey和服务商CorpID计算Signature与GET请求参数中的Signature作对比,如果一样就验证通过,返回"success"。

具体的加密计算方法不再赘述,自己阅读文档。企业微信为我们集成好了加解密的包,我们拿之即用就行:
https://work.weixin.qq.com/api/doc/90000/90138/90307

通用开发参数回调配置

还有一个重要参数需要设置,就是 通用开发参数 里的 系统事件接收URL
在这里插入图片描述

企业 安装应用时,微信服务器会向这个 系统事件接收URL 发送验证请求,我们需要做的响应跟上面的应用回调验证基本一样。 所以我将两者URL、Token、EncodingAESKey设为一样,通过同一个接口来处理这两种请求。

由于后台还未搭建,这些设置都保存不了,接下来我们先搭建后台。

二、构建spring-boot项目

Spring Boot的出现简直就是像我这样的后端渣渣的福音,我们可以很快速的构建一个服务系统。所以我很愉快的选择了它!

还有很重要的一点,本系列文重在打通企业微信的一些功能,以及验证一些逻辑。代码尽量简洁。所以我暂时不会遵循实战项目的设计逻辑,也缺少一些排错处理。请读者知悉!

新建项目

新建一个项目:cwp(company wechat project之意)
并新建一些必要的包和类,具体目录结构如下:
在这里插入图片描述

项目目录结构

  • 新建controller目录:这里暂时不考虑打通数据库,接收请求和数据处理全在controller完成。
  • 新建confg目录:存放Constant.java——公用参数。
  • 新建 data.properties :通过controller获得的数据存在此文件中。
  • 新建 util 目录:存放 PropertiesUtil.java ——此类作用是动态向data.properties写入内容。存放 WxUtil.java——此类存放xml和map互转等方法。

导入加解密包

还记的之前说过的企业微信给的加密计算方法实例吗?
微信准备了xml和json两种解密方式。这里选择Java的xml方式。将验证包中的类全部考入 wechataes包 中:
在这里插入图片描述
同时pom.xml中需要引入commons.codec包

        <!-- commons-codec-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

写验证类

此类有两个方法:

  • doGetValid : 接收验证请求,用于验证通用开发参数系统事件接收URL、数据回调URL、指令回调URL。当企业微信后台录入回调URL点击保存时,微信服务器会立即发送一条GET请求到对应URL,该函数就对URL的signature进行验证。
  • doPostValid: 用于获取 suite_ticket ,以及安装应用时传递过来的 auth_code ,还有用户从企业微信打开应用时也会调用此函数。解密包的第三个参数需做区分:当刷新ticket和安装应用时传递 【SuitID】 ;当打开应用时传递 【CorpID

再来捋一下,数据回调和指令回调除了保存配置时被访问,还有哪些情况会被微信服务器访问:

  • 数据回调URL: 当每次从企业微信打开应用时。【doPostValid 中的解密参数传递CorpID】
  • 指令回调URL: 微信服务器推送suite_ticket以及安装应用时推送auth_code时。【doPostValid 中的解密参数传递SuitID】

1、回调验证

doGetValid方法如下:

package com.tan.cwp.controller;

import com.tan.cwp.config.Constant;
import com.tan.cwp.wechataes.WXBizMsgCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@RestController
@RequestMapping("/verify")
public class VerifyController {
    Logger logger = LoggerFactory.getLogger(VerifyController.class);

    /*
     * 验证通用开发参数及应用回调
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.GET)
    public void doGetValid(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");
        // 随机字符串
        // 如果是刷新,需返回原echostr
        String echostr = request.getParameter("echostr");

        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, Constant.CorpID);

        String sEchoStr=""; //需要返回的明文
        PrintWriter out;
        try {
            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp,
                    nonce, echostr);
            logger.info("verifyurl echostr: " + sEchoStr);

            // 验证URL成功,将sEchoStr返回
            out = response.getWriter();
            out.print(sEchoStr);
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            e.printStackTrace();
        }
    }
}

在后台录入好接口URL保存时,微信后台即会向该接口发送GET请求,并携带四个参数:msg_signature、timestamp、nonce、echostr。

通过 request.getParameter(xxx)获取到他们。

还需要从企业微信后台获取三个固定参数用于计算singnature:TOKEN、EncodingAESKey、CorpID

我把固定参数都放在 配置类Constant 中:

package com.tan.cwp.config;

public class Constant {
    // 服务商相关
    /**
     * 服务商CorpID
     */
    public static final String CorpID = "xxxx";
    /**
     * 服务商身份的调用凭证
     */
    public static final String ProviderSecret = "xxxx";

    // 应用相关
    /**
     * 应用的唯一身份标识
     */
    public static final String SuiteID = "xxxx";
    /**
     * 应用的调用身份密钥
     */
    public static final String SuiteSecret = "xxxx";

    // 回调相关
    /**
     * 回调/通用开发参数Token, 两者解密算法一样,所以为方便设为一样
     */
    public static final String TOKEN = "xxxx";

    /**
     * 回调/通用开发参数EncodingAESKey, 两者解密算法一样,所以为方便设为一样
     */
    public static final String EncodingAESKey = "xxxx";

}

有了这些重要参数就可以通过企业微信准备的包进行验证了。运行项目,并点击申请验证:
在这里插入图片描述
都成功返回 echostr,并显示已验证:

在这里插入图片描述
在这里插入图片描述
通用参数也同样验证成功:
在这里插入图片描述

2、获取suite_ticket及auth_code

  • suite_ticket: 是用于获取 第三方应用凭证(suite_access_token) 的重要参数,由企业微信后台每过十分钟推送一次给 “指令回调URL”
  • auth_code: 是用于获取企业临时授权码的重要参数,由企业安装应用成功时返回

两者获取方式完全相同,微信服务器会通过POST请求向我们服务器传递:msg_signature,timestamp,nonce,echostr四个参数 ,同样通过 request.getParameter(xxx)获取。

同时还会传递一个加密过的xml格式的请求体,通过输入流方式获取该参数,并通过企业微信的验证包进行解密。

具体解密过程查看 doPostValid 方法:

package com.tan.cwp.controller;

import com.tan.cwp.config.Constant;
import com.tan.cwp.util.PropertiesUtil;
import com.tan.cwp.util.WxUtil;
import com.tan.cwp.wechataes.WXBizMsgCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Map;

@RestController
@RequestMapping("/verify")
public class VerifyController {
    Logger logger = LoggerFactory.getLogger(VerifyController.class);

    /**
     * 验证通用开发参数
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.GET)
    public void doGetValid(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");
        // 随机字符串
        // 如果是刷新,需返回原echostr
        String echostr = request.getParameter("echostr");

        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, Constant.CorpID);

        String sEchoStr=""; //需要返回的明文
        PrintWriter out;
        try {
            sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp,
                    nonce, echostr);

            // 验证URL成功,将sEchoStr返回
            out = response.getWriter();
            out.print(sEchoStr);
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            e.printStackTrace();
        }
    }

    /**
     * 刷新 ticket
     */
    @RequestMapping(value = "callback_verify.do" ,method = RequestMethod.POST)
    public String doPostValid(HttpServletRequest request) throws Exception {

        // 微信加密签名
        String msg_signature = request.getParameter("msg_signature");
        // 时间戳
        String timestamp = request.getParameter("timestamp");
        // 随机数
        String nonce = request.getParameter("nonce");

        String type = request.getParameter("type");

        String id = "";

        // 访问应用和企业回调传不同的ID
        if(type.equals("data")){
            id = Constant.CorpID;
        } else {
            id = Constant.SuiteID;
        }



        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(Constant.TOKEN, Constant.EncodingAESKey, id);

        String postData="";   // 密文,对应POST请求的数据
        //1.获取加密的请求消息:使用输入流获得加密请求消息postData
        ServletInputStream in = request.getInputStream();
        BufferedReader reader =new BufferedReader(new InputStreamReader(in));

        String tempStr="";   //作为输出字符串的临时串,用于判断是否读取完毕
        while(null!=(tempStr=reader.readLine())){
            postData+=tempStr;
        }

        String suiteXml=wxcpt.DecryptMsg( msg_signature, timestamp, nonce, postData);
        logger.info("suiteXml: " + suiteXml);

        Map suiteMap = WxUtil.transferXmlToMap(suiteXml);
        if(suiteMap.get("SuiteTicket") != null) {
            PropertiesUtil.setProperty("suite_ticket", (String) suiteMap.get("SuiteTicket"));
        } else if(suiteMap.get("AuthCode") != null){
            PropertiesUtil.setProperty("auth_code", (String) suiteMap.get("AuthCode"));
        }

        String success = "success";
        return success;
    }


}



注意这段:
在这里插入图片描述
我们在数据回调URL加个参数用于判断是用corpid还是suiteid:
在这里插入图片描述
最终解密过后都会获取到一个字符串类型的xml,我们用 WxUtil类 中的 parseXml方法 将string格式转为map格式:

package com.tan.cwp.util;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class WxUtil {
    /**
     * 将 Map 转化为 XML
     *
     * @param map
     * @return
     */
    public static String transferMapToXml(SortedMap<String, Object> map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (String key : map.keySet()) {
            sb.append("<").append(key).append(">")
                    .append(map.get(key))
                    .append("</").append(key).append(">");
        }
        return sb.append("</xml>").toString();
    }

    /**
     * 将 XML 转化为 map
     *
     * @param strxml
     * @return
     * @throws IOException
     */
    public static Map transferXmlToMap(String strxml) throws IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = null;
        try {
            doc = builder.build(in);
        } catch (JDOMException e) {
            throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
        }
        // 解析 DOM
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //关闭流
        in.close();
        return m;
    }

    // 辅助 transferXmlToMap 方法递归提取子节点数据
    private static String getChildrenText(List<Element> children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator<Element> it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List<Element> list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }
}



里面用到了dom4J,需要在pom.xml中引入:

        <!--XML 解析包-->
        <dependency>
            <groupId>org.jdom</groupId>
            <artifactId>jdom2</artifactId>
            <version>2.0.6</version>
        </dependency>

由于并不急着打通数据库,这里通过 PropertiesUtil类 动态的读取和写入获取到的参数到 data.properties中:

package com.tan.cwp.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

public class PropertiesUtil {
    private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);

    private static Properties props;
    private static String fileName = "data.properties";
    private static String path ="D:/Nobug/WeChat/backend/cwp/src/main/resources/data.properties";
    static {
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
            logger.error("配置文件读取异常",e);
        }
    }
    
    // 读取参数
    public static String getProperty(String key){
        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            return null;
        }
        return value.trim();
    }

    // 写入参数
    public static String setProperty(String key, String value) throws IOException {
        props.setProperty(key, value);
        FileOutputStream file = new FileOutputStream(path);
        props.store(file,"refresh");
        return null;
    }

}

pom.xml需要引入commons-lang3

      <!--StringUtils-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>

记住要先在data.properties中放好suite_ticket和auth_code:
在这里插入图片描述
正确获取到参数后,还需要返回success给微信服务器(注意要将"success"字符串复制给一个变量返回,这是一个坑!)

再次运行项目,我们先获取suite_ticket:
在这里插入图片描述
写入成功:
写入成功
注意!!以下安装测试需先通过授权才能安装。授权方法参考下一章[企业微信三方开发(二)的授权配置一节](https://blog.csdn.net/YNEWA/article/details/106885604),建议把下一章全部调通再回过头来测试安装应用!

我们再试下安装测试:
在这里插入图片描述
在这里插入图片描述

通过企业微信扫码安装后,显示安装成功:
在这里插入图片描述
AuthCode也写入:
在这里插入图片描述

总结

在第三方应用开发提供的接口中,主要围绕三种类型的access_token进行开发:

  • provider_access_token 服务商的token
  • suite_access_token 第三方应用的token
  • access_token 授权企业的token

通过今天的配置和获取到SuiteTicketAuthCode后,接下来要做的就是通过一系列HTTP请求来愉快获取这些token!

2020/10/7重要更正:
在这里插入图片描述
今天在测试数据回调时发现数据回调后台提示corpid验证失败。原来逻辑是实例化WXBizMsgCrypt类时传递的corpid为服务商的id。通过测试发现要用安装企业的id才行,赶紧回看文档果然有变化。等于说企业微信服务器在访问数据回调url时都会携带上访问的企业id,后台在处理时将corpid取出用作参数就行了!
在这里插入图片描述
首先将数据回调URL加上 &corpid=$CORPID$ ,微信服务器会自动将 $CORPID$ 的内容替换为该企业的corpid,我们后台直接获取该参数就行了。

在这里插入图片描述

  • 20
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
企业微信开发回调验证是指通过验证企业微信接收到的外部事件回调(例如消息、联系人等),确保这些事件确实是由企业微信发送的。下面给出一个用Java代码实现企业微信开发回调验证的示例: ```java import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; public class CallbackVerifier { private static final String TOKEN = "your_token"; // 企业微信后台配置的Token // 验证回调请求的签名是否合法 public static boolean verifySignature(String signature, String timestamp, String nonce) { String[] arr = {TOKEN, timestamp, nonce}; Arrays.sort(arr); // 字典序排序 StringBuilder content = new StringBuilder(); for (String s : arr) { content.append(s); } return signature.equals(sha1(content.toString())); } // 使用SHA1算法计算字符串的哈希值 private static String sha1(String str) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(str.getBytes()); byte[] bytes = digest.digest(); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { String hex = Integer.toHexString(b & 0xff); if (hex.length() == 1) { sb.append("0"); } sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } // 测试 public static void main(String[] args) { String signature = "signature"; // 从企业微信接收到的请求参数 String timestamp = "timestamp"; String nonce = "nonce"; boolean isValid = verifySignature(signature, timestamp, nonce); System.out.println("是否合法:" + isValid); } } ``` 在上述代码中,首先定义了一个TOKEN常量,即企业微信后台配置的Token。然后定义了一个verifySignature方法,该方法接收从企业微信接收到的signature、timestamp和nonce参数,并通过字典序排序和SHA1算法生成待验证的签名。最后,通过判断生成的签名是否与接收到的signature一致来验证回调请求的合法性。 该示例代码可以直接运行,并且可以将TOKEN替换为实际的Token进行验证
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值