欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

微信开放平台【第三方平台】java开发总结:验证票据(component_verify_ticket)(-)

程序员文章站 2023-12-24 15:09:45
...

概述
微信公众平台-第三方平台(简称第三方平台)开放给所有通过开发者资质认证后的开发者使用。在得到公众号或小程序运营者(简称运营者)授权后,第三方平台开发者可以通过调用微信开放平台的接口能力,为公众号或小程序的运营者提供账号申请、小程序创建、技术开发、行业方案、活动营销、插件能力等全方位服务。同一个账号的运营者可以选择多家适合自己的第三方为其提*品能力或委托运营。

第三方开放平台的网址是:https://open.weixin.qq.com

第三方开放平台微信官方文档网址:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html

第三方开发资料

首先需要申请创建第三方服务平台账号,填写好对应的资料信息。

以上的开发资料信息都是申请时需要我们自己填写的。

开发资料说明:
授权发起页域名:公众号和小程序发起授权的页面操作,必须在该域名下发起
测试公众号列表:全网发布以前,只有列表里的公众号才能继续授权,这里是小程序和公众号的原始id

授权时间接收URL:该回调接口主要用来接收微信官方推送的消息通知,用户授权、取消授权、发送ticket等都通过这个接口,必须返回success响应
消息校验Token:收到微信的消息时,解密校验用
消息加密Key:微信和我们通信时,消息加解***
消息与事件接收URL:用来接收公众号和小程序发送的消息,小程序代码审核的结果通知
小程序服务器域名:小程序服务器域名地址,用于小程序发起网络请求
小程序业务域名:小程序调用web-view接口,打开的网页必须在此域名下
白名单ip地址列表:仅此ip名单下才可以调用微信的接口

推送 component_verify_ticket:
验证票据
验证票据(component_verify_ticket),在第三方平台创建审核通过后,微信服务器会向其 ”授权事件接收URL” 每隔 10 分钟以 POST 的方式推送 component_verify_ticket

@ApiOperation(value = "授权事件接收URL,验证票据")
    @RequestMapping(value = "/platform/event",
            method = RequestMethod.POST)
    public ResponseEntity<ResultBean> wechatPlatformEvent(@RequestParam("timestamp") String timestamp,
                                                          @RequestParam("nonce") String nonce,
                                                          @RequestParam("msg_signature") String msgSignature,
                                                          @RequestBody String postData) {
        ResultBean<String> resultBean = new ResultBean<>();
        ResponseEntity<ResultBean> responseEntity;
        logger.debug("授权事件接收URL,验证票据");
        try {
            resultBean.setData(dbyWechatExtService.parsePequest(timestamp,nonce,msgSignature,postData));
            responseEntity = new ResponseEntity<ResultBean>(resultBean, HttpStatus.OK);
            logger.debug("第三方平台授权事件接收URL,验证票据成功");
        } catch (Exception e) {
            logger.error("第三方平台授权事件接收URL,验证票据异常", e.getMessage(), e);
            ResultBean<String> errorResultBean = new ResultBean<>();
            errorResultBean.setMsg("第三方平台授权事件接收URL,验证票据异常");
            errorResultBean.setErrorMsg(e.getMessage());
            errorResultBean.setCode(422);
            responseEntity = new ResponseEntity<ResultBean>(errorResultBean, HttpStatus.UNPROCESSABLE_ENTITY);
        }
        return responseEntity;
    }
/**
     * 获得授权事件的票据
     *
     * @param timestamp    时间戳
     * @param nonce        随机数
     * @param msgSignature 消息体签名
     * @param postData     消息体
     * @return 如果获得只需要返回 SUCCESS
     */
    String parsePequest(String timestamp, String nonce, String msgSignature, String postData);
  • 填写自己的配置信息:第三方平台appId、第三方平台 secret、第三方平台 消息加解密Key、第三方平台 消息校验Token
/**
     * 第三方平台appId
     */
    private static final String PLATFORM_APP_ID = "****************";

    /**
     * 第三方平台 secret
     */
    private static final String PLATFORM_APP_SECRET = "****************";

    /**
     * 第三方平台 消息加解密Key
     */
    private static final String PLATFORM_AES_KEY = "****************";

    /**
     * 第三方平台 消息校验Token
     */
    private static final String PLATFORM_COMPONENT_TOKEN = "****************"; 

@Override
    public String parsePequest(String timeStamp, String nonce, String msgSignature, String postData) {
        logger.debug("==============================开始授权事件接收URL=================================");
        try {
            //这个类是微信官网提供的解密类,需要用到消息校验Token 消息加密Key和服务平台appid
            WXBizMsgCrypt pc = new WXBizMsgCrypt(PLATFORM_COMPONENT_TOKEN, PLATFORM_AES_KEY, PLATFORM_APP_ID);
            String xml = pc.decryptMsg(msgSignature, timeStamp, nonce, postData);
            Map<String, String> result = WXXmlToMapUtil.xmlToMap(xml);// 将xml转为map
            String componentVerifyTicket = MapUtils.getString(result, "ComponentVerifyTicket");
            if (StringUtils.isNotEmpty(componentVerifyTicket)) {
                // 存储平台授权票据,保存ticket
                redisTemplate.opsForValue().set("component_verify_ticket", componentVerifyTicket, 60 * 10, TimeUnit.SECONDS);
                String verifyTicket = redisTemplate.opsForValue().get("component_verify_ticket").toString();
                logger.debug("====================授权票据【ComponentVerifyTicket】:【" + verifyTicket + "】====================");
            } else {
                throw new RuntimeException("微信开放平台,第三方平台获取【验证票据】失败");
            }
        } catch (AesException e) {
            e.printStackTrace();
        }
        logger.debug("==============================结束授权事件接收URL=================================");
        return "success";
    }

