Java对接微信开放平台详解

Java对接微信开放平台详解

  • 1.开放平台概述
    • 1.1开放平台定义
    • 1.2 开放平台概述
  • 2.开放平台配置
    • 2.1 配置流程说明
    • 2.2 配置具体操作
    • 2.3 授权序列图
    • 2.4 项目对接流程
  • 3.项目实现
    • 3.1 创建项目
    • 3.2 项目依赖
    • 3.3 项目配置
    • 3.4 加解密工具
    • 3.5 代码实现
  • 4.验证测试
    • 4.1 项目测试流程
    • 4.2 启动验证票据服务
    • 4.3 获取验证票据
    • 4.4获取component_access_token
    • 4.5获取pre_auth_code
    • 4.6获取链接,手动拼接html的链接
    • 4.7扫码授权
    • 4.8授权成功跳转成功页面
    • 4.9获取authorizer_access_token

1.开放平台概述

1.1开放平台定义

关于第三方平台概述,官网是这样定义的。
微信开放平台-第三方平台(简称第三方平台)开放给所有通过开发者资质认证的开发者使用。在得到公众号或小程序运营者(简称运营者)授权后,第三方平台开发者可以通过调用微信开放平台的接口能力,为公众号或小程序的运营者提供账号申请、小程序创建、技术开发、行业方案、活动营销、插件能力等全方位服务。

官网文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/Third_party_platform_appid.html

总结来说,开放平台-三方平台就是为了对公众号或者小程序进行统一管理而设定的。

1.2 开放平台概述

这里有几个概念需要了解,分别是开放平台,公众平台,小程序平台,三个不完全独立又相互不同的平台,比如开放平台是统一管理公众号与小程序的平台,公众平台是微信公众号平台,小程序平台是管理小程序以及小程序下对应的直播等功能的平台,三个平台注册的时候需要提供一个没有被其他两个平台注册的邮箱,很奇怪,明明对应一家平台却需要三个邮箱注册,开放平台能统一管理说明数据是想通的,注册邮箱不能重复说明又是独立的,这个很尴尬。

下边唠唠叨叨说了一大堆就是为了了解三个平台的区别,可能新人对此有些困惑,了解的可以直接跳过。

由于业务需要对公司不同微信公众号进行统一管理,所以要管理的是微信公众号,但是需要注意的是要对接微信开放平台,而不是对接微信公众号。

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

如果你针对微信公众号进行过开发,那么你肯定曾在微信公众号后台配置过自己的域名服务器以及密钥等信息。基于这个配置之后针对该公众进行定制化的开发,例如自动登录、个人中心、自定义二维码等。当你需要针对第二个微信公众号进行开发时,你可能仍然需要进行重复的步骤,并且配置不同的域名以及密钥等,于此同时每一次开发你需要保存公众号的账号密码,并需要获取公众号所有者的授权,不安全且繁琐。

随着需要运维公众号的数量逐步增加,需要投入的资源以及成本同步增加(不同公众号需要不同的域名以及服务器资源)。此时你可能需要一个平台,能够同时管理这么多公众号。因为无论具体的业务是什么场景,基于微信的开发必然是基于微信底层提供的各种接口进行不同业务形态的展开。为此我们可以通过抽象出功能层以及业务层来解构每个微信公众号的开发,其中功能层即微信提供的基础功能(回复消息/微信菜单管理/分组管理等),业务层即不同的业务场景。那么功能层就变成了第三方平台,一次开发供N个公众号使用,提供标准化的接口服务来满足业务的基础需求,业务层基于第三方平台进行其他更深的业务拓展,例如搭建CMS投放系统。

所谓对接微信开放平台就是将公众号授权给第三方平台进行管理,第三方平台拥有全部或部分的该公众号的接口权限,可以协助运营人员批量管理运营公众号。
微信开放平台本质上扮演的是一个对接微信的服务开发商角色,不管是公司内部还是三方方式提供微信公众号以及小程序等微信生态产品的服务。

微信文档相对比较详细,但是对于新手流程不够清晰,在此梳理记录。

2.开放平台配置

2.1 配置流程说明

整个对接流程主要分为两个部分
1.是前期首先需要开放平台相关的环境配置。
2.是通过API接口实现授权的过程,代码实现的也就是这部分功能。

注意:所有的这一切都需要可以对接外网,也就是需要内网穿透,穿透工具有很多,不花钱不稳定,这个因人而异选择吧,本文使用Sunny-Ngrok使用教程,使用很简单,可以免费使用,先参照官网,有时间出教程详解。

首先是开放平台配置相关操作。参照官方文档可以分为7步。
Java对接微信开放平台详解_第1张图片三方平台官方文档:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/getting_started/how_to_be.html

1、注册开放平台以及完成开发者资质认证
2、创建第三方平台
3、申请全网发布
4、更多操作指南请查看操作指引纵览

注意:这里需要注意的是,
1.开放平台配置相关需要提前注册申请,因为申请审核周期一般5-7天周期比较长。
2.注册开放平台完成开发者资质认证与创建三方平台两者是不同的东西,开放平台是微信官方提供的,在开放平台内创建三方平台,这个三方平台是你自己公司的注册信息,以后所有你们公司相关的公众号小程序都是绑定的你创建的三方平台上,当然每个三方平台绑定的公众号与小程序数量是有限的,这个官网有说明。
3.完成前三步骤即可完成调试功能,不用等到第四部提交审核才开始调试,创建完成三方平台,完成开发资料配置,先不要提交审核,等调试完成在审核以及全网发布,因为审核周期比较长,没上线之前也没必要。

2.2 配置具体操作

开放平台-三方平台创建完成状态
Java对接微信开放平台详解_第2张图片需要注意配置要关注的三个点
管理中心-第三方平台-详情-开发信息:
1.APPID,APPSECRET生成的时候要保存起来,尤其是APPSECRET只有生成的时候可以看,再次查看只能重置,这样之前的就会全部失效,很麻烦。
2.权限集一定要勾选上,否则接口无权限。
3.开发资料是配置重点。
(1)授权事件接收URL:接收验证票据,授权事件等消息回调接口地址。
(2)消息与事件接收URL:这个是绑定的小程序或者公众号消息事件回调接口地址。
(3)消息校验Token:这个自定义的,用此Token来校验消息,就是一个双方约定好的标识,证明是合法的。
(4)消息加解密Key:加解密密匙,这个是自定义的,公众平台可以自动生成,可以直接拿来用,用于接口调用时结果的解密参数使用。
(5)授权发起页域名:这个要注意,必须与上边授权事件,消息事件的的域名一致,这个是干嘛用的呢,就是你自己的应用页面跳转二维码之前的页面地址域名,也就是下边代码中生成授权连接的地址必须通过页面标签跳转,也就是这个页面的域名。这里顺便强调一句,生成的授权连接不能直接放到浏览器打开,否则页面提示错误:授权入口页所在域名:空。
(6)公众号开发域名:这个可以先不写,
(7)授权测试公众号/小程序列表:公众号或小程序的原始ID。
(8)白名单IP地址列表:这个特别重要,域名对应IP一定要添加白名单,否则接口没有权限访问。
Java对接微信开放平台详解_第3张图片

2.3 授权序列图

Java对接微信开放平台详解_第4张图片授权流程:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/creat_token.html

2.4 项目对接流程

1.配置授权事件URL
配置授权事件URL,用于接收验证票据,这个在开放平台配置阶段已经完成。

2.启动票据推送服务
启动ticket推送服务,该 API 用于启动ticket推送服务。
接口详细说明参照官方文档:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_verify_ticket_service.html

3.获取验证票据
验证票据(component_verify_ticket),在第三方平台创建审核通过后,微信服务器会向其 ”授权事件接收URL” 每隔 10 分钟以 POST 的方式推送 component_verify_ticket。
出于安全考虑,在第三方平台创建审核通过后,微信服务器 每隔 10 分钟会向第三方的消息接收地址推送一次 component_verify_ticket,用于获取第三方平台接口调用凭据。omponent_verify_ticket有效期为12h。】
接口详细说明参照官方文档:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_verify_ticket.html

4.获取调用凭据
第三方平台接口的调用凭据,这一步是通过验证票据获取component_access_token过程。
令牌(component_access_token)是第三方平台接口的调用凭据。令牌的获取是有限制的,每个令牌的有效期为 2 小时,请自行做好令牌的管理,在令牌快过期时(比如1小时50分),重新调用接口获取。
接口详细说明参照官方文档:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/component_access_token.html

5.获取预授权码
通过component_access_token获取预授权码的过程。
预授权码(pre_auth_code)是第三方平台方实现授权托管的必备信息,每个预授权码有效期为 1800秒。需要先获取令牌才能调用。使用过程中如遇到问题,可在开放平台服务商专区发帖交流。
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/pre_auth_code.html

6.构建授权链接
这一步要做的就是通过上一步获取到的预授权码获取授权码的过程,也是用户扫码授权的过程。
微信官方提供了三种方案,构建PC端授权链接的方法,构建移动端授权链接的方法,使用插件。
(1)构建PC端授权链接的方法
就是通过浏览器打开微信提供的链接,传入相对应的参数,即可打开二维码页面,用户通过微信客户端扫码授权以后,微信服务器自动回调redirect_uri指定的地址。即可完成授权。

PC端用户扫码授权的步骤
https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=xxxx&pre_auth_code=xxxxx&redirect_uri=xxxx&auth_type=xxx。
参数说明:
component_appid:component_appid
pre_auth_code:预授权码(预授权码有效期为 1800秒,30分钟,也就是二维码有效期30分钟)
redirect_uri:回调页面,用于接收授权码。

这里需要特别注意一下:
这里有个需要注意的点就是不能将拼接的地址直接放入浏览器打开,这时候会有错误提示,需要前端设置链接如a标签等打开,而且打开的域名地址与开放平台配置的域名地址要保持一致。

1.生成的授权连接地址不能直接放浏览器访问,否则错误提示:
请确认授权入口页所在域名,与授权后回调页所在域名相同,并且,此两者都必须与申请第三方平台时填写的授权发起页域名相同。授权入口页所在域名:空

2.必须通过自己应用页面标签形式跳转,而且应用页面域名必须与三方平台配置的授权域名保持一致,否则错误提示:
请确认授权入口页所在域名,与授权后回调页所在域名相同,并且,此两者都必须与申请第三方平台时填写的授权发起页域名相同。授权入口页所在域名:localhost

(2)构建移动端授权链接的方法
构建PC端授权链接的方法生成的二维码是微信服务器提供的二维码,而构建移动端授权链接的方法是自己生成的二维码,但是二维码生成信息是通过微信提供的链接生成,只需要用户通过微信客户端扫描即可打开授权页面,不需要再跳转微信的二维码页面。

构建移动端授权链接的方法
https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&no_scan=1&component_appid=xxxx&pre_auth_code=xxxxx&redirect_uri=xxxx&auth_type=xxx&biz_appid=xxxx#wechat_redirect

将指定参数位置替换为正确的参数,然后将替换后的链接生成二维码,仅测试可以通过在线工具生成二维码比如草料二维码生成地址,用户通过微信客户端直接扫描生成的二维码即可跳转授权页面进行授权回调。
Java对接微信开放平台详解_第5张图片扫码授权文档说明:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html

7.用户扫码授权,获取授权码
用户扫码完后,点击授权之后会进入设定的授权回调页面 redirect_url?auth_code=xxx&expires_in=600。url附带参数auth_code(授权码)和expires_in(有效期)。
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html

8.通过授权码获取授权信息
通过授权码获取授权信息,包括authorizer_access_token与authorizer_refresh_token。
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html

这里通过授权信息还可以顺便获取所授权应用的信息,比如公众号小程序基本信息。
获取授权方的帐号基本信息:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/api_get_authorizer_info.html

9.刷新授权方令牌
这一步就是刷新令牌,保持令牌有效性,比较令牌获取次数有限不能每次调用,存储缓存,定时刷新。
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/api_authorizer_token.html

3.项目实现

3.1 创建项目

创建springboot项目
项目说明,代码还有很多可以优化的地方,比如消息可以做策略,代码中的枚举类型,常量,授权信息可以放缓存等等,因为这里只做测试,可以结合测试自行优化。
为了方便测试,这些授权参数放到集合内存里面了,所以每次重启丢失,后期是要优化到redis缓存持久化的,验证票据有效期12小时所以为了测试可以写死,其他的从内存取就可以了。

另外可以有许多开源的微信开发框架可以参考学习使用,这里依赖引入微信开发框架weixin-Java-tools binarywang只是为了学习研究,并为使用。
weixin-Java-tools:https://gitee.com/itwu/weixin-java-tools#https://mp.weixin.qq.com/s/nIk_xOf6dxkhKfqq830Cuw

3.2 项目依赖


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.4version>
        <relativePath/> 
    parent>
    <groupId>com.zrjgroupId>
    <artifactId>wechatartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>wechatname>
    <description>Demo project for Spring Bootdescription>
    <properties>
        <java.version>1.8java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.1.4version>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
        
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>30.1.1-jreversion>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.6.3version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.72version>
        dependency>
        <dependency>
            <groupId>com.google.code.gsongroupId>
            <artifactId>gsonartifactId>
            <version>2.8.6version>
        dependency>
        
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.6.1version>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.6.1version>
        dependency>
        
        <dependency>
            <groupId>commons-codecgroupId>
            <artifactId>commons-codecartifactId>
            <version>1.9version>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.13.2version>
        dependency>
        <dependency>
            <groupId>org.dom4jgroupId>
            <artifactId>dom4jartifactId>
            <version>2.1.1version>
        dependency>
        
        <dependency>
            <groupId>javax.validationgroupId>
            <artifactId>validation-apiartifactId>
            <version>2.0.1.Finalversion>
        dependency>
        <dependency>
            <groupId>org.hibernate.validatorgroupId>
            <artifactId>hibernate-validatorartifactId>
            <version>6.0.13.Finalversion>
        dependency>
        <dependency>
            <groupId>org.apache.httpcomponentsgroupId>
            <artifactId>httpcoreartifactId>
            <version>4.4.14version>
        dependency>
        <dependency>
            <groupId>org.apache.httpcomponentsgroupId>
            <artifactId>httpclientartifactId>
            <version>4.5.13version>
        dependency>
        
        <dependency>
            <groupId>com.github.binarywanggroupId>
            <artifactId>weixin-java-commonartifactId>
            <version>4.1.0version>
        dependency>
        <dependency>
            <groupId>com.github.binarywanggroupId>
            <artifactId>weixin-java-mpartifactId>
            <version>4.1.0version>
        dependency>
        <dependency>
            <groupId>com.github.binarywanggroupId>
            <artifactId>weixin-java-openartifactId>
            <version>4.1.0version>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombokgroupId>
                            <artifactId>lombokartifactId>
                        exclude>
                    excludes>
                configuration>
            plugin>
        plugins>
    build>

project>

3.3 项目配置

server:
  port: 8080

spring:
  # 数据库配置
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/advertising?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    driver-class-name: com.mysql.jdbc.Driver

    hikari:
      pool-name: Retail_HikariCP #连接池名称
      minimum-idle: 10 #最小空闲连接数量
      idle-timeout: 120000 #空闲连接存活最大时间,默认600000(10分钟)
      maximum-pool-size: 20 #连接池最大连接数,默认是10
      auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true
      max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
      connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
      connection-test-query: SELECT 1
  mvc:
    view:
      prefix: /
      suffix: .html
  # redis
#  redis:
#    host: 127.0.0.1
#    port: 6379
#    database: 0
#    timeout: 10000

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: com.zrj.wechat

#showSql
logging:
  level:
    com:
      example:
        mapper : debug

#微信配置
wechat:
  appID:  wxe07a7b9ee654ff21 #公众号AppID
  secret: 43fc08b8d86c9883e0e15503fe9cc12b #公众平台配置的APPSecret
  token: zrj #公众平台配置的Token
  encodingAESKey: 123 #公众平台配置的EncodingAESKey

3.4 加解密工具

消息加解密说明:
https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Message_encryption_and_decryption.html

加解密工具下载地址:https://res.wx.qq.com/op_res/-serEQ6xSDVIjfoOHcX78T1JAYX-pM_fghzfiNYoD8uHVd3fOeC0PC_pvlg4-kmP

1.下载解压后根据对应的语言选择对应的工具类,直接打成jar包放入私服引用或者直接放入项目引用即可。
2.需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
3.这里需要注意的是加解密的时候与公众号加解密参数不同,因为返回的xml不一样,如果使用公众号的加解密会出现空指针,需要根据返回结果调整,公众号加密后xml是ToUser与Encrypt标签,开放平台的返回是AppId与Encrypt标签,其中Encrypt是加密内容。

			Element root = document.getDocumentElement();
			NodeList nodelist1 = root.getElementsByTagName("Encrypt");
			//NodeList nodelist2 = root.getElementsByTagName("ToUserName");
			NodeList nodelist2 = root.getElementsByTagName("AppId");

Java对接微信开放平台详解_第6张图片

/**
 * 针对org.apache.commons.codec.binary.Base64,
 * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
 * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
 */

/**
 * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
 * 
    *
  1. 第三方回复加密消息给公众平台
  2. *
  3. 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
  4. *
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 *
    *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. *
*/

3.5 代码实现

WechatOpService

package com.zrj.wechat.service;

import com.zrj.wechat.entity.*;

/**
 * 微信开放平台接口
 *
 * @author zrj
 * @since 2021/10/23
 **/
public interface WechatOpService {
    /**
     * 授权事件接收URL
     * 用于接收平台推送给第三方平台账号的消息与事件,如授权事件通知、component_verify_ticket等。
     * 注意,该URL的一级域名需与“消息与事件接收URL”的一级域名一致
     *
     * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param echostr   随机字符串
     * @param postdata  消息体
     * @return 如果获得只需要返回 SUCCESS
     */
    String authEvent(String signature, String timestamp, String nonce, String echostr, String encryptType, String msgSignature, String postdata);

    /**
     * 消息事件接收URL
     * 用于代授权的公众号或小程序的接收平台推送的消息与事件,该参数按规则填写(需包含/$APPID$,如www.abc.com/$APPID$/callback),实际接收消息时$APPID$将被替换为公众号或小程序AppId。
     * 注意:该URL的一级域名需与“授权事件接收URL”的一级域名一致
     *
     * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param echostr   随机字符串
     * @param postData  消息体
     * @return 如果获得只需要返回 SUCCESS
     */
    String msgEvent(String appId, String signature, String timestamp, String nonce, String openid, String echostr, String postData);

    /**
     * 启动票据推送服务
     *
     * @return
     */
    Response apiStartPushTicket();

    /**
     * 获取令牌,第三方平台接口的调用凭据
     *
     * @param componentAccessTokenReq
     * @return
     */
    Response getComponentAccessToken(ComponentAccessTokenReq componentAccessTokenReq);

    /**
     * 获取预授权码
     *
     * @param preAuthCodeReq
     * @return
     */
    Response getPreAuthCode(PreAuthCodeReq preAuthCodeReq);

    /**
     * 获取授权链接
     *
     * @return
     */
    Response getScanCodeAuthUrl();

    /**
     * 扫码授权回调
     * 用户扫码授权获取授权码,AuthAccessToken,AuthRefreshToken
     *
     * @return
     */
    Response getAuthAccessToken(String authCode, Long expiresIn);

    /**
     * 获取刷新授权公众号或小程序的接口调用凭据
     *
     * @param
     * @return Response
     */
    Response getAuthRefreshToken();
}

WechatOpServiceImpl

package com.zrj.wechat.service.impl;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zrj.wechat.aes.WXBizMsgCrypt;
import com.zrj.wechat.constants.WechatConstants;
import com.zrj.wechat.entity.*;
import com.zrj.wechat.service.WechatOpService;
import com.zrj.wechat.utils.WechatUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 微信开放平台接口实现类
 *
 * @author zrj
 * @since 2021/10/23
 **/
@Slf4j
@Service("WechatOpServiceImpl")
public class WechatOpServiceImpl implements WechatOpService {

    //三方平台APPID,这个根据实际配置,后期优化配置中心
    private static final String COMPONENT_APP_ID = "wx00b122312421433e3";
    //三方平台APPSECRET,这个根据实际配置,后期优化配置中心
    private static final String COMPONENT_APP_SECRET = "f9774124wfew1232eff8e";
    //消息校验Token,后期优化配置中心
    private static final String COMPONENT_TOKEN = "zrj";
    //消息加解密Key,后期优化配置中心
    private static final String COMPONENT_KEY = "tVNZZP2WuEJwZDF1TpIZB0vcIGjjfAcnZxUAYHvKbl6";

    //公众平台上,开发者设置的token,开发者设置的EncodingAESKey,appid
    //public static final String COMPONENT_APP_ID = "wx0e5231257a4503";
    //public static final String MP_AES_KEY = "tVNZZP2WuEJwZDF1123GjjfAcnZxUAYHvKbl6";
    //public static final String MP_COMPONENT_TOKEN = "zrj";

    //启动ticket推送服务
    private static final String API_START_PUSH_TICKET = "https://api.weixin.qq.com/cgi-bin/component/api_start_push_ticket";
    //获取令牌,第三方平台接口的调用凭据
    private static final String COMPONENT_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/component/api_component_token";
    private static final String PRE_AUTH_CODE_URL = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=COMPONENT_ACCESS_TOKEN";
    private static final String AUTHORIZER_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=COMPONENT_ACCESS_TOKEN";
    private static final String AUTHORIZER_REFRESH_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=COMPONENT_ACCESS_TOKEN";

    //微信相关参数容器,后期可以优化到缓存
    private static Map<String, Object> wechatOpMap = new ConcurrentHashMap<>();

    /**
     * 授权事件接收URL
     * 用于接收平台推送给第三方平台账号的消息与事件,如授权事件通知、component_verify_ticket等。
     * 注意,该URL的一级域名需与“消息与事件接收URL”的一级域名一致
     *
     * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param echostr   随机字符串
     * @param postdata  消息体
     * @return 如果获得只需要返回 SUCCESS
     */
    @Override
    public String authEvent(String signature, String timestamp, String nonce, String echostr, String encryptType, String msgSignature, String postdata) {
        log.info("【微信开放平台授权事件接收URL】请求参数:signature:【{}】,timestamp:【{}】,nonce:【{}】,echostr:【{}】,encryptType:【{}】,msgSignature:【{}】,postdata:【{}】", signature, timestamp, nonce, echostr, encryptType, msgSignature, postdata);
        System.out.println(postdata);

        //配置url验证
        if (StrUtil.isEmpty(postdata)) {
            log.info("【微信开放平台授权事件接收URL】消息体为空直接返回验证成功,返回随机数,echostr:{}", echostr);
            return echostr;
        }

        //接收事件url
        try {
            //这个类是微信官网提供的解密类,需要用到消息校验Token 消息加密Key和服务平台appid
            WXBizMsgCrypt pc = new WXBizMsgCrypt(COMPONENT_TOKEN, COMPONENT_KEY, COMPONENT_APP_ID);
            //加密模式:需要解密
            String xml = pc.decryptMsg(msgSignature, timestamp, nonce, postdata);
            log.info("【微信开放平台授权事件接收URL】解密后xml:【{}】", xml);

            //将xml转为map
            Map<String, String> resultMap = WechatUtils.xmlToMap(xml);
            log.info("【微信开放平台授权事件接收URL】resultMap:【{}】", JSON.toJSONString(resultMap));

            //授权事件类型
            String infotype = MapUtil.getStr(resultMap, "InfoType");
            if ("component_verify_ticket".equals(infotype)) {
                log.info("【微信开放平台授权事件接收URL】票据类型事件");

                //获取验证票据
                String componentVerifyTicket = MapUtil.getStr(resultMap, "ComponentVerifyTicket");
                log.info("【微信开放平台授权事件接收URL】三方平台获取验证票据,componentVerifyTicket:【{}】", componentVerifyTicket);

                if (StrUtil.isEmpty(componentVerifyTicket)) {
                    log.warn("【微信开放平台授权事件接收URL】三方平台获取验证票据失败,验证票据为空");
                    return WechatConstants.TICKET_SUCCESS;
                }

                //将验证票据放入容器,后期可以优化到缓存Redis
                wechatOpMap.put(COMPONENT_APP_ID + "ComponentVerifyTicket", componentVerifyTicket);

                log.info("【微信开放平台授权事件接收URL】验证票据处理成功");
            } else if ("authorized".equals(infotype)) {
                log.info("【微信开放平台授权事件接收URL】授权类型事件");

                //微信开放平台授权事件参数
                String AuthorizerAppid = MapUtil.getStr(resultMap, "AuthorizerAppid");
                String AuthorizationCode = MapUtil.getStr(resultMap, "AuthorizationCode");
                String AuthorizationCodeExpiredTime = MapUtil.getStr(resultMap, "AuthorizationCodeExpiredTime");
                log.info("【微信开放平台授权事件接收URL】授权类型事件,AuthorizerAppid:【{}】,AuthorizationCode:【{}】", AuthorizerAppid, AuthorizationCode);

                //获取授权
                getAuthAccessToken(AuthorizationCode, Long.valueOf(AuthorizationCodeExpiredTime));

            } else if ("unauthorized".equals(infotype)) {
                log.info("【微信开放平台授权事件接收URL】用户取消授权事件");
            } else if ("updateauthorized".equals(infotype)) {
                log.info("【微信开放平台授权事件接收URL】授权变更事件");
            } else {
                log.info("【微信开放平台授权事件接收URL】未知类型,暂不处理");
            }

            return WechatConstants.TICKET_SUCCESS;
        } catch (Exception e) {
            log.info("【微信开放平台授权事件接收URL】处理异常:{}", e);
            return WechatConstants.TICKET_SUCCESS;
        }
    }

    /**
     * 消息事件接收URL
     * 用于代授权的公众号或小程序的接收平台推送的消息与事件,该参数按规则填写(需包含/$APPID$,如www.abc.com/$APPID$/callback),实际接收消息时$APPID$将被替换为公众号或小程序AppId。
     * 注意:该URL的一级域名需与“授权事件接收URL”的一级域名一致
     *
     * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param echostr   随机字符串
     * @param postData  消息体
     * @return 如果获得只需要返回 SUCCESS
     */
    @Override
    public String msgEvent(String appId, String signature, String timestamp, String nonce, String openid, String echostr, String postData) {
        log.info("【微信开放平台授权事件接收URL】请求参数:appId:【{}】,signature:【{}】,timestamp:【{}】,nonce:【{}】,openid:【{}】,echostr:【{}】,postData:【{}】", appId, timestamp, nonce, openid, echostr, signature, postData);

        log.info("【微信开放平台消息事件接收URL】暂不处理消息事件!");
        return WechatConstants.TICKET_SUCCESS;
    }

    /**
     * 1.启动票据推送服务
     */
    @Override
    public Response apiStartPushTicket() {
        log.info("【微信开放平台启动票据推送服务】");

        try {
            //构建请求参数
            Map<String, Object> paramMap = new HashMap<>(16);
            //平台型第三方平台的appid
            paramMap.put("component_appid", COMPONENT_APP_ID);
            //平台型第三方平台的APPSECRET
            paramMap.put("component_secret", COMPONENT_APP_SECRET);

            //执行请求,获取结果
            log.info("【微信开放平台启动票据推送服务】请求参数:【{}】,请求地址:【{}】", JSON.toJSONString(paramMap), API_START_PUSH_TICKET);
            String result = HttpUtil.post(API_START_PUSH_TICKET, JSON.toJSONString(paramMap));
            JSONObject resultJsonObject = JSON.parseObject(result);
            log.warn("【微信开放平台启动票据推送服务】响应结果:【{}】", resultJsonObject);

            if (0 != resultJsonObject.getIntValue("errcode")) {
                log.info("【微信开放平台启动票据推送服务失败】");
                return Response.fail("微信开放平台启动票据推送服务失败");
            }

            log.info("【微信开放平台启动票据推送服务成功】");
            return Response.success("微信开放平台启动票据推送服务成功", resultJsonObject);
        } catch (Exception e) {
            log.info("【微信开放平台启动票据推送服务异常】处理异常:{}", e);
            return Response.fail("微信开放平台启动票据推送服务异常");
        }
    }

    /**
     * 2.获取令牌,第三方平台接口的调用凭据
     * 获得component_verify_ticket后,按照获取第三方平台 component_access_token 接口文档,调用接口获取component_access_token
     * component_access_token有效期2h,当遭遇异常没有及时收到component_verify_ticket时,建议以上一次可用的component_verify_ticket继续生成component_access_token。避免出现因为 component_verify_ticket 接收失败而无法更新 component_access_token 的情况。
     */
    @Override
    public Response getComponentAccessToken(ComponentAccessTokenReq componentAccessTokenReq) {
        log.info("【微信开放平台获取令牌】");

        //将验证票据放入容器,后期可以优化到缓存Redis
        String ComponentVerifyTicket = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "ComponentVerifyTicket");

        //构建请求参数,测试用
        componentAccessTokenReq.setComponent_appid(COMPONENT_APP_ID);
        componentAccessTokenReq.setComponent_appsecret(COMPONENT_APP_SECRET);
        //componentAccessTokenReq.setComponent_verify_ticket(ComponentVerifyTicket);
        componentAccessTokenReq.setComponent_verify_ticket("ticket@@@oZJEAXrC57sqA0Gjgzb1V7ZHZ3A9VKzgovwiI70HAWgtLgkLdkVgWAyFQ6lu3EDe1hYF3-tgJSZkogvukXIewg");

        try {
            String jsonString = JSON.toJSONString(componentAccessTokenReq);
            log.warn("【微信开放平台获取令牌】请求参数:【{}】,请求路径:【{}】", jsonString, COMPONENT_ACCESS_TOKEN_URL);
            JSONObject jsonObject = WechatUtils.doPoststr(COMPONENT_ACCESS_TOKEN_URL, jsonString);
            log.warn("【微信开放平台获取令牌】响应结果:【{}】", jsonObject);

            if (jsonObject == null) {
                log.warn("【微信开放平台获取令牌】微信开放平台获取令牌失败,返回结果为空");
                return Response.fail("微信开放平台获取令牌失败");
            }

            String component_access_token = jsonObject.getString("component_access_token");
            Long expires_in = jsonObject.getLong("expires_in");
            if (StrUtil.isEmpty(component_access_token)) {
                log.warn("获取令牌,第三方平台接口的调用凭据失败,返回结果为空,component_access_token:{}", component_access_token);
            }

            //将令牌放入容器,后期可以优化到缓存Redis
            wechatOpMap.put(COMPONENT_APP_ID + "_component_access_token", component_access_token);

            ComponentAccessTokenResp componentAccessTokenResp = ComponentAccessTokenResp.builder()
                    .component_access_token(component_access_token)
                    .expires_in(expires_in)
                    .build();

            log.info("【微信开放平台获取令牌成功】");
            return Response.success("微信开放平台获取令牌成功", componentAccessTokenResp);
        } catch (Exception e) {
            log.info("【微信开放平台获取令牌异常】处理异常:{}", e);
            return Response.fail("微信开放平台获取令牌异常");
        }
    }

    /**
     * 3.获取预授权码
     * 获得component_access_token后,按照获取预授权码 pre_auth_code接口文档 ,调接口获取pre_auth_code
     * 用于生成扫码授权二维码或者链接需要的pre_auth_code
     *
     * @param preAuthCodeReq
     * @return
     */
    @Override
    public Response getPreAuthCode(PreAuthCodeReq preAuthCodeReq) {
        log.info("【微信开放平台获取预授权码】");

        //将验证票据放入容器,后期可以优化到缓存Redis
        String component_access_token = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_component_access_token");
        //构建请求参数,测试用
        preAuthCodeReq.setComponent_appid(COMPONENT_APP_ID);
        preAuthCodeReq.setComponent_access_token(component_access_token);

        try {
            String jsonString = JSON.toJSONString(preAuthCodeReq);
            String preAuthCodeUrl = WechatUtils.replaceComponentAccessToken(PRE_AUTH_CODE_URL, component_access_token);
            log.warn("【微信开放平台获取预授权码】请求参数:【{}】,请求路径:【{}】", jsonString, preAuthCodeUrl);
            JSONObject jsonObject = WechatUtils.doPoststr(preAuthCodeUrl, jsonString);
            log.warn("【微信开放平台获取预授权码】响应结果:【{}】", JSON.toJSONString(jsonObject));
            if (jsonObject == null) {
                log.warn("【微信开放平台获取预授权码】获取预授权码失败,返回结果为空");
                return Response.fail("获取预授权码失败,返回结果为空");
            }
            //获取预授权码
            String preAuthCode = jsonObject.getString("pre_auth_code");
            Long expiresIn = jsonObject.getLong("expires_in");
            log.warn("【微信开放平台获取预授权码】预授权码:【{}】,有效时间:【{}】", preAuthCode, expiresIn);

            //将预授权码放入容器,后期可以优化到缓存Redis
            wechatOpMap.put(COMPONENT_APP_ID + "_pre_auth_code", preAuthCode);

            PreAuthCodeResp preAuthCodeResp = PreAuthCodeResp.builder()
                    .pre_auth_code(preAuthCode)
                    .expires_in(expiresIn)
                    .build();

            log.info("【微信开放平台获取预授权码成功】");
            return Response.success("微信开放平台获取预授权码成功", preAuthCodeResp);
        } catch (Exception e) {
            log.info("【微信开放平台获取预授权码异常】处理异常:{}", e);
            return Response.fail("微信开放平台获取预授权码异常");
        }
    }

    /**
     * 获取扫码授权链接
     * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html
     * 1、构建PC端授权链接的方法
     * https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=xxxx&pre_auth_code=xxxxx&redirect_uri=xxxx&auth_type=xxx。
     * component_appid 	是 	第三方平台方 appid
     * pre_auth_code 	是 	预授权码
     * redirect_uri 	是 	回调 URI(插件版授权页面,无该参数)
     * auth_type 	    是 	要授权的帐号类型:1 则商户点击链接后,手机端仅展示公众号、2 表示仅展示小程序,3 表示公众号和小程序都展示。如果为未指定,则默认小程序和公众号都展示。第三方平台开发者可以使用本字段来控制授权的帐号类型。
     * biz_appid 	    否 	指定授权唯一的小程序或公众号
     * 2、构建移动端授权链接的方法
     * 3、使用插件
     *
     * @return
     */
    @Override
    public Response getScanCodeAuthUrl() {
        log.info("【微信开放平台获取扫码授权链接】");

        try {
            String componentAppId = COMPONENT_APP_ID;
            String preAuthCode = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_pre_auth_code");
            String redirectUri = "http://zrj.free.idcfengye.com/wechat/op/getAuthAccessToken";
            String authType = "3";
            String QRLink = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=COMPONENT_APPID&pre_auth_code=PRE_AUTH_CODE&redirect_uri=REDIRECT_URI&auth_type=AUTH_TYPE";
            log.info("【微信开放平台获取扫码授权链接】QRLink:【{}】", QRLink);

            String scanCodeAuthUrl = QRLink.replaceAll("COMPONENT_APPID", componentAppId).replaceAll("PRE_AUTH_CODE", preAuthCode)
                    .replaceAll("REDIRECT_URI", redirectUri)
                    .replaceAll("AUTH_TYPE", authType);

            log.info("【微信开放平台获取扫码授权链接成功】");
            return Response.success("微信开放平台获取扫码授权链接成功", scanCodeAuthUrl);
        } catch (Exception e) {
            log.info("【微信开放平台获取扫码授权链接异常】处理异常:{}", e);
            return Response.fail("微信开放平台获取扫码授权链接异常");
        }
    }

    /**
     * 扫码授权回调
     * 用户扫码授权获取授权码,AuthAccessToken,AuthRefreshToken
     * 4.获得pre_auth_code后,按照授权技术流程说明文档 ,引导用户授权后获取authorization_code
     * 5.获得authorization_code后,按照使用授权码换取公众号或小程序的接口调用凭据和授权信息 接口文档 ,调接口获取authorizer_refresh_token
     *
     * @param authCode      授权码
     * @param authExpiresIn 有效期
     * @return
     */
    @Override
    public Response getAuthAccessToken(String authCode, Long authExpiresIn) {
        log.info("【微信开放平台用户扫码授权】请求参数,authCode:【{}】,authExpiresIn:【{}】", authCode, authExpiresIn);

        try {
            //缓存获取鉴权信息
            //String componentAccessToken = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_component_access_token");
            String componentAccessToken = "50_E817-W7oDU30_8PA_CIAsJgE1rtXaiPDzzHs3FbNSY9PYvvdBc7N8JaRwbgJi_U_AVnUQXBvs2nu-c3p8prouRZzqHBNfgsZfTwoy48G7_rF5y8nK0UVu-JD4PVN8MS6qDl8PSv0i-VNov5nAQBfAHAOEP";

            AccessTokenReq authTokenReq = new AccessTokenReq();
            //第三方平台 appid
            authTokenReq.setComponent_appid(COMPONENT_APP_ID);
            //授权码, 会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
            authTokenReq.setAuthorization_code(authCode);
            String jsonString = JSON.toJSONString(authTokenReq);

            String accessUrl = WechatUtils.replaceComponentAccessToken(AUTHORIZER_ACCESS_TOKEN_URL, componentAccessToken);
            log.warn("【微信开放平台用户扫码授权】使用授权码获取授权信息,请求参数:【{}】,请求路径:【{}】", jsonString, accessUrl);
            JSONObject jsonObject = WechatUtils.doPoststr(accessUrl, jsonString);
            log.warn("【微信开放平台用户扫码授权】使用授权码获取授权信息,响应结果:【{}】", JSON.toJSONString(jsonObject));
            if (jsonObject == null) {
                log.warn("【微信开放平台用户扫码授权】使用授权码获取授权信息失败,返回结果为空");
                return Response.fail("使用授权码获取授权信息失败,返回结果为空");
            }

            JSONObject authorizationInfo = jsonObject.getJSONObject("authorization_info");
            if (authorizationInfo == null) {
                log.warn("【微信开放平台用户扫码授权】使用授权码获取授权信息失败,返回结果为空");
                return Response.fail("使用授权码获取授权信息失败,返回结果为空");
            }

            Long expiresIn = authorizationInfo.getLong("expires_in");
            String authorizerAppId = authorizationInfo.getString("authorizer_appid");
            String authorizerAccessToken = authorizationInfo.getString("authorizer_access_token");
            String authorizerRefreshToken = authorizationInfo.getString("authorizer_refresh_token");

            //缓存授权信息,,通过授权方appid拼接,保证key值唯一,后期可以优化到缓存Redis
            wechatOpMap.put(COMPONENT_APP_ID + "_authorizer_access_token", authorizerAccessToken);
            wechatOpMap.put(COMPONENT_APP_ID + "_authorizer_refresh_token", authorizerRefreshToken);
            wechatOpMap.put(COMPONENT_APP_ID + "_authorizerAppId", authorizerAppId);

            log.info("【微信开放平台用户扫码授权】使用授权码获取授权信息成功");
            return Response.success("【微信开放平台用户扫码授权】使用授权码获取授权信息成功", jsonObject);
        } catch (Exception e) {
            log.info("【微信开放平台用户扫码授权异常】处理异常:{}", e);
            return Response.fail("微信开放平台用户扫码授权异常");
        }
    }

    /**
     * 6.获取刷新授权公众号或小程序的接口调用凭据
     * 刷新令牌可以通过定时任务处理
     * 获得authorizer_refresh_token后,按照获取/刷新授权公众号或小程序的接口调用凭据 接口文档 ,调接口获取authorizer_access_token
     */
    @Override
    public Response getAuthRefreshToken() {
        log.info("【微信开放平台刷新凭据】获取刷新授权公众号或小程序的接口调用凭据");

        try {
            //缓存获取鉴权信息
            String authorizerAccessToken = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_authorizer_access_token");
            String authorizerRefreshToken = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_authorizer_refresh_token");
            String authorizerAppId = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_authorizerAppId");
            //String componentAccessToken = MapUtil.getStr(wechatOpMap, COMPONENT_APP_ID + "_component_access_token");
            String componentAccessToken = "50_E817-W7oDU30_8PA_CIAsJgE1rtXaiPDzzHs3FbNSY9PYvvdBc7N8JaRwbgJi_U_AVnUQXBvs2nu-c3p8prouRZzqHBNfgsZfTwoy48G7_rF5y8nK0UVu-JD4PVN8MS6qDl8PSv0i-VNov5nAQBfAHAOEP";

            // 构建请求参数
            RefreshTokenReq refreshTokenReq = new RefreshTokenReq();
            refreshTokenReq.setComponent_appid(COMPONENT_APP_ID);
            refreshTokenReq.setAuthorizer_appid(authorizerAppId);
            refreshTokenReq.setAuthorizer_refresh_token(authorizerRefreshToken);
            String jsonString = JSON.toJSONString(refreshTokenReq);

            String refreshUrl = WechatUtils.replaceComponentAccessToken(AUTHORIZER_REFRESH_TOKEN_URL, componentAccessToken);
            log.warn("【微信开放平台刷新凭据】请求参数:【{}】,请求路径:【{}】", jsonString, refreshUrl);
            JSONObject jsonObject = WechatUtils.doPoststr(refreshUrl, jsonString);
            log.warn("【微信开放平台刷新凭据】响应结果:【{}】", JSON.toJSONString(jsonObject));

            if (jsonObject == null) {
                log.warn("【微信开放平台刷新凭据】使用授权码获取授权信息失败,返回结果为空");
                return Response.fail("使用授权码获取授权信息失败");
            }

            Long expiresInNew = jsonObject.getLong("expires_in");
            String authorizerAppIdNew = refreshTokenReq.getAuthorizer_appid();
            String authorizerAccessTokenNew = jsonObject.getString("authorizer_access_token");
            String authorizerRefreshTokenNew = jsonObject.getString("authorizer_refresh_token");

            //缓存授权信息,覆盖之前的token
            wechatOpMap.put(COMPONENT_APP_ID + "_authorizer_access_token", authorizerAccessTokenNew);
            wechatOpMap.put(COMPONENT_APP_ID + "_authorizer_refresh_token", authorizerRefreshTokenNew);

            log.info("【微信开放平台刷新凭据成功】");
            return Response.success("微信开放平台刷新凭据成功", jsonObject);
        } catch (Exception e) {
            log.info("【微信开放平台刷新凭据异常】处理异常:{}", e);
            return Response.fail("微信开放平台刷新凭据异常");
        }
    }

}

WechatUtils

package com.zrj.wechat.utils;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

/**
 * 微信工具类
 *
 * @author zrj
 * @since 2021/9/22
 **/
@Slf4j
public class WechatUtils {

    private static final String AUTHORIZER_ACCESS_TOKEN = "ACCESS_TOKEN";
    private static final String COMPONENT_ACCESS_TOKEN = "COMPONENT_ACCESS_TOKEN";

    /**
     * 获取授权token
     */
    public static String getAuthorizerAccessToken() {
        // 从缓存获取授权信息,使用授权码获取授权信息
        String authorizerAccessToken = "";
        return authorizerAccessToken;
    }

    /**
     * 替换请求URL的COMPONENT_ACCESS_TOKEN
     */
    public static String replaceComponentAccessToken(String url, String componentAccessToken) {
        if (StrUtil.isEmpty(componentAccessToken)) {
            return url;
        }
        return url.replaceAll(COMPONENT_ACCESS_TOKEN, componentAccessToken);
    }

    /**
     * 替换请求URL的AUTHORIZER_ACCESS_TOKEN
     */
    public static String replaceAuthorizerAccessToken(String url, String authorizerAccessToken) {
        return url.replaceAll(AUTHORIZER_ACCESS_TOKEN, authorizerAccessToken);
    }

    /**
     * 校验响应信息
     */
    public static boolean validWechatResult(JSONObject jsonObject) {
        if (jsonObject == null) {
            log.error("执行失败,响应结果为空,jsonObject:{}", jsonObject);
            return false;
        }

        int errcode = jsonObject.getIntValue("errcode");
        String errmsg = jsonObject.getString("errmsg");
        if (0 != errcode) {
            log.error("执行失败,响应信息 errcode:{} errmsg:{}", errcode, errmsg);
            return false;
        }
        log.error("执行成功,响应信息 errcode:{} errmsg:{}", errcode, errmsg);
        return true;
    }

    /**
     * 处理Get请求
     *
     * @param url
     * @return
     */
    public static JSONObject doGetstr(String url) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);
        JSONObject jsonObject = null;
        try {
            CloseableHttpResponse response = httpclient.execute(httpGet);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                String result = EntityUtils.toString(entity);
                //jsonObject =JSONObject.fromObject(result);
                jsonObject = JSONObject.parseObject(result);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return jsonObject;
    }

    /**
     * 处理Post请求
     *
     * @param url    请求url
     * @param outStr 请求字符串
     * @return JSONObject
     */
    public static JSONObject doPoststr(String url, String outStr) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(url);
        JSONObject jsonObject = null;
        try {
            httpPost.setEntity(new StringEntity(outStr, "utf-8"));
            CloseableHttpResponse response = httpclient.execute(httpPost);
            String result = EntityUtils.toString(response.getEntity(), "utf-8");
            //jsonObject =JSONObject.fromObject(result);
            jsonObject = JSONObject.parseObject(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return jsonObject;
    }

    /**
     * xml转map
     *
     * @param xml 要转换的xml字符串
     * @return 转换成map后返回结果
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String xml) throws Exception {
        Map<String, String> respMap = new HashMap<>(16);
        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 Exception
     */
    public static String mapToXml(Map<String, Object> map) throws Exception {
        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 Element
     * @param map  Map
     * @return
     */
    private static Element mapToXml(Element root, Map<String, Object> map) {
        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;
    }

}

WechatOpController

package com.zrj.wechat.controller;

import com.zrj.wechat.entity.ComponentAccessTokenReq;
import com.zrj.wechat.entity.PreAuthCodeReq;
import com.zrj.wechat.entity.Response;
import com.zrj.wechat.service.WechatOpService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * 微信开放平台服务
 *
 * @author zrj
 * @since 2021/10/23
 **/
@Slf4j
@RestController
@RequestMapping("/wechat/op")
@Api(tags = "微信开放平台服务", description = "微信开放平台服务")
public class WechatOpController {
    @Resource
    private WechatOpService wechatOpService;

    /**
     * 微信开放平台测试服务
     */
    @GetMapping("/optest")
    @ApiOperation(value = "微信开放平台测试服务", notes = "测试方法的备注说明", httpMethod = "GET")
    public Response optest() {
        log.info("【微信开放平台测试服务测试成功】");
        return Response.success("微信开放平台测试服务测试成功", null);
    }

    /**
     * 跳转页面,打开授权连接只能通过页面标签,直接放浏览器打开异常
     * 直接放浏览器打开异常:错误 请确认授权入口页所在域名,与授权后回调页所在域名相同,并且,此两者都必须与申请第三方平台时填写的授权发起页域名相同。授权入口页所在域名:空
     * 访问页面:http://zrj.free.idcfengye.com/hello.html
     * 通过连接访问授权连接,管理员扫码授权确认之后,授权页会自动跳转进入回调 URI,并在 URL 参数中返回授权码和过期时间(redirect_url?auth_code=xxx&expires_in=600)
     */
    @GetMapping("/hello")
    @ApiOperation(value = "微信开放平台扫码授权跳转页面")
    public String index() {
        log.info("【微信开放平台扫码授权跳转页面】");
        return "hello";
    }

    /**
     * 微信开放平台授权事件接收URL
     * 用于接收平台推送给第三方平台账号的消息与事件,如授权事件通知、component_verify_ticket等。
     * 注意,该URL的一级域名需与“消息与事件接收URL”的一级域名一致
     * 开放平台-管理中心-第三方平台-详情-开发信息-开发资料
     * https://open.weixin.qq.com/wxaopen/createThirdAccount/modifyDevInfo?appId=wx00bc5c32f45d33e3&token=5d50c33a346c0e6d4334a4f24cd3c19a97f9957c
     * 

* 1、配置授权事件URL,用于接收component_verify_ticket * 出于安全考虑,【在第三方平台创建审核通过后】,微信服务器 每隔 10 分钟会向第三方的消息接收地址推送一次 component_verify_ticket,用于获取第三方平台接口调用凭据。omponent_verify_ticket有效期为12h * * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数 * @param timestamp 时间戳 * @param nonce 随机数 * @param echostr 随机字符串 * @param postdata 消息体 * @return java.lang.String */ @RequestMapping("/authEvent") @ApiOperation(value = "微信开放平台授权事件接收URL") public String authEvent(@RequestParam(name = "signature") String signature, @RequestParam(name = "timestamp") String timestamp, @RequestParam(name = "nonce") String nonce, @RequestParam(name = "echostr", required = false) String echostr, @RequestParam(name = "encrypt_type", required = false) String encryptType, @RequestParam(name = "msg_signature", required = false) String msgSignature, @RequestBody(required = false) String postdata) { return wechatOpService.authEvent(signature, timestamp, nonce, echostr, encryptType, msgSignature, postdata); } /** * 微信开放平台消息事件接收URL * 用于代授权的公众号或小程序的接收平台推送的消息与事件,该参数按规则填写(需包含/$APPID$,如www.abc.com/$APPID$/callback),实际接收消息时$APPID$将被替换为公众号或小程序AppId。 * 注意:该URL的一级域名需与“授权事件接收URL”的一级域名一致 * 开放平台-管理中心-第三方平台-详情-开发信息-开发资料 * https://open.weixin.qq.com/wxaopen/createThirdAccount/modifyDevInfo?appId=wx00bc5c32f45d33e3&token=5d50c33a346c0e6d4334a4f24cd3c19a97f9957c * * @param signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数 * @param timestamp 时间戳 * @param nonce 随机数 * @param echostr 随机字符串 * @param postData 消息体 * @return java.lang.String */ @RequestMapping("/msgEvent/{APPID}") @ApiOperation(value = "微信开放平台消息事件接收URL") public String msgEvent(@PathVariable(name = "APPID") String appId, @RequestParam(name = "signature") String signature, @RequestParam(name = "timestamp") String timestamp, @RequestParam(name = "nonce") String nonce, @RequestParam(name = "openid", required = false) String openid, @RequestParam(name = "echostr", required = false) String echostr, @RequestBody(required = false) String postData) { return wechatOpService.msgEvent(appId, signature, timestamp, nonce, openid, echostr, postData); } /** * 启动票据推送服务 * * @param * @return Response */ @GetMapping("/apiStartPushTicket") @ApiOperation(value = "启动票据推送服务", notes = "微信鉴权说明", httpMethod = "GET") public Response apiStartPushTicket() { return wechatOpService.apiStartPushTicket(); } /** * 获取令牌 * * @param * @return Response */ @PostMapping("/getComponentAccessToken") @ApiOperation("获取令牌第三方平台接口的调用凭据") public Response getComponentAccessToken(@RequestBody ComponentAccessTokenReq componentAccessTokenReq) { return wechatOpService.getComponentAccessToken(componentAccessTokenReq); } /** * 获取预授权码 * * @param preAuthCodeReq * @return Response */ @ApiOperation("获取预授权码") @PostMapping("/getPreAuthCode") public Response getPreAuthCode(@RequestBody PreAuthCodeReq preAuthCodeReq) { return wechatOpService.getPreAuthCode(preAuthCodeReq); } /** * 获取授权链接 * * @return */ @ApiOperation("获取授权链接") @PostMapping("/getScanCodeAuthUrl") public Response getScanCodeAuthUrl() { return wechatOpService.getScanCodeAuthUrl(); } /** * 授权回调接口 * * @return */ @ApiOperation("扫码授权回调") @GetMapping("/getAuthAccessToken") //public Response getAuthAccessToken(@RequestParam("auth_code") String authCode, // @RequestParam("expires_in") Long expiresIn) { // return wechatOpService.getAuthAccessToken(authCode, expiresIn); //} public String getAuthAccessToken(@RequestParam("auth_code") String authCode, @RequestParam("expires_in") Long expiresIn) { //授权处理 Response authAccessToken = wechatOpService.getAuthAccessToken(authCode, expiresIn); if ("200".equals(authAccessToken.getCode())){ return "恭喜您,授权成功!"; } return "授权失败,请稍后重试!"; } /** * 获取刷新授权公众号或小程序的接口调用凭据 * * @return */ @ApiOperation("获取刷新授权公众号或小程序的接口调用凭据") @PostMapping("/getAuthRefreshToken") public Response getAuthRefreshToken() { return wechatOpService.getAuthRefreshToken(); } }

hello.html
页面路径:resources-static-hello.html
页面作用:用来跳转授权连接,前面多次提到授权域名,必须与三方平台配置一致。

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    <h1>我是一个html页面h1>
    br><a href="url">Link texta>
    br><a href="https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=wx00bc5c32f45d33e3&pre_auth_code=preauthcode@@@_k4N8hq2SN0WoR_fVFEFiXJ-W4gLVBrQadelustfo8nFB0I5HJjEugVKIkTLLQKbi1O_imQsSjlYvvPR0o_9xQ&redirect_uri=http://zrj.free.idcfengye.com/wechat/op/getAuthAccessToken&auth_type=3">授权页new!!!a>
    br>
body>
html>

4.验证测试

4.1 项目测试流程

项目结合了swagger,获取票据是微信服务器每十分钟推送一次,可以通过swagger页面手动获取。component_accesstoken,pre_auth_code,然后手动拼接html的链接,扫码授权以后自动获取auth_code,auth_access_token,auth_refresh_token。

启动项目状态:
Java对接微信开放平台详解_第7张图片

4.2 启动验证票据服务

手动调用验证票据服务
Java对接微信开放平台详解_第8张图片

【微信开放平台启动票据推送服务】
【微信开放平台启动票据推送服务】请求参数:【{"component_appid":"wx00bc5c32f45d33e3","component_secret":"f9774107e069fedb35f1989a842eff8e"}】,请求地址:【https://api.weixin.qq.com/cgi-bin/component/api_start_push_ticket】
【微信开放平台启动票据推送服务】响应结果:【{"errcode":0,"errmsg":"in a normal state"}】
【微信开放平台启动票据推送服务成功】

4.3 获取验证票据

启动项目,静等10分钟,前提外网一定是通的。

【微信开放平台授权事件接收URL】请求参数:signature:【a828d0e8fd76048586f72f3b2147ce1ca4ccae40】,timestamp:【1635314879】,nonce:【946485952】,echostr:【null】,encryptType:【aes】,msgSignature:【ef270bf5509281cea255fe83eb08785f788f38a6】,
解密前xml:

    
    

解密后xml:

	
	1635312459
	
	

【微信开放平台授权事件接收URL】三方平台获取验证票据,componentVerifyTicket:【ticket@@@G25CcKX48Zunvj-eIf9-ndA】

4.4获取component_access_token

Java对接微信开放平台详解_第9张图片

4.5获取pre_auth_code

Java对接微信开放平台详解_第10张图片

4.6获取链接,手动拼接html的链接

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    <h1>我是一个html页面h1>
    br><a href="url">Link texta>
    br><a href="https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=wx00bc5c32f45d33e3&pre_auth_code=preauthcode@@@UW3rLA3d2qFKeV6-fn9GDyMnwS9s-pVhoVmNpDn-CuTqqCoN0q_7AYq5o4HfisgI6_ylFOSSmSJ-JuXRZ7A2uA&redirect_uri=http://zrj.free.idcfengye.com/wechat/op/getAuthAccessToken&auth_type=3">授权页new!!!a>
    br>
body>
html>

访问地址
http://zrj.free.idcfengye.com/hello.html
Java对接微信开放平台详解_第11张图片跳转二维码页面

https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=wx00bc5c32f45d33e3&pre_auth_code=preauthcode@@@UW3rLA3d2qFKeV6-fn9GDyMnwS9s-pVhoVmNpDn-CuTqqCoN0q_7AYq5o4HfisgI6_ylFOSSmSJ-JuXRZ7A2uA&redirect_uri=http://zrj.free.idcfengye.com/wechat/op/getAuthAccessToken&auth_type=3

Java对接微信开放平台详解_第12张图片

4.7扫码授权

Java对接微信开放平台详解_第13张图片

4.8授权成功跳转成功页面

http://zrj.free.idcfengye.com/wechat/op/getAuthAccessToken?auth_code=queryauthcode@@@feL54abPgUcld7kXlXZhigQf6SIqh6cwpr_qjSIotTwECicfukoNM6S6OLgy-96UfBZmnyIW0T6qN1gaqWUiNA&expires_in=3600

在这里插入图片描述

4.9获取authorizer_access_token

通过扫码授权回调获取授权信息

【微信开放平台用户扫码授权】请求参数,authCode:【queryauthcode@@@feL54abPgUcld7kXlXZhigQf6SIqh6cwpr_qjSIotTwECicfukoNM6S6OLgy-96UfBZmnyIW0T6qN1gaqWUiNA】,authExpiresIn:【3600】
【微信开放平台用户扫码授权】使用授权码获取授权信息,请求参数:【{"authorization_code":"queryauthcode@@@feL54abPgUcld7kXlXZhigQf6SIqh6cwpr_qjSIotTwECicfukoNM6S6OLgy-96UfBZmnyIW0T6qN1gaqWUiNA","component_appid":"wx00bc5c32f45d33e3"}】,请求路径:【https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=50_HCfoQojsKmvk-k4Xlz9m3VJlz4-vixoI_yZyVW2yQ86vnxxPLxcyelg_WvorFqsLe0-Ij-5DQIiSVFBmlOV1h8bVUMyMqToStrv49I8DWeY_cKobuoUNu1h_4ErmJLojL8K0aLJ9EZDgZ6k-OQUjACABDH】
【微信开放平台用户扫码授权】使用授权码获取授权信息,响应结果:【{"authorization_info":{
"authorizer_appid":"wx0e5c56c1257a4503",
"authorizer_access_token":"50_hncB_7J3SUPjAJDFHC", "authorizer_refresh_token":"refreshtoken@@@SureygUvtG9Y",
"expires_in":7200,
"func_info":[{"funcscope_category":{"id":1}}]】
【微信开放平台用户扫码授权】使用授权码获取授权信息成功

你可能感兴趣的:(服务架构,Java对接微信开放平台)