Springboot微信公众号开发入门流程(校验签名、access_token获取、生成带参二维码、发送文字、图文消息、被动回复消息、图文消息静默跳转)


微信公众号简介

1.微信公众号简介

微信公众号分为服务号、订阅号、企业号,订阅号可以个人申请,服务号和企业号要有企业资质才可以。

我们所说的微信公众号开发指的是订阅号和服务号。关于订阅号和服务号的区别,官方是这样解释的

  • 服务号**:主要偏向于服务交互(功能类似12315,114,银行,提供绑定信息,服务交互),每月可群发4条消息;服务号**适用人群:**媒体、企业、政府或其他组织。
  • 订阅号**:主要偏向于为用户传达资讯,(功能类似报纸杂志,为用户提供新闻信息或娱乐趣事),每天可群发1条消息;订阅号适用人群:个人、媒体、企业、政府或其他组织。

2.微信公众号交互模式

在这里插入图片描述

一、申请测试公众号

个人订阅号有一些接口是没有权限的,也就是说个人订阅号无法调用一些高级的权限接口,如生成二维码、网页授权、自定义菜单这样的接口权限个人订阅号是没有调用权限的, 幸运的是,微信公众平台提供了测试公众账号,测试公众号有很多个人订阅号不具备的权限, 测试公众号的注册地址为:
微信测试号申请页面
进入页面后可以扫描二维码进行登录,登录成功后即可使用微信分配给我们的测试号
在这里插入图片描述
测试号拥有基本公众号的大部分权限,但是对于微信扫一扫,微信支付之类的权限是没有的,需要申请正式的服务号可以获得。

二、使用NATAPP搭建本地调试环境

因为微信用户每次向公众号发起请求时,微信服务器会先接收到用户的请求,然后再转发到我们的服务器上,微信服务器是要和我们的服务器进行网络交互,所以我们必须保证我们的服务器外网可以访问到,这种部署到公网服务器进行测试的做法对于我们开发者来说简直是噩梦。所以我们借用NATAPP可以实现内网穿透,意思就是我们可以将内网的服务器映射到外网给别人访问,这种方式会大幅度的减少我们的部署,调试时间,进行更加高效的编码。

1.下载客户端

NATAPP官网链接
进入后向下滑动,可以看到下载页面
在这里插入图片描述
根据自己的系统选择不同的下载方式即可。

2.安装NATAPP

下载完成后是一个安装包,解压完成后
在这里插入图片描述
然后配置环境变量
在这里插入图片描述
这时,配置已经好了,但是打开natapp.exe还是不行
因为这时需要token认证的
登录你的NATAPP NATAPP官网链接 购买免费隧道
在这里插入图片描述
这个本地端口就是需要把你本地这个端口映射到域名上,到时候启动,如果映射的前端端口,输入域名则会显示你前端页面,后端就是域名+接口访问路径就是你的接口路径

购买完成后,进入我的隧道,查看自己的authtoken
在这里插入图片描述
然后这时cmd进入natapp目录执行 natapp -authtoken 你自己的authtoken 回车
在这里插入图片描述
此时外网的用户可以直接使用http://5nyb3y.natappfree.cc这个域名访问到我内网的127.0.0.1:8080服务器
这时就已经成功了。

三、微信公众号介入(校验签名)

可以参考微信的官方文档微信接入指南

1.校验签名讲解

接入微信公众平台开发,开发者需要按照如下步骤完成:

  1. 填写服务器配置
  2. 验证服务器地址的有效性

验证时的请求为get请求,其余的微信回调操作为post请求,可以根据请求进行区分
在这里插入图片描述

  • url:是你的接口验证地址
  • token:可以随便进行填写,但是一定要与接口里面验证的token一致

点击提交验证服务器地址的有效性,这时微信服务器将发送一个http的get请求到刚刚填写的服务器地址,并且携带四个参数

参数描述
signature微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp时间戳
nonce随机数
echostr随机字符串
开发者通过检验signature对请求进行校验。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
  1. 将token、timestamp、nonce三个参数进行字典序排序
  2. 将三个参数字符串拼接成一个字符串进行sha1加密
  3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

2.代码展示

WxcharController.java

import io.modules.portal.wx.service.WechatService;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/wechat")
public class WechatController {

    @Resource
    private WechatService wechatService;

    //微信回调的校验接口
    @RequestMapping(value = "/check", produces = "text/plain;charset=UTF-8", method = {RequestMethod.GET, RequestMethod.POST})
    //微信服务器根据小程序配置的token,结合时间戳timestamp和随机数nonce通过SHA1生成签名,发起get请求,检验token的正确性,
    //检验正确原样返回随机字符串echostr,失败返回空字符串
    public String check(HttpServletRequest request, HttpServletResponse response,
                        @RequestParam("signature")String signature,
                        @RequestParam("timestamp") String timestamp,
                        @RequestParam("nonce")String nonce,
                        String echostr) throws Exception{
        //若是为get请求,则为开发者模式验证
        if ("get".equals(request.getMethod().toLowerCase())) {
            return wechatService.checkSignature(signature, timestamp, nonce, echostr);
        }
    }
}

WechatServiceImpl

import io.modules.portal.wx.exception.AesException;
import io.modules.portal.wx.service.WechatService;
import io.modules.portal.wx.util.SHA1;
import org.springframework.stereotype.Service;

@Service
public class WechatServiceImpl implements WechatService {
        @Override
        public String checkSignature(String signature, String timestamp, String nonce, String echostr) {
                try {
                        //这里的“token”是正确的token,由服务器定义,小程序只有使用正确的token,微信服务器才会验证通过
                        String checkSignature = SHA1.creatSHA1("cherry",timestamp,nonce);
                        if (checkSignature.equals(signature)){
                                return echostr;
                        }
                } catch (AesException e) {
                        e.printStackTrace();
                }
                return null;
        }
}

一个工具类SHA1

import io.modules.portal.wx.exception.AesException;
import java.security.MessageDigest;
import java.util.Arrays;

//SHA1加密算法类
public class SHA1 {
    /**
     *
     * @param token
     * @param timestamp 时间戳
     * @param nonce 随机字符串
     * @return 安全签名
     * @throws AesException
     */
    public static String creatSHA1(String token, String timestamp, String nonce) throws AesException
    {
        try {
            String[] array = new String[] { token, timestamp, nonce};
            StringBuffer sb = new StringBuffer();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 3; 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);
        }
    }
}

一个异常类AesException

public class AesException extends Exception {

    public final static int OK = 0;
    public final static int ValidateSignatureError = -40001;
    public final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    public final static int IllegalAesKey = -40004;
    public final static int ValidateAppidError = -40005;
    public final static int EncryptAESError = -40006;
    public final static int DecryptAESError = -40007;
    public final static int IllegalBuffer = -40008;

    private int code;

    private static String getMessage(int code) {
        switch (code) {
            case ValidateSignatureError:
                return "签名验证错误";
            case ParseXmlError:
                return "xml解析失败";
            case ComputeSignatureError:
                return "sha加密生成签名失败";
            case IllegalAesKey:
                return "SymmetricKey非法";
            case ValidateAppidError:
                return "appid校验失败";
            case EncryptAESError:
                return "aes加密失败";
            case DecryptAESError:
                return "aes解密失败";
            case IllegalBuffer:
                return "解密后得到的buffer非法";
            default:
                return null; // cannot be
        }
    }

    public int getCode() {
        return code;
    }

    public AesException(int code) {
        super(getMessage(code));
        this.code = code;
    }
}

接口写完后,开始在测试号进行校验,成功后我们的公众号应用已经能够和微信服务器正常通信了,也就是说我们的公众号已经接入到微信公众平台了。

四、access_token管理

官方文档链接
我们的公众号和微信服务器对接成功之后,接下来要做的就是根据我们的业务需求调用微信公众号提供的接口来实现相应的逻辑了。在使用微信公众号接口中都需要一个access_token。

1.access_token讲解

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

总结以上说明,access_token需要做到以下两点:

  1. 因为access_token有2个小时的时效性,要有一个机制保证最长2个小时重新获取一次。
  2. 因为接口调用上限每天2000次,所以不能调用太频繁。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。
接口调用请求说明

https请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数是否必须说明
grant_type获取access_token填写client_credential
appid第三方用户唯一凭证
secret第三方用户唯一凭证密钥,即appsecret

返回说明
正常情况下,微信会返回下述JSON数据包给公众号:

{“access_token”: “ACCESS_TOKEN”,“expires_in”:7200}

参数说明

参数说明
access_token获取到的凭证
expires_in凭证有效时间,单位:秒

2.代码展示

pom引入依赖

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.13</version>
</dependency>
<dependency>
	<groupId>net.sf.json-lib</groupId>
	<artifactId>json-lib</artifactId>
	<version>2.2.3</version>
	<classifier>jdk15</classifier>
</dependency>

静态常量类WechatConstants

//静态常量类
public class WechatConstants {
    //公众号appid
    public static final String APPID = "";

    //公众号appSecert
    public static final String appSecret = "";

    //获取access_token_Url
    private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";

    //将appid与appSecert填入后得到的获取access_token_Url
    public static String getAccess_token_url(){
        return ACCESS_TOKEN_URL.replace("APPID",APPID).replace("APPSECRET",appSecret);
    }
}

一个工具类MyX509TrustManager

import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;

public class MyX509TrustManager implements X509TrustManager {
    /**
     * 该方法用于检查客户端的证书,若不信则抛出异常
     * 由于我们不需要对客户端进行认证,可以不做任何处理
     */
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateEncodingException {

    }

    /**
     * 该方法用于检验服务器端的证书,若不信任则抛出异常
     * 通过自己实现该方法,可以使之信任我们指定的任何证书
     * 在实现该方法时,也可以不做任何处理,即一个空的方法实现
     * 由于不会抛出异常,它就会信任任何证书
     */
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateEncodingException {

    }

    /**
     * 返回收信任的X509证书数组
     */
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

一个工具类WeixinUtil

import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;

/**
 * 公众请求工具
 */
public class WeixinUtil {
    private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);

    /**
     * 发起https请求并获取结果
     *
     * @param requestUrl 请求地址
     * @param requestMethod 请求方式(GET、POST)
     * @param outputStr 提交的数据
     * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
     */
    public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
        JSONObject jsonObject = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = { new MyX509TrustManager() };
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            // 从上述SSLContext对象中得到SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(requestUrl);
            HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
            httpUrlConn.setSSLSocketFactory(ssf);

            httpUrlConn.setDoOutput(true);
            httpUrlConn.setDoInput(true);
            httpUrlConn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            httpUrlConn.setRequestMethod(requestMethod);

            if ("GET".equalsIgnoreCase(requestMethod))
                httpUrlConn.connect();

            // 当有数据需要提交时
            if (null != outputStr) {
                OutputStream outputStream = httpUrlConn.getOutputStream();
                // 注意编码格式,防止中文乱码
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }

            // 将返回的输入流转换成字符串
            InputStream inputStream = httpUrlConn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String str = null;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            bufferedReader.close();
            inputStreamReader.close();
            // 释放资源
            inputStream.close();
            inputStream = null;
            httpUrlConn.disconnect();
            jsonObject = JSONObject.fromObject(buffer.toString());
        } catch (ConnectException ce) {
            log.error("Weixin server connection timed out.");
        } catch (Exception e) {
            log.error("https request error:{}", e);
        }
        return jsonObject;
    }
}

我的access_token是存入的数据库,所以需要建表
在这里插入图片描述
建完表之后建议添加一条数据,id为1,内容随便,因为我只写了定时修改方法,没有写新增方法
实体类AccessToken

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

//这个注解不用写get和set方法了
@Data
public class AccessToken {

	//自增返回id,不过这里好像没什么用
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String accessToken; //获取到的凭证
}

一个定时任务AccessTokenThread

import io.modules.portal.entity.AccessToken;
import io.modules.portal.wx.constants.WechatConstants;
import io.modules.portal.wx.service.AccessTokenService;
import io.modules.portal.wx.util.WeixinUtil;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * 定时获取微信access_token的线程
 *在主函数中注解@EnableScheduling,在程序启动时就开启定时任务。
 * 每90分钟执行一次
 */
@Component
public class AccessTokenThread {
    private static Logger log = LoggerFactory.getLogger(AccessTokenThread.class);