提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串),微信官方提供的解密类会报错extact方法会报空指针…

下面是提供的解密类

package com.dby.advert.advert.utils.sdk;

import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import java.io.StringReader;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).  
 * <ol>  * <li>第三方回复加密消息给公众平台</li>  * <li>第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。</li>  
 * </ol> 
 * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案  
 * <ol>  
 * <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:  *
 * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>  
 * <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
 * <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>  
 * <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
 * 
 * </ol>  
 */
public class WXBizMsgCrypt {
    static Charset CHARSET = Charset.forName("utf-8");
    Base64 base64 = new Base64();
    byte[] aesKey;
    String token;
    String appId;

    /**
     * 构造函数 
     * @param token 公众平台上,开发者设置的token  
     * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey  
     * @param appId 公众平台appid 
     *
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */
    public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException {
        if (encodingAesKey.length() != 43) {
            throw new AesException(AesException.IllegalAesKey);
        }

        this.token = token;
        this.appId = appId;
        aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    // 还原4个字节的网络字节序
    int recoverNetworkBytesOrder(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }

    /**
     * 对密文进行解密.   
     * @param text 需要解密的密文  
     * @return 解密得到的明文  
     * @throws AesException aes解密失败  
     */
    String decrypt(String text) throws AesException {
        byte[] original;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.decodeBase64(text);

            // 解密
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.DecryptAESError);
        }

        String xmlContent, from_appid;
        try {
            // 去除补位字符
            byte[] bytes = PKCS7Encoder.decode(original);

            // 分离16位随机字符串,网络字节序和AppId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

            int xmlLength = recoverNetworkBytesOrder(networkOrder);

            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
            from_appid =
                    new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.IllegalBuffer);
        }

        // appid不相同的情况
        if (!from_appid.equals(appId)) {
            throw new AesException(AesException.ValidateSignatureError);
        }
        return xmlContent;

    }

    /**
     *  * 检验消息的真实性,并且获取解密后的明文.  
     * <ol>  
     * <li>利用收到的密文生成安全签名,进行签名验证</li>  
     * <li>若验证通过,则提取xml中的加密消息</li>  
     * <li>对消息进行解密</li>  
     * </ol>
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature  
     * @param timeStamp 时间戳,对应URL参数的timestamp  
     * @param nonce 随机串,对应URL参数的nonce  
     * @param postData 密文,对应POST请求的数据
     * @return 解密后的原文  
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息  
     */
    public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
            throws AesException {

        // **,公众账号的app secret
        // 提取密文
        Object[] encrypt = extract(postData);

        // 验证安全签名
        String signature = getSHA1(token, timeStamp, nonce, encrypt[1].toString());

        // 和URL中的签名比较是否相等
        // System.out.println("第三方收到URL中的签名:" + msg_sign);
        // System.out.println("第三方校验签名:" + signature);
        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }

        // 解密
        String result = decrypt(encrypt[1].toString());
        return result;
    }

    /**
     * 提取出xml数据包中的加密消息  
     * @param xmltext 待提取的xml字符串  
     * @return 提取出的加密消息字符串  
     * @throws AesException  
     */
    public static Object[] extract(String xmltext) throws AesException {
        Object[] result = new Object[3];
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);
            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader sr = new StringReader(xmltext);
            InputSource is = new InputSource(sr);
            Document document = db.parse(is);

            Element root = document.getDocumentElement();
            NodeList nodelist1 = root.getElementsByTagName("Encrypt");
            NodeList nodelist2 = root.getElementsByTagName("ToUserName");
            result[0] = 0;
            result[1] = nodelist1.item(0).getTextContent();

            //注意这里,获取ticket中的xml里面没有ToUserName这个元素,官网原示例代码在这里会报空
            //空指针,所以需要处理一下
            if (nodelist2 != null) {
                if (nodelist2.item(0) != null) {
                    result[2] = nodelist2.item(0).getTextContent();
                }
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            throw new AesException(AesException.ParseXmlError);
        }
    }

    /**
     * 用SHA1算法生成安全签名
     * @param token 票据  
     * @param timestamp 时间戳  
     * @param nonce 随机字符串  
     * @param encrypt 密文  
     * @return 安全签名  
     * @throws
     * AesException  
     */
    public static String getSHA1(String token, String timestamp, String nonce, String encrypt)
            throws AesException {
        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();
            throw new AesException(AesException.ComputeSignatureError);
        }
    }
}

接收 POST 请求后,只需直接返回字符串 success。为了加强安全性,postdata 中的 xml 将使用服务申请时的加解密 key 来进行加密,具体请见《加密解密技术方案》, 在收到推送后需进行解密(详细请见《消息加解密接入指引》)

推送内容解密后的示例:

<xml>
<AppId>some_appid</AppId>
<CreateTime>1413192605</CreateTime>
<InfoType>component_verify_ticket</InfoType>
<ComponentVerifyTicket>some_verify_ticket</ComponentVerifyTicket>
</xml>

参数说明:
微信开放平台【第三方平台】java开发总结:验证票据(component_verify_ticket)(-)
注意:

component_verify_ticket 的有效时间较 component_access_token 更长,建议保存最近可用的component_verify_ticket,在 component_access_token 过期之前可以直接使用该 component_verify_ticket 进行更新,避免出现因为 component_verify_ticket 接收失败而无法更新 component_access_token 的情况。

如有疑问请私信

上一篇:

下一篇: