vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息

项目中遇到的,只是做个笔记, 项目用的ruoyi的前后端分离框架,有些类没有的话,可以去官网下http://www.ruoyi.vip/

源码整理了下:百度网盘 提取码:zuet

补充!补充!补充!补充!补充!补充!补充!补充!
后面发现有cookie丢失的问题,后来使用reids和回调参数的方案来解决这种问题,具体的代码在文章中,云盘上没有去改!!!!!
具体的思路:

  1. 用户进入公众号,调用后台cookie接口,有就不生成没有就生成
  2. 在请求url的时候获取cookie,存入reids,然后将cookie值(可以利用state参数也可以自定义参数)拼接在回调地址路径上
  3. 回调是获取cookie,然后去reids查找,如果找到了生成token或获取openid,然后存到reids,如果没找到就说明cookie变更或者回调被改动就不进行授权

业务需求

  1. 动态菜单
  2. 实现微信授权,获取openid
  3. 关注公众号时,推送消息(包括:文本、图文消息)

整体代码,没有单独拿出来,有需要的可以私信,我整理一下,基本上所有的代码都在这了,其他的个别工具类,官网上可以找到

讲解:
因为是前后端分离,无状态,问题在于要怎么存储openid,又怎么获取。

实现方法:

  1. 利用cookie,作为每个用户的kay,(因为是前后台分离,所以生成cookie时要声明cookie的域及设置path哪儿些方法可以共用cookie)
  2. 将key(cookie),存储到redis
  3. 访问页面时,去查询后台是否存在cookie,不存在就声明,去调用微信授权接口,如果存在,就用获取的cookie去查redis是否存在openid,不存在就调用微信授权接口
  4. 授权成功后,重定向到前端首页

关于本地测试方法,将微信公众号配置的回调url用nginx代理到本地,然后使用微信开发者工具进行网页访问就能实现本地环境的测试流程。
订阅消息推送当时应该也是使用nginx代理实现的,这个当时没做笔记,忘了怎么处理的了
注意:具体问题具体分析,这是我在本地测试的使用的方式

pom

		
        <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-allartifactId>
                
                <version>5.1.0version>
            dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

yml

#微信公众号
wxApp:
  #请求code地址
 code-uri: https://open.weixin.qq.com/connect/oauth2/authorize
 # 微信appid
 appId: appid
  #公众号appSecret
 appsecret: appSecret
 #重定向地址  
 redirect-uri: 自己写的回调方法 #例如: http://xx.xx.com/wxapp/wxAppWeb/getWxOpenId
 #请求token地址
 token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
 # 解密时 校验获取的数据
 token: # 微信公众号后台获取
 #数据加密key
 encodingAESKey: #微信公众号后台获取
 #授权成功后,重定向的地址
 redirect-index: 前端域名 #例如: http://xx.xx.com:81/
 #cookie的域名 域是前台访问域,这样cookie前后台可公用
 cookie-domain: cookie域 #例如: xx.xx.com
  # 获取微信access_token 的uri
 access-token-uri: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${wxApp.appId}&secret=${wxApp.appsecret}
 #微信公众号创建菜单uri
 menu-create-uri: https://api.weixin.qq.com/cgi-bin/menu/create
 #微信公众号查询菜单uri
 menu-query-uri: https://api.weixin.qq.com/cgi-bin/menu/get
 #微信公众号删除菜单uri
 menu-del-uri: https://api.weixin.qq.com/cgi-bin/menu/delete

ServletUtils

package com.ruoyi.common.utils;

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.ruoyi.common.core.text.Convert;

/**
 * 客户端工具类
 * 
 * @author ruoyi
 */
public class ServletUtils
{
    /**
     * 获取String参数
     */
    public static String getParameter(String name)
    {
        return getRequest().getParameter(name);
    }

    /**
     * 获取String参数
     */
    public static String getParameter(String name, String defaultValue)
    {
        return Convert.toStr(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name)
    {
        return Convert.toInt(getRequest().getParameter(name));
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name, Integer defaultValue)
    {
        return Convert.toInt(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest()
    {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse()
    {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取session
     */
    public static HttpSession getSession()
    {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes()
    {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 是否是Ajax异步请求
     * 
     * @param request
     */
    public static boolean isAjaxRequest(HttpServletRequest request)
    {
        String accept = request.getHeader("accept");
        if (accept != null && accept.indexOf("application/json") != -1)
        {
            return true;
        }

        String xRequestedWith = request.getHeader("X-Requested-With");
        if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1)
        {
            return true;
        }

        String uri = request.getRequestURI();
        if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml"))
        {
            return true;
        }

        String ajax = request.getParameter("__ajax");
        if (StringUtils.inStringIgnoreCase(ajax, "json", "xml"))
        {
            return true;
        }
        return false;
    }
}

RedisConfig

package com.ruoyi.framework.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * redis配置
 * 
 * @author ruoyi
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

RedisCache 工具类

package com.ruoyi.common.core.redis;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

/**
 * spring redis 工具类
 *
 * @author ruoyi
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> long setCacheSet(final String key, final Set<T> dataSet)
    {
        Long count = redisTemplate.opsForSet().add(key, dataSet);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

微信公众号授权,实现

WxAppCookieUtils

package com.ruoyi.web.controller.wxapp;

import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.ServletUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import java.util.concurrent.TimeUnit;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/14 10:58
 */
@Component
public class WxAppCookieUtils {

    @Autowired
    private RedisCache redisCache;

    /**
     * 获取缓存中的openid
     * @param
     * @return
     */
    public String getOpenid(){
        String wxCookie = getCookie();
        String redisValue = "";
        if(!StrUtil.hasEmpty(wxCookie)){
            redisValue = redisCache.getCacheObject(wxCookie);
            if(!StrUtil.hasEmpty(redisValue)){
                redisCache.expire(wxCookie,20, TimeUnit.MINUTES);
            }
        }
        return redisValue;

    }

    /**
     * 获取cookie
     * @param
     * @return
     */
    public String getCookie(){
        Cookie[] cookies = ServletUtils.getRequest().getCookies();
        String wxCookie = "";
        if(cookies != null){
            for(Cookie item: cookies){
                if(item.getName().equals("wx-cookie")){
                    wxCookie=item.getValue();
                    System.err.println("有Cookie"+item.getValue());
                    break;
                }
            }
        }
        return wxCookie;
    }
}

WxAppController

package com.ruoyi.web.controller.wxapp;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.domain.server.Sys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;


/**
 * @author qb
 * @version 1.0
 * @Description  微信公众号 相关接口
 * @date 2020/9/24 11:08
 */
@RestController
@RequestMapping("/wxapp/wxAppWeb")
public class WxAppController {

    Logger logger = LoggerFactory.getLogger(getClass());

    private static String REDIS_STATE  = "STATE";

    @Value("${wxApp.code-uri}")
    private String CODE_URI;

    @Value("${wxApp.appId}")
    private String APP_ID;

    @Value("${wxApp.appsecret}")
    private String APPSECRET;

    @Value("${wxApp.redirect-uri}")
    private String REDIRECTURI;

    @Value("${wxApp.token-uri}")
    private String TOKEN_URI;

    @Value("${wxApp.redirect-index}")
    private String REDIRECT_INDEX;

    @Value("${wxApp.cookie-domain}")
    private String COOKIE_DOMAIN;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private WxAppCookieUtils wxAppCookieUtils;

    /**
     * 获取微信公众号code
     * @throws IOException
     */
    @RequestMapping(value = "/getWxCode")
    public void getWxCode() throws IOException {
        String uri = CODE_URI;
            // 如果遇到cookie问题。此处增加cookie参数
            // String wxCookie = wxAppCookieUtils.getCookie();
        //  redisCache.setCacheObject(COOKIE_KEY_CHECK + cookie, cookie, 60, TimeUnit.SECONDS);
       // String params = "appid="+APP_ID+"&redirect_uri="+ REDIRECTURI+"&response_type=code" +
          //      "&scope=snsapi_userinfo&state="+cookie+"#wechat_redirect";
        String params = "appid="+APP_ID+"&redirect_uri="+ REDIRECTURI+"&response_type=code" +
                "&scope=snsapi_userinfo&state="+REDIS_STATE+"#wechat_redirect";
        System.err.println(uri+"?"+params);
    
        ServletUtils.getResponse().sendRedirect(uri+"?"+params);

    }

    /**
     * 获取微信openid
     * @param code
     * @param state
     * @throws Exception
     */
    @RequestMapping(value = "/getWxOpenId")
    public void getWxOpenId(String code, String state) throws Exception {
        String wxCookie = wxAppCookieUtils.getCookie();
        System.err.println("openid wxCode:"+wxCookie);
        //带参get请求,把参数拼接再url后
        String urlString = TOKEN_URI+"?appid="+APP_ID+"&secret="+APPSECRET+"&code="+code+"&grant_type=authorization_code";
        String result =  HttpUtil.get(urlString, CharsetUtil.CHARSET_UTF_8);
        JSONObject object = JSON.parseObject(result);
        logger.info("返回的appid对象: {}",object);
        String redirectUri = REDIRECT_INDEX+"?state=4001";
        if(object != null){
            String openId = (String) object.get("openid");
            logger.info("获取到的openid为: {}",openId);
            if(!StrUtil.hasEmpty(openId)){
                 // cookie丢失问题
                 // Object cacheObject = redisCache.getCacheObject(COOKIE_KEY_CHECK + state);
			     //   if(cacheObject == null){
			     //      throw new Exception("回调参数变更,登录失败");
			     //  }
			     // wxCookie = (String) cacheObject 
                    System.err.println("没有被修改过则进入储存缓存!");
                    redisCache.setCacheObject(wxCookie,openId);
                    redisCache.expire(wxCookie,20, TimeUnit.MINUTES);
                    redirectUri = REDIRECT_INDEX;
                
            }
        }
        ServletUtils.getResponse().sendRedirect(redirectUri);
    }

    /**
     * 检查openid和cookie是否存在
     * @param
     * @param
     * @return
     */
    @RequestMapping(value = "checkCookieAndOpenId")
    public AjaxResult checkCookieAndOpenId(){
        String wxCookie = wxAppCookieUtils.getCookie();
        System.err.println("检查获取的wxCookie:"+wxCookie);
        Map<String,Object> codeMap = new HashMap<>();
        if(StrUtil.hasEmpty(wxCookie)){
            String uuidCookie = IdUtil.simpleUUID();
            Cookie cookie = new Cookie("wx-cookie", uuidCookie);
            //设置cookie域名
            cookie.setDomain(COOKIE_DOMAIN);
            //设置什么方法使用,  / 所有方法
            cookie.setPath("/");
            ServletUtils.getResponse().setHeader("wx-cookie",cookie.getValue());
            ServletUtils.getResponse().addCookie(cookie);
            System.err.println(" 生成的Cookie: "+cookie.getValue());
            wxCookie = cookie.getValue();
        }
        String redisValue = redisCache.getCacheObject(wxCookie);
        System.err.println("获取redis中的缓存的openid:"+redisValue);
        if(!StrUtil.hasEmpty(redisValue)){
            redisCache.expire(wxCookie,20, TimeUnit.MINUTES);
            System.err.println("redisValue 不为null");
            codeMap.put("code",4002);
            return AjaxResult.success("获取openId成功!",codeMap);
        }
        codeMap.put("code",4001);
        return AjaxResult.success("获取openId失败",codeMap);
    }

}

前端用的vant 组件库

vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第1张图片

微信公众号自定义菜单 实现

WxAppMenuC 微信公众号菜单实体

package com.ruoyi.web.controller.wxapp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/16 9:56
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppMenuC {

    /**
     * 按钮类型
     */
    private String type;

    /**
     * 分支菜单名
     */
    private String name;

    /**
     * 菜单key
     */
    private String key;

    /**
     * 菜单连接
     */
    private String url;
}

WxAppMenuM 微信公众号目录实体

package com.ruoyi.web.controller.wxapp;

import com.ruoyi.common.core.domain.entity.WxAppMenu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.poi.ss.formula.functions.T;

import java.util.List;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/16 9:45
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppMenuM {

    /**
     * 分支目录名称
     */
    private String name;

    /**
     * 菜单集合
     */
    private List<WxAppMenuC> sub_button;
}

WxAppMenuUtils 微信公众号自定义菜单及事件推送 工具类

package com.ruoyi.web.controller.wxapp;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.entity.WxAppMenu;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * @author qb
 * @version 1.0
 * @Description 微信公众号自定义菜单及事件推送 工具类
 * @date 2020/10/15 16:56
 */
@Component
public class WxAppMenuUtils {

    /**
     * 获取微信accessToken url
     */
    public static String ACCESS_TOKEN_URI;

    /**
     * 微信公众号创建菜单接口
     */
    public static String MENU_CREATE_URI;


    public static String MENU_QUERY_URI;


    public static String MENU_DEL_URI;

    /**
     * 微信公众号 token令牌
     */
    public static String TOKEN;

    /**
     * 微信公众号消息加密秘钥
     */
    public static String ENCODING_AES_KEY;



    @Value("${wxApp.access-token-uri}")
    public void setAccessTokenUri(String accessTokenUri){
        WxAppMenuUtils.ACCESS_TOKEN_URI = accessTokenUri;
    }


    @Value("${wxApp.menu-create-uri}")
    public  void setMenuCreate(String menuCreate){
        WxAppMenuUtils.MENU_CREATE_URI = menuCreate;
    }


    @Value("${wxApp.menu-query-uri}")
    public void setMenuQuery(String menuQuery){
        WxAppMenuUtils.MENU_QUERY_URI = menuQuery;
    }


    @Value("${wxApp.menu-del-uri}")
    public void setMenuDel(String menuDel){
        WxAppMenuUtils.MENU_DEL_URI = menuDel;
    }

    @Value("${wxApp.token}")
    public void setTOKEN(String token){
        WxAppMenuUtils.TOKEN = token;
    }

    @Value("${wxApp.encodingAESKey}")
    public void setEncodingAesKey(String encodingAesKey){
        WxAppMenuUtils.ENCODING_AES_KEY = encodingAesKey;
    }

    /**
     * 获取access_token
     * @return
     */
    public static String getAccessToken(){
        String result = HttpUtil.get(ACCESS_TOKEN_URI, CharsetUtil.CHARSET_UTF_8);
        System.err.println("uri: "+ACCESS_TOKEN_URI);
        JSONObject jsonObject = JSONObject.parseObject(result);
        if(jsonObject.get("access_token") != null){
            return (String) jsonObject.get("access_token");
        }
        return null;
    }

    /**
     * 获取微信菜单  处理成微信接口所需json
     * @param wxAppMenuList
     * @return
     */
    public static String getMenuJson(List<WxAppMenu> wxAppMenuList){
        Map<String ,Object> map = new HashMap<>();
        Map<String,Object> menu = new HashMap<>();
        Set<String> strIds = new LinkedHashSet<>();
        List<Object> menuList = new ArrayList<>();
        wxAppMenuList.forEach(item ->{
            if(item.getParentId() == 0 && item.getWxMenuType().equals("M")){
                WxAppMenuM wxAppMenuM = new WxAppMenuM();
                List<WxAppMenuC> wxAppMenuCList=new ArrayList<>();
                wxAppMenuM.setName(item.getWxMenuName());
                wxAppMenuM.setSub_button(wxAppMenuCList);
                map.put(item.getId().toString(),wxAppMenuM);
                strIds.add(item.getId().toString());
            }else if(map.get(item.getParentId().toString())!= null){
                WxAppMenuC wxAppMenuC = new WxAppMenuC();
                wxAppMenuC.setName(item.getWxMenuName());
                wxAppMenuC.setType(item.getWxMenuType());
                wxAppMenuC.setUrl(item.getWxMenuUri());
                WxAppMenuM wxAppMenuM = (WxAppMenuM) map.get(item.getParentId().toString());
                wxAppMenuM.getSub_button().add(wxAppMenuC);
            }else{
                WxAppMenuC wxAppMenuC = new WxAppMenuC();
                wxAppMenuC.setName(item.getWxMenuName());
                wxAppMenuC.setType(item.getWxMenuType());
                wxAppMenuC.setUrl(item.getWxMenuUri());
                map.put(item.getId().toString(),wxAppMenuC);
                strIds.add(item.getId().toString());
            }
        });
        strIds.forEach(item ->{
            menuList.add(map.get(item));
        });
        menu.put("button",menuList);
        JSONObject json = new JSONObject(menu);
        return json.toJSONString();
    }


    /**
     * 更新微信公众号菜单
     * @param accessToken
     * @param params
     * @return
     */
    public static int setWxAppMenu(String accessToken,String params){
        String postUri = MENU_CREATE_URI+"?access_token="+accessToken;
        String result = HttpUtil.post(postUri,params);
        JSONObject jsonObject = JSONObject.parseObject(result);
        if(0 == (int)jsonObject.get("errcode")){
            return 0;
        }
        return 1;
    }

    public static String WxAppEvent(){

        return "";
    }
}

controller

    /**
     * 微信公众号立刻更新使用当前配置的菜单
     * @return
     */
    @PreAuthorize("@ss.hasAnyPermi('wxapp:wxAppMenu:UseCurrentConfigure')")
    @Log(title = "微信公众号更新使用当前菜单" , businessType = BusinessType.OTHER)
    @GetMapping("/UseCurrentConfigure")
    public AjaxResult UseCurrentConfigure(){
        WxAppMenu wxAppMenu = new WxAppMenu();
        wxAppMenu.setWxMenuStatus("0");
        List<WxAppMenu> menus = iWxAppMenuService.selectWxAppMenuList(wxAppMenu);
        String menuJson =WxAppMenuUtils.getMenuJson(menus);
        String accessToken = WxAppMenuUtils.getAccessToken();
        String message = "微信公众号菜单更新失败!原因: accessToken或者menuJson(数据库菜单json)出错!";
        if(!StrUtil.hasEmpty(accessToken) && !StrUtil.hasEmpty(menuJson)){
            int result = WxAppMenuUtils.setWxAppMenu(accessToken,menuJson);
            if(result == 0){
                return AjaxResult.success("微信公众号菜单已更新!");
            }else{
                message="微信公众号菜单更新失败!原因: 可能MENU_CREATE_URI请求出错。";
            }
        }
        return AjaxResult.error(message);
    }

获取后台菜单sql

注意排序,主要用于处理成微信官方定义的json

    <select id="selectWxAppMenuList" parameterType="WxAppMenu" resultMap="WxAppMenuResult">
        <include refid="selectWxAppMenuVo"/>
        <where>
            <if test="wxMenuName != null and wxMenuName != ''">
                AND wx_menu_name like concat('%', #{wxMenuName}, '%')
            if>
            <if test="wxMenuStatus != null and wxMenuStatus != ''">
                AND wx_menu_status = #{wxMenuStatus}
            if>
        where>
        order by parent_id, order_num
    select>

前台是改的ruoyi的代码

前台效果图
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第2张图片

微信公众号效果图

vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第3张图片

数据库截图

vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第4张图片

前台字典截图

在这里插入图片描述

vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第5张图片

公众号订阅消息推送,实现

WxAppMenuUtils增加代码

 /**
     * 微信 公众号appid
     */
    public static String APP_ID;
   @Value("${wxApp.appId}")
    public void setAppId(String appId){
        WxAppMenuUtils.APP_ID = appId;
    }
	
	 /**
     * 微信事件推送的回调 -》实现
     * @return
     * @throws IOException
     */
    public static String WxAppEvent() throws IOException, AesException {
        String result="";
        String msgSignature = ServletUtils.getRequest().getParameter("msg_signature");
        String timestamp = ServletUtils.getRequest().getParameter("timestamp");
        String nonce = ServletUtils.getRequest().getParameter("nonce");
        InputStream is = ServletUtils.getRequest().getInputStream();
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(is,"UTF-8"));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null){
                stringBuilder.append(line +'\n');
            }
            result = stringBuilder.toString();
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            is.close();
        }
        //微信官方对密文解密
        WXBizMsgCrypt wxBizMsgCrypt = new WXBizMsgCrypt(TOKEN,ENCODING_AES_KEY,APP_ID);
        result = wxBizMsgCrypt.decryptMsg(msgSignature,timestamp,nonce,result);
        //xml字符串转map
        Map<String,Object> resultMap = XmlUtil.xmlToMap(result);
        if(resultMap.get("MsgType").equals(MSG_TYPE)){
            if(resultMap.get("Event").equals(EVENT)){
                //订阅后的处理
                result = PushWxAppNews(resultMap);
            }
        }
        return result;
    }

    /**
     * 消息推送  加密
     * @param resultMap
     * @return
     */
    public static String PushWxAppNews(Map<String,Object> resultMap) throws AesException {
        String timestamp = ServletUtils.getRequest().getParameter("timestamp");
        String nonce = ServletUtils.getRequest().getParameter("nonce");
        //文本推送
        //String result = WxAppTextPush(resultMap);
        //图文推送
        String result = WxAppImgTextPush(resultMap);
        //将转义的字符串替换回来
        result = result.replaceAll("<","<");
        result = result.replaceAll(">",">");
        //微信官方数据加密
        WXBizMsgCrypt wxBizMsgCrypt = new WXBizMsgCrypt(TOKEN,ENCODING_AES_KEY,APP_ID);
        String aseResult = wxBizMsgCrypt.encryptMsg(result,timestamp,nonce);
        return aseResult;
    }

    /**
     * 文本推送  根据微信官方参数处理
     * @param resultMap
     * @param
     * @param
     * @return
     */
    public static String WxAppTextPush(Map<String,Object> resultMap){
        WxAppTextMessage message = new WxAppTextMessage();
        message.setToUserName((String) resultMap.get("FromUserName"));
        //获取一秒为单位的时间戳
        message.setCreateTime(getSecondTimestampTwo(new Date()));
        message.setMsgType("text");
        message.setFromUserName((String) resultMap.get("ToUserName"));
        message.setContent("欢迎关注,公众号!");
        //类的路径
        //String className = "com.ruoyi.web.controller.wxapp.WxAppTextMessage";
        //Object转map
        Map<String,Object> ObjectToMap =  ObjectToMap(message);
        //map转xml
        String result = XmlUtil.mapToXmlStr(ObjectToMap,"xml");
        return result;
    }

    /**
     * 图文推送  根据微信官方参数处理
     * 为什么不用map转xml,因为微信官方的参数,可以传多个图文消息,每个图文用item标签分割
     * 但是map的键是唯一的,而xml要求多个图文要有多个item标签,所以map不适用,不好维护
     * @return
     */
    public static String WxAppImgTextPush(Map<String,Object> resultMap){
        WxAppImgTextMessage wxAppImgTextMessage = new WxAppImgTextMessage();
        WxAppArticle wxAppArticle = new WxAppArticle();
        wxAppImgTextMessage.setMsgType("news");
        wxAppImgTextMessage.setCreateTime(getSecondTimestampTwo(new Date()));
        wxAppImgTextMessage.setToUserName((String) resultMap.get("FromUserName"));
        wxAppImgTextMessage.setFromUserName((String) resultMap.get("ToUserName"));
        wxAppImgTextMessage.setArticleCount("1");
             wxAppArticle.setPicUrl("图片地址");
        wxAppArticle.setUrl("图文连接");
        wxAppArticle.setTitle("图文标题");
        wxAppArticle.setDescription("图文描述");
        Map<String,Object> ObjectToMap =  ObjectToMap(wxAppImgTextMessage);
        System.err.println("ObjectToMap: "+ObjectToMap);
        Document document = XmlUtil.mapToXml(ObjectToMap,"xml");
        //创建Articles标签
        Element element = document.createElement("Articles");
        Node node = document.getElementsByTagName("xml").item(0);
        //节点添加Articles标签
        node.appendChild(element);
        Node ArticlesNode = document.getElementsByTagName("Articles").item(0);
        document = strElement(ArticlesNode,document,wxAppArticle);
        return XmlUtil.toStr(document);
    }

    /**
     * 对象 转 map
     * @param
     * @param t
     * @return
     */
    public static Map<String,Object> ObjectToMap(Object t){
        Map<String,Object> resultMap = new HashMap<>(60);
        try {
            //动态加载类
            Class cls = Class.forName(t.getClass().toString().split(" ")[1]);
            List<Field> fieldList = new ArrayList<Field>();
            while(cls != null){
                //当父类为null的时候说明到达了最上层的父类(Object类)
                fieldList.addAll(Arrays.asList(cls .getDeclaredFields()));
                //得到父类,然后赋给自己
                cls = cls.getSuperclass();
            }
            for(Field f : fieldList){
                //true  可以获取public,private修饰的字段
                f.setAccessible(true);
                //System.out.println(f.getName()+","+f.get(t));
                if("CreateTime".equals(f.getName())){
                    //时间戳不用加  微信官网的参数例子
                    resultMap.put(f.getName(),f.get(t));
                   // continue;
                }else{
                    //添加key-value 键值对
                    resultMap.put(f.getName(),"+f.get(t)+"]]>");
                }
            }
        } catch (ClassNotFoundException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return resultMap;
    }

    /**
     * xml Document操作
     * @param
     * @param t
     * @return
     */
    public static Document strElement(Node ArticlesNode,Document document,Object t){
        try {
            //动态加载类
            Class cls = Class.forName(t.getClass().toString().split(" ")[1]);
            List<Field> fieldList = new ArrayList<Field>();
            Node nodeItem =null;
            while(cls != null){
                //当父类为null的时候说明到达了最上层的父类(Object类)
                fieldList.addAll(Arrays.asList(cls .getDeclaredFields()));
                //得到父类,然后赋给自己
                cls = cls.getSuperclass();
            }
            Element item = document.createElement("item");
            ArticlesNode.appendChild(item);
            nodeItem = document.getElementsByTagName("item").item(0);
            for(Field f : fieldList){
                //true  可以获取public,private修饰的字段
                f.setAccessible(true);
                //时间戳不用加  微信官网的参数例子
                Element children = document.createElement(f.getName());
                children.setTextContent("+f.get(t)+"]]>");
                nodeItem.appendChild(children);
            }
        } catch (ClassNotFoundException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return document;
    }

    public static void main(String[] args){
        //测试  生成string   xml
        WxAppImgTextMessage wxAppImgTextMessage = new WxAppImgTextMessage();
        WxAppArticle wxAppArticle = new WxAppArticle();
        wxAppImgTextMessage.setMsgType("text");
        SimpleDateFormat sdf2=new SimpleDateFormat("yyyy-MM-dd");
        wxAppImgTextMessage.setCreateTime(sdf2.format(new Date()));
        wxAppImgTextMessage.setToUserName("toUserName");
        wxAppImgTextMessage.setFromUserName("fromUserName");
        wxAppImgTextMessage.setArticleCount("1");
        wxAppArticle.setPicUrl("图片地址");
        wxAppArticle.setUrl("图文连接");
        wxAppArticle.setTitle("图文标题");
        wxAppArticle.setDescription("图文描述");
        Class classStr = wxAppImgTextMessage.getClass();
        System.err.println("class: "+classStr.toString().split(" ")[1]);
        //String className = "com.ruoyi.web.controller.wxapp.WxAppTextMessage";
        Map<String,Object> resultMap =  ObjectToMap(wxAppImgTextMessage);
//        Map resultMap2 = ObjectToMap(wxAppArticle);
        System.err.println("resultMap: "+resultMap);
        String xmlStr ="";
        String xmlStrItem = "";
        // XmlUtil.mapToXmlStr(resultMap,"xml");
        Document document = XmlUtil.mapToXml(resultMap,"xml");
        Element element = document.createElement("Articles");
        Node node = document.getElementsByTagName("xml").item(0);
        node.appendChild(element);
        //Element item = document.createElement("item");
        Node ArticlesNode = document.getElementsByTagName("Articles").item(0);
        //ArticlesNode.appendChild(item);
        document = strElement(ArticlesNode,document,wxAppArticle);
        //Articles
        xmlStr = XmlUtil.toStr(document);
        xmlStr = xmlStr.replaceAll("<","<");
        xmlStr = xmlStr.replaceAll(">",">");
        System.err.println("xmlStr: "+xmlStr);
        System.err.println(getSecondTimestampTwo(new Date()));
    }
    /**
     * 获取精确到秒的时间戳
     * @param date
     * @return
     */
    public static String getSecondTimestampTwo(Date date){
        if (null == date) {
            return "";
        }
        String timestamp = String.valueOf(date.getTime()/1000);
        return timestamp;
    }

WxAppMessageBase 消息公用实体

package com.ruoyi.web.controller.wxapp;


import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;


/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/19 10:36
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppMessageBase {

    /**
     * 接收方账号或者发送方账号  (openId)
     */
     private String ToUserName;

    /**
     * 开发者微信号
     */
    private String FromUserName;

    /**
     * 消息创建时间  整形
     */
     private String CreateTime;

    /**
     * 消息类型
     */
    private String MsgType;

}

WxAppTextMessage 文本消息

package com.ruoyi.web.controller.wxapp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/19 10:35
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppTextMessage  extends WxAppMessageBase{

    private String Content;

}

WxAppImgTextMessage 图文消息

package com.ruoyi.web.controller.wxapp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.List;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/20 11:38
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppImgTextMessage extends WxAppMessageBase{

    /**
     * 图文消息个数
     */
    private String ArticleCount;


}

WxAppArticle文章实体

package com.ruoyi.web.controller.wxapp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author qb
 * @version 1.0
 * @Description
 * @date 2020/10/20 11:39
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppArticle {

    /**
     * 图文消息标题
     */
    private String Title;

    /**
     * 图文消息描述
     */
    private String Description;

    /**
     * 图片链接  支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
     */
    private String PicUrl;

    /**
     * 点击图文消息跳转链接
     */
    private String Url;

}

WxAppImgMessage 图片消息

package com.ruoyi.web.controller.wxapp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author qb
 * @version 1.0
 * @Description  图片消息
 * @date 2020/10/16 17:53
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode
public class WxAppImgMessage extends WxAppMessageBase{

    /**
     * 通过素材管理中的接口上传多媒体文件,得到的id。
     *
     */
    private String MediaId;


}

controller 此地址要去微信公众号后台配置,推送的事件回调

  @RequestMapping(value = "/WxPushNews")
    public String WxPushNews() throws IOException, AesException {
       String result = WxAppMenuUtils.WxAppEvent();
       return result;
    }

公众号效果图

1.文本消息截图
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第6张图片
2.图文消息截图
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第7张图片

注意:

1. 异常java.security.InvalidKeyException:illegal Key Size的解决方案

  • 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址):
  •  http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
    
  • 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  • 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  • 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件

2. 数据加密和解密所用的包(WXBizMsgCrypt )

  • 1.在官方网站下载dome,传送门

  • 2.可以看demo,查看数据加密、解密的参数说明
    

获取微信加密和解密的jar,图片截图

vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第8张图片
解压后
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第9张图片

文件目录
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第10张图片
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第11张图片

将dist包中的jar,复制到项目lib(没有就新建一个)中,
vue及springboot前后端分离实现微信公众号授权、自定义菜单、订阅推送消息_第12张图片
idea选中jar右键添加为库,即可

你可能感兴趣的:(微信公众号,java,spring,boot,vue.js)