    @Autowired
    private AccessTokenService accessTokenService;
    //定时任务,90分钟执行一次
    @Scheduled(fixedDelay = 2*2700*1000)
    public void getTimingAccessToken(){
        //获取微信服务器返回的json
        JSONObject accessTokenJson = AccessTokenThread.getAccessToken();
        String access_token = accessTokenJson.getString("access_token");
        int expires_in = accessTokenJson.getInt("expires_in");
        log.info("成功获取access_token:"+access_token);
        //只有修改方法,没有新增,注意看上面文章
        AccessToken accessToken = new AccessToken();
        accessToken.setId(1);
        accessToken.setAccessToken(access_token);
        accessTokenService.saveOrUpdate(accessToken);
    }

    public static JSONObject getAccessToken(){
        return WeixinUtil.httpRequest(WechatConstants.getAccess_token_url(), "GET", null);
    }
}

需要在启动类中添加注解@EnableScheduling用来启动定时任务

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 这里,启用定时任务
public class ScienceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ScienceApplication.class, args);
		System.out.println("启动成功");
	}
}

到这时,项目启动时已经可以自动获取access_token并且完成修改,之后定时任务会自动一个半小时执行一次进行修改。

最近微信更改了稳定版接口调用凭据,我使用之前的方法经常会获取到的access_token经常无法使用,现在更新一下稳定版接口调用凭证。

//添加一个处理微信POST请求的方法

import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson2.JSON;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;
import java.util.HashMap;

/**
 * 公众请求工具
 */
public class WxUtil {
    private static Logger log = LoggerFactory.getLogger(WxUtil.class);
    /**
     * 发起https请求并获取结果 (使用post方式解析)
     * @param requestUrl 请求地址
     * @param outputMap 提交的数据
     * @return
     */
    public static com.alibaba.fastjson2.JSONObject postRequest(String requestUrl, HashMap<String, Object> outputMap){
        String jsonStr = JSON.toJSONString(outputMap);
        HttpResponse response = HttpRequest.post(requestUrl)
                .header(Header.CONTENT_ENCODING, "UTF-8")
                // 发送json数据需要设置contentType
                .header(Header.CONTENT_TYPE, "application/x-www-form-urlencoded")
                .body(jsonStr)
                .execute();
        if (response.getStatus() == HttpStatus.HTTP_OK) {
            return com.alibaba.fastjson2.JSONObject.parseObject(response.body());
        }
        return com.alibaba.fastjson2.JSONObject.parseObject("");
    }
}

然后把定时任务AccessTokenThread中的代码改为

import io.modules.portal.entity.AccessToken;
import io.modules.portal.wx.constants.WechatConstants;
import io.modules.portal.wx.service.AccessTokenService;
import io.modules.portal.wx.util.WeixinUtil;
import net.sf.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * 定时获取微信access_token的线程
 *在主函数中注解@EnableScheduling,在程序启动时就开启定时任务。
 * 每90分钟执行一次
 */
@Component
public class AccessTokenThread {
    private static Logger log = LoggerFactory.getLogger(AccessTokenThread.class);

    @Autowired
    private AccessTokenService accessTokenService;
    //定时任务,90分钟执行一次
    @Scheduled(fixedDelay = 2*2700*1000)
    public void getTimingAccessToken(){
        //获取微信服务器返回的json
        HashMap<String, Object> requestParam = new HashMap<>();
        // 手机号调用凭证
        requestParam.put("grant_type", "client_credential");
        requestParam.put("appid", WX_APPID);
        requestParam.put("secret", WX_SECRET);
        com.alibaba.fastjson2.JSONObject jsonObject = WxUtil.postRequest(ACCESS_TOKEN_STABLE_URL, requestParam);
        String access_token = jsonObject.get("access_token").toString();
        log.info("成功获取access_token:"+access_token);
        //只有修改方法,没有新增,注意看上面文章
        AccessToken accessToken = new AccessToken();
        accessToken.setId(1);
        accessToken.setAccessToken(access_token);
        accessTokenService.saveOrUpdate(accessToken);
    }

    public static JSONObject getAccessToken(){
        return WeixinUtil.httpRequest(WechatConstants.getAccess_token_url(), "GET", null);
    }
}

五、生成带参数的二维码

微信官方文档生成带参二维码

1.生成带参数的二维码讲解

目前有2种类型的二维码:

  1. 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
  2. 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景

获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借ticket到指定URL换取二维码。

创建二维码ticket

每次创建二维码ticket需要提供一个开发者自行设定的参数(scene_id),分别介绍临时二维码和永久二维码的创建二维码ticket过程。

临时二维码请求说明

http请求方式: POST URL:

https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN

POST数据格式:json POST数据例子:{“expire_seconds”: 604800, “action_name”: “QR_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{“expire_seconds”: 604800, “action_name”: “QR_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}

永久二维码请求说明

http请求方式: POST URL:

https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN

POST数据格式:json POST数据例子:{“action_name”: “QR_LIMIT_SCENE”, “action_info”: {“scene”: {“scene_id”: 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数: {“action_name”: “QR_LIMIT_STR_SCENE”, “action_info”: {“scene”: {“scene_str”: “test”}}}

参数说明

参数说明
expire_seconds该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为60秒。
action_name二维码类型,QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值
action_info二维码详细信息
scene_id场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1–100000)
scene_str场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
返回说明
正确的Json返回结果:

{“ticket”:“gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
3sUw==”,“expire_seconds”:60,“url”:“http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI”}

参数说明
ticket获取的二维码ticket,凭借此ticket可以在有效时间内换取二维码。
expire_seconds该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。
url二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片
通过ticket换取二维码

获取二维码ticket后,开发者可用ticket换取二维码图片。请注意,本接口无须登录态即可调用。

请求说明

HTTP GET请求(请使用https协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode

返回说明

ticket正确情况下,http 返回码是200,是一张图片,可以直接展示或者下载。
错误情况下(如ticket非法)返回HTTP错误码404。

2.代码展示

一个工具类,QrcodeUtil

import java.io.*;
import java.net.URL;
import java.net.URLConnection;

public class QrcodeUtil {

    //post请求
    public static String sendPost(String param, String url) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            // out = new PrintWriter(conn.getOutputStream());
            out = new PrintWriter(new OutputStreamWriter(
                    conn.getOutputStream(), "utf-8"));
            // 发送请求参数
            out.print(param);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(
                    conn.getInputStream(), "UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送 POST 请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定URL发送GET方法的请求
     *
     */
    public static String get(String url) {
        BufferedReader in = null;
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            // 建立实际的连接
            connection.connect();
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            StringBuffer sb = new StringBuffer();
            String line;
            while ((line = in.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        } catch (Exception e) {

        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return null;
    }
}

然后测试生成二维码

import io.modules.portal.entity.AccessToken;
import io.modules.portal.wx.service.AccessTokenService;
import io.modules.portal.wx.util.QrcodeUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;


/**
 * 测试生成带参数二维码,   2592000(有效期30天)
 */
@RestController
@RequestMapping("/qrcode")
@Api(tags = "测试生成带参数二维码")
public class QrcodeController {

    @Autowired
    private AccessTokenService accessTokenService;

    @RequestMapping("/getImg")
    @ApiOperation("创建生成二维码")
    public static void getImg(AccessToken accessToken,String account, HttpServletResponse resp) throws Exception{
        AccessToken byId = accessTokenService.getById(1);
        if (accessToken!=null) {
            String scene_id = account;
            String temporaryQR = getTemporaryQR(accessToken.getAccessToken(), scene_id);
//            System.out.println(temporaryQR);
            JSONObject fromObject = JSONObject.fromObject(temporaryQR);
            String ticket = fromObject.get("ticket").toString();
//            System.out.println(ticket);
            //通过ticket换取二维码
            String url2 = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket + "";
            try {
                URL url = new URL(url2);
                HttpURLConnection httpUrl = (HttpURLConnection) url.openConnection();
                httpUrl.connect();
                httpUrl.getInputStream();
                InputStream is = httpUrl.getInputStream();
                BufferedImage image = ImageIO.read(is);
                // 可通过输出流输出到页面
                ImageIO.write(image, "png", resp.getOutputStream());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /*生成临时二维码*/
    public static String getTemporaryQR(String access_token,String scene_id){
        //获取数据的地址(微信提供)
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token="+access_token+"";

        //发送给微信服务器的数据 expire_seconds为时间,单位秒,最大2592000,30天
        String jsonStr = "{\"expire_seconds\": 120,\"action_name\": \"QR_SCENE\", \"action_info\": {\"scene\": {\"scene_id\": "+scene_id+"}}}";

        //将得到的字符串转化成json对象
        String response = QrcodeUtil.sendPost(jsonStr, url);
        return response.toString();
    }
}

这时已经可以实现扫描二维码跳转公众号了。

六、用户关注后的微信回调(包含发送文字、图文消息、被动回复消息、图文消息实现静默跳转)

1.详解

微信官方文档接受普通消息 微信官方文档接收事件推送 微信官方文档被动回复用户消息 微信官方文档网页授权
这些文档中包含了接受普通消息,接收事件推送,被动回复用户消息,网页授权的一些xml案例,以及参数描述
可以详细阅读一下
具体的内容都在代码中展示,可以挑选自己需要的进行查看

2.代码展示

引入依赖

<dependency>
	<groupId>com.thoughtworks.xstream</groupId>
	<artifactId>xstream</artifactId>
	<version>1.4.18</version>
</dependency>

在yml文件中填写一项域名,当然也可以一个一个放域名,然后修改,只不过有一些麻烦

#域名
web:
    url: http://5nyrb4.natappfree.cc

一个工具类MessageUtil
使用的静默页面跳转,就是默认授权,用户不会感知到信息,直接跳转页面,如果想要传递参数,我通过网上试了试,目前session不行,我是使用的路径后用?拼接参数,前端拦截路径,获取到路径上的参数
记住,跳转的前端最后路径不能带有#/,否则会在微信跳转的时候会报错

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.StaxDriver;
import io.modules.portal.wx.constants.WechatConstants;
import io.modules.portal.wx.entity.News;
import io.modules.portal.wx.entity.NewsMessage;
import io.modules.portal.wx.entity.TextMessage;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.*;

@Component
public class MessageUtil {

	//当SAVE_WEB_PATH被static修饰了之后,就赋不了值
    private static String SAVE_WEB_PATH;

	// 把@Value(value="${web.url}")放到静态变量的set方法上面即可,
    // 需要注意的是set方法要去掉static,还有就是当前类要交给spring来管理
    @Value("${web.url}")
    public void setSaveWebPath(String saveWebPath) {
        SAVE_WEB_PATH = saveWebPath;
    }

    public static final String MESSAGE_TEXT="text";//文本
    public static final String MESSAGE_IMAGE="image";//图片
    public static final String MESSAGE_NEWS="news";//图文消息
    public static final String MESSAGE_VOICE="voice";//语音
    public static final String MESSAGE_VIDEO="video";//视频
    public static final String MESSAGE_MUSIC="music";//音乐
    public static final String MESSAGE_LOCATION="location";//位置
    public static final String MESSAGE_LINK="link";//链接消息
    public static final String MESSAGE_EVENT="event";//事件
    public static final String MESSAGE_SUBSCRIBE="subscribe";//关注
    public static final String MESSAGE_UNSUBSCRIBE="unsubscribe";//取消关注
    public static final String MESSAGE_CLICK="CLICK";//点击
    public static final String MESSAGE_VIEW="VIEW";//点击菜单跳转链接时的事件推送
    public static final String MESSAGE_SCANCODE= "scancode_push";//扫码

    /**
     * xml 转 map
     * @param request
     * @return
     * @throws IOException
     * @throws DocumentException
     */
    public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException{
        Map<String, String> map = new HashMap<String,String>();
        SAXReader reader = new SAXReader();
        InputStream ins = request.getInputStream();
        Document doc = reader.read(ins);
        Element root = doc.getRootElement();
        List<Element> list = root.elements();
        for(Element e : list){
            map.put(e.getName(), e.getText());
        }
        ins.close();
        return map;
    }

    //初始化文本消息
    public static String initText(String toUserName,String fromUserName,String content){
        String message = "";
        TextMessage text = new TextMessage();
        text.setFromUserName(toUserName);
        text.setToUserName(fromUserName);
        text.setMsgType(MESSAGE_TEXT);
        text.setCreateTime(new Date().getTime()+"");
        text.setContent(content);
        message = MessageUtil.textMessageToXml(text);
        return message;
    }


    /**
     * 初始化图文消息(首次,两条)
     */
    public static String initNewsMessage(String toUSerName, String fromUserName) {
        List<News> newsList = new ArrayList<>();
        //组建一条图文 第四个字段url我测试的只能拼接一个字段,所以我把参数都放进了一个里面,然后前端进行截取
        //因为微信会自动在连接上拼接code,建议字段不要写code,
        //微信拼接路径为&code=073uhOll2xF9586eTDml2neqEs3uhOlX&state=BINDFACE
        News news = getNews("hello,我是服务小助手~", "", SAVE_WEB_PATH + "/science/file/weixin.png", SAVE_WEB_PATH+"?openid="+fromUserName);
        newsList.add(news);
        News news2 = getNews("我们专注服务,打造技术服务平台!", "", "", SAVE_WEB_PATH+"?openid="+fromUserName);
        newsList.add(news2);
        News news3 = getNews("软件开发", "您刚才浏览的需求详情在这里,快来沟通吧~~", SAVE_WEB_PATH + "/science/file/weixin3.png", SAVE_WEB_PATH + "/Information?openid="+fromUserName);
        newsList.add(news3);
        //组装图文消息相关信息
        NewsMessage newsMessage = getNewsMessage(fromUserName, toUSerName, MESSAGE_NEWS, newsList);
        //调用newsMessageToXml将图文消息转化为XML结构并返回
        return MessageUtil.newsMessageToXml(newsMessage);
    }

    /**
     * 初始化图文消息(单条)
     */
    public static String initNews(String toUSerName, String fromUserName) {
        List<News> newsList = new ArrayList<>();
        //组建一条图文
        News news = getNews("软件开发", "您刚才浏览的需求详情在这里,快来沟通吧~", SAVE_WEB_PATH + "/science/file/weixin3.png", SAVE_WEB_PATH + "/Information?openid="+fromUserName);
        newsList.add(news);
        //组装图文消息相关信息
        NewsMessage newsMessage = getNewsMessage(fromUserName, toUSerName, MESSAGE_NEWS, newsList);
        //调用newsMessageToXml将图文消息转化为XML结构并返回
        return MessageUtil.newsMessageToXml(newsMessage);
    }

	//title:标题,description:副标题,picUrl:图片路径,url:跳转的前端页面路径
    public static News getNews(String title,String description,String picUrl,String url){
        News newsItem = new News();
        newsItem.setTitle(title);
        newsItem.setDescription(description);
        newsItem.setPicUrl(picUrl);
        newsItem.setUrl("https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + WechatConstants.APPID + "&redirect_uri=" +
                url + "&response_type=code&scope=snsapi_base&state=BINDFACE#wechat_redirect");
        return newsItem;
    }

    public static NewsMessage getNewsMessage(String fromUserName,String toUSerName,String MsgType,List<News> newsList){
        NewsMessage newsMessage = new NewsMessage();
        newsMessage.setToUserName(fromUserName);
        newsMessage.setFromUserName(toUSerName);
        newsMessage.setCreateTime(new Date().getTime());
        newsMessage.setMsgType(MESSAGE_NEWS);
        newsMessage.setArticles(newsList);
        newsMessage.setArticleCount(newsList.size());
        return newsMessage;
    }

    /**
     * 将文本消息对象转换成xml
     * @param textMessage
     * @return
     * 注意事项:添加xstream.jar
     */
    public static String textMessageToXml(TextMessage textMessage){
        //XStream xStream = new XStream(new StaxDriver());
        xStream.alias("xml", textMessage.getClass());
        return xStream.toXML(textMessage);
    }

    /**
     * xStream本身不支持生成cdata块生成,对xstream扩展,让其自动生成cdata块
     */
    private static XStream xStream = new XStream(new StaxDriver(){
        public HierarchicalStreamWriter createWriter(Writer out){
            return new PrettyPrintWriter(out){
                boolean cdata = true;

                public void startNode(String name,Class clazz){
                    super.startNode(name, clazz);
                }
                protected void writeText(QuickWriter writer, String text){
                    if(cdata){
                        writer.write("<![CDATA[");
                        writer.write(text);
                        writer.write("]]>");
                    }else{
                        writer.write(text);
                    }
                }
            };
        }
    });

    /**
     * 图文消息转XML结构方法
     */
    public static String newsMessageToXml(NewsMessage message) {
        XStream xs = new XStream();
        //由于转换后xml根节点默认为class类,需转化为<xml>
        xs.alias("xml", message.getClass());
        xs.alias("item", new News().getClass());
        return xs.toXML(message);
    }
}

然后就是接口:是吧最开始的校验签名部分添加了一部分代码。
因为校验采用的是get请求,其余采用post请求,所以直接使用else即可,这里也就是微信的回调方法
用户通过关注进入微信后,会自动走入该方法。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.modules.portal.entity.*;
import io.modules.portal.service.*;
import io.modules.portal.utils.NumberUtil;
import io.modules.portal.wx.service.WechatService;
import io.modules.portal.wx.util.MessageUtil;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Map;

@RestController
@RequestMapping("/wechat")
public class WechatController {

    @Resource
    private WechatService wechatService;

    //微信回调的校验接口
    @RequestMapping(value = "/check", produces = "text/plain;charset=UTF-8", method = {RequestMethod.GET, RequestMethod.POST})
    //微信服务器根据小程序配置的token,结合时间戳timestamp和随机数nonce通过SHA1生成签名,发起get请求,检验token的正确性,
    //检验正确原样返回随机字符串echostr,失败返回空字符串
    public String check(HttpServletRequest request, HttpServletResponse response,
                        @RequestParam("signature")String signature,
                        @RequestParam("timestamp") String timestamp,
                        @RequestParam("nonce")String nonce,
                        String echostr) throws Exception{
        //若是为get请求,则为开发者模式验证
        if ("get".equals(request.getMethod().toLowerCase())) {
            return wechatService.checkSignature(signature, timestamp, nonce, echostr);
        }else{
            request.setCharacterEncoding("UTF-8");
            response.setCharacterEncoding("UTF-8");
            PrintWriter out = response.getWriter();
            try {
                Map<String, String> map = MessageUtil.xmlToMap(request);
                String fromUserName = map.get("FromUserName");  //一个openid
                String toUserName = map.get("ToUserName");  //开发者微信号
                String msgType = map.get("MsgType");    //消息类型
                String event = map.get("Event"); //从集合中,获取是哪一种事件传入 事件类型,subscribe(订阅)、unsubscribe(取消订阅) 如果是已关注的用户,事件类型,SCAN
                String eventKey = map.get("EventKey");
                String message = null;
                String code = "";    //二维码携带参数
                if (event==null){
                    //其余    回复文本消息
                    //消息类型判断    发送文本时
                    //接收到用户信息时,自动回复文本内容
                    if(MessageUtil.MESSAGE_TEXT.equals(msgType)){
                        String text = "好的收到~小助手看到后马上和您联系~若想尽快联系,可拨打电话xxxxx";
                        //调用初始化文本消息方法
                        message = MessageUtil.initText(toUserName, fromUserName, text);
                    }
                    out.print(message);
                    out.close();
                    return null;
                }
                //取消订阅,则直接结束
                if (event.equals("unsubscribe")){
                    out.close();
                    return null;
                }
                if (event.equals("subscribe")) {
                    //订阅
                    //eventKey 事件KEY值,qrscene_为前缀,后面为二维码的参数值
                    //对获取到的参数进行处理
                    //消息类型判断    触发事件时
                    String[] params = eventKey.split("_");
                    if (params.length == 2) {
                        code = params[1];
                    }
                } else if (event.equals("SCAN")){
                    // 已关注的用户
                    //eventKey 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
                    code = eventKey;
                }
                System.out.println(code);
                //只有订阅,或者已关注用户才走,因为不关注走不到这一步
                if (fromUserName!=null){
                    //查看库中是否有这个人的信息 这部分属于我自己的逻辑部分,其中下面的发送多条图文和单条应该会用到
                    QueryWrapper<CommonUser> queryWrapper = new QueryWrapper<>();
                    CommonUser openId = commonUserService.getOne(queryWrapper.eq("open_id", fromUserName));
                    if (openId == null){
                        if(MessageUtil.MESSAGE_EVENT.equals(msgType)){
                            //没有填写信息 进行填写信息 发送多条图文消息
                            message = MessageUtil.initNewsMessage(toUserName, fromUserName,code);
                            out.print(message);
                        }
                    }else{
                        //填写过信息,应返回用户想要的电话,姓名等 发送单条图文信息
                        if (MessageUtil.MESSAGE_EVENT.equals(msgType)) {
                            message = MessageUtil.initNews(toUserName, fromUserName,code);
                            out.print(message);
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            out.close();
            return null;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值