微信公众号开发那些事

前言:最近接触的新项目是关于微信公众号的相关开发,其中会涉及到菜单栏的创建,配置跳转页面等等,在这过程中也是一边查资料一边摸索,也遇到一些坑,在这里分享下,如果有错误的地方或者写的不好的还请指点,也欢迎有疑问可以一起探讨。

一、准备工作

1. 微信SDK的选择

开发不一定就是自己造轮子,有优秀的开源的SDK那我们完全可以使用,这样可以避免采坑,还有繁杂的封装,从而提高开发效率 。这里我选择的是一位大佬的优秀SDK,作者一直在更新,目前已经来到了3.8.0版本。

1.1. SDK源码地址: github.

1.2. SDK部分Maven 依赖,可根据需求引入参考

		<dependency>
            <groupId>com.github.binarywanggroupId>
            <artifactId>(根据不同需求引入参考以下模板)artifactId>
            <version>3.8.0version>
        dependency>
ArtifactId
微信小程序:weixin-java-miniapp
微信支付:weixin-java-pay
微信开放平台:weixin-java-open
公众号(包括订阅号和服务号):weixin-java-mp
企业号/企业微信:weixin-java-cp
(由于我是公众号开发,这里用到的是 weixin-java-mp)

1.3. JAVA项目相关配置

1.3.1. 配置application.yml(详细信息及对应公众平台配置值见目录 2.公众平台相关配置)

business:
  config:
    wechat:
      appId: # 公众号appid
      secret: # 公众号的appsecret
      token: # 接口配置里的Token值
      aesKey: # 接口配置里的EncodingAESKey值
      authUrl:

1.3.2. 配置SDK相关配置类
WeChatAppConfig.java 用来加载yml中的配置信息

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "business.config.wechat")
public class WeChatAppConfig {
     
     /**
     * 设置微信公众号的appId
     */
    private String appId;
    /**
     * 设置微信公众号的app secret
     */
    private String secret;
    /**
     * 设置微信公众号的token
     */
    private String token;
    /*
     * 设置微信公众号的 EncodingAESKey
     */
    private String aesKey;
    /**
     * 权限URL
     */
    private String authUrl;
}

SDK微信服务核心配置 WeChatMpConfig.java

import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(WxMpService.class)
@EnableConfigurationProperties(WeChatAppConfig.class)
public class WeChatMpConfig {
     

    @Autowired
    private WeChatAppConfig weChatAppConfig;

    /**
     * 用户取消关注事件处理服务
     */
    @Autowired //如果不需要用到微信的相关事件可以不用
    private UnsubscribeHandler unsubscribeHandler;

    @Bean
    @ConditionalOnMissingBean
    public WxMpConfigStorage wxMpConfigStorage() {
     
        WxMpDefaultConfigImpl configStorage = new WxMpDefaultConfigImpl();
        configStorage.setAppId(weChatAppConfig.getAppId());
        configStorage.setSecret(weChatAppConfig.getSecret());
        configStorage.setToken(weChatAppConfig.getToken());
        configStorage.setAesKey(weChatAppConfig.getAesKey());
        return configStorage;
    }

    @Bean
    @ConditionalOnMissingBean
    public WxMpService wxMpService(WxMpConfigStorage configStorage) {
     
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(configStorage);
        return wxMpService;
    }

    @Bean //如果不需要用到微信的相关事件可以不用配置此项
    public WxMpMessageRouter router(WxMpService wxMpService) {
     
        final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
        // 取消关注事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.UNSUBSCRIBE)
                .handler(this.getUnsubscribeHandler()).end();
        return newRouter;
    }

    protected UnsubscribeHandler getUnsubscribeHandler() {
     
        return this.unsubscribeHandler;
    }
}

2. 公众号平台相关配置

微信公众号平台登录: 链接地址.

2.1. 公众号基本配置: 开发>基本配置

微信公众号开发那些事_第1张图片

  1. 开发者ID(AppID):application.yml配置中 -> appId
  2. 开发者密码(AppSecret):application.yml配置中 -> secret,目前平台要求生成后密码由自己保存管理,如果忘记可以进行重置
  3. IP白名单:对于去请求调用获取access_token 接口的服务端或者前端需要将其所在IP设置为白名单,多个IP地址配置可用回车键隔开
  4. 服务器地址(URL):这里配置的是开发者服务器地址。用于处理用户被动回复消息(用户给公众号发消息,微信服务器会将消息发送到开发者设置的当前服务器地址,开发者进行消息验证,公众号进行回复),关注,取关,扫码等等一些事件,来响应微信服务器发送的一些验证或者消息
  5. 令牌(Token):application.yml配置中 -> token
  6. 消息加解密密钥(EncodingAESKey):application.yml配置中 -> aesKey
  7. 消息加解密方式:根据业务需要可以选择消息加解密类型

2.2. 公众号网页相关配置 公众号设置->功能设置

微信公众号开发那些事_第2张图片

  1. JS接口安全域名:这里设置的是前端页面所在域名,设置后开发者可以在公众号中该域名下调用微信开放的JS接口,只支持域名方式不支持IP地址,端口号等(如 wx.qq.com)不需要http,保存成功后需要点击下载生成的 MP_verify_*.txt 文件上传至填写的域名web服务器,详细可查看设置时平台给的提示
  2. 网页授权域名:用户在网页授权同意后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠,填写保存后同上点1需要上传MP_verify_*.txt 文件上传至填写的域名web服务器

二、实际应用

1.微信网页授权与回调

这里只是一个简单里范例,看个人业务需求选择,SDK中的WxMpService 服务封装了各种常用的微信接口,可以参考git中的文档使用。这里的重定向使用的是MVC中的重定向方式:return "redirect:+url, 而Spring Boot使用了@RestController注解,以下写法只能返回字符串,可以将一个HttpServletResponse参数添加到处理程序方法然后调用response.sendRedirect(url)进行重定向。

package com.wechat.controller;

import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.net.URLEncoder;

@Controller
@RequestMapping("/wechat")
public class WechatAuthController {
     

    /**
     * sdk服务
     */
    @Autowired
    private WxMpService wxMpService;

    /**
     * 请求微信服务器认证授权(网页授权)
     *
     * 用户在公众号中访问第三方的网页,公众号通过当前接口调用微信网页授权,用户确认同意授权后来获取用户基本信息。
     *
     * 回调机制可根据个人业务需求:1.服务端->网页回调  2.服务端->服务端回调,我这里采用的是2方式,
     * 回调服务端来获取用户信息,也可以回调前端,前端根据返回的code在来调用获取用户信息接口
     *
     * @param returnUrl 跳转回调页面地址
     * @return
     */
    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl) throws Exception {
     
        // 网页授权回调地址(这里可以是服务端接口,也可以是回调页面地址)
        String url = "https://你的域名/wechat/userInfo";
        String redirectURL = wxMpService
                .oauth2buildAuthorizationUrl(
                        url,//网页授权回调地址,不管是页面回调,还是服务端接口的回调地址必须在微信平台配置的网页授权域名下,不然会回调失败
                        WxConsts.OAuth2Scope.SNSAPI_USERINFO,//网页授权两种scope,一种是静默授权,一种是非静默方式
                        URLEncoder.encode(returnUrl));//state参数,微信回调重定向会带上该参数,值是不变的,一般做校验用,这里我放的是前端的回调地址。
        return "redirect:" + redirectURL;//由于网页授权回调地址 url我这里是服务端的接口地址,会重定向到获取用户信息接口,获取相关用户信息
    }

    /**
     * 获取用户信息
     *
     * @param code 用户code,使用后即失效,不使用5分钟失效,用于获取用户access_token的
     * @param returnUrl 网页授权时传入的state参数,回调时会原原本本返回
     * @return
     * @throws Exception
     */
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("code") String code,
                           @RequestParam("state") String returnUrl) throws Exception {
     
        WxMpOAuth2AccessToken wxMpOAuth2AccessToken;
        try {
     
            // 获取用户accessToken
            wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
            // 获取用户信息
            WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpOAuth2AccessToken, null);
        } catch (WxErrorException e) {
     
            e.printStackTrace();
            throw new Exception(e.getError().getErrorMsg());
        }
        // 获取openId,用户在一个公众号中的唯一标识
        String openId = wxMpOAuth2AccessToken.getOpenId();

        return "redirect:" + returnUrl + "?openid=" + openId;
    }
}

1.1.如何调用未支持接口

虽然SDK中封装部分常用的接口,但由于是个人开发有时候会比微信开发api慢一些,或者有些不常用的接口是没有提供支持,当然作者在 开发文档也提到,并提供了3个急救方案,这里举个栗子。开发中可能需要知道用户是否关注了公众号,刚好该接口SDK中尚未支持,以下是简单的实现

/**
     * 获取用户信息
     *
     * @param openid 用户唯一标识
     * @return
     */
    private WxMpUser getUserInfo(String openid) {
     
        try {
     
            String responseContent = wxMpService.get(WxMpApiUrl.User.USER_INFO_URL, "openid=" + openid + "&lang=zh_CN");
            WxMpUser wxMpUser = WxMpUser.fromJson(responseContent);
            return wxMpUser;
        } catch (WxErrorException e) {
     
            log.error("get user info error", e);
        }
        throw new CustomException(ResultEnum.LOGIN_FAILED);
    }

这里需要注意的是该接口中,如果用户尚未关注公众号,获取的只有openId信息,而关注了才能获取用户的基本信息。但不管关注未关注返回结果中都有一个 Boolean类型 subscribe字段,可以根据该字段来判断用户是否关注。

2.获取JS-SDK签名信息,调用微信开放JS

在网页中常常需要调用微信开放的JS,在调用前需要先获取签名信息,根据签名信息才能成功调用微信的JS-SDK,详细可以参考 微信JS-SDK说明文档.,这里我是在服务端提供一个获取签名信息的接口,前端在调用前可以先进行签名信息的获取。

import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 获取微信jsdk签名信息
 */
@Controller
@RequestMapping("/wechat/jsdk")
public class WxJsdkController {
     

    /**
     * sdk服务
     */
    @Autowired
    private WxMpService wxMpService;

    @ResponseBody
    @RequestMapping(value = "/getTicket")
    public WxJsapiSignature getTicket(@RequestParam("url") String url) {
     
        try {
     
            WxJsapiSignature jsapi = wxMpService.createJsapiSignature(url);
            return jsapi;
        } catch (WxErrorException e) {
     
            e.printStackTrace();
        }
        // 获取失败
        return null;
    }
}

SDK中的创建签名方法中已经封装好,包括获取签名需要的时间戳,随机串,并且按照开发文档中的要求排序生成签名,保存ticket信息等。以上只是简单的案例,可以根据个人业务需求去封装一个结果,对获取失败进行处理,返回相关错误信息等。一下为WxJsapiSignature 类的数据格式:

{
     
    "appId": "appid",
    "nonceStr": "Rr5RUodeRn8BTArc",
    "signature": "16f72c24f53d69199ac8af56c2a0786ff8baf04e",
    "timestamp": 1596000000,
    "url": "https://www.xx.cn/test/test.html"
  }

三、测试及遇到坑

1.测试号的申请与配置

在微信公众平台开发中,由于功能还在测试阶段需要调试,且微信公众号注册也有一定的门槛,微信公众平台提供了: 接口测试账号申请.用户使用微信登录后就有一个可测试的账号。
微信公众号开发那些事_第3张图片

1.1.微信回调本地服务配置,固定端口内网穿透

根据申请到的测试号的相关配置 appId,secret,url,token,js域名,在平台中及项目中配置好,如果项目在本地,需要微信回调到本地服务,这时我们就需要下载安装一个ngrok.exe内网穿透工具.将本地项目对应的端口映射到一个可以访问的公网地址。下载安装后,工具的命令很简单,假如本地项目端口为8080,如下:
使用cmd切换到ngrok.exe目录,执行命令 ngrok http + 需要映射的端口

ngrok.exe http 8080

回车后结果如下,会生成带时效的 session Expire 这里是 8小时的公网地址,有http和https格式,其域名都是一样的,切记当前窗口要保持开启才有效。将以下的域名配置到测试号平台中如js域名等,这样在请求微信网页授权时候传的回调地址里使用该地址+具体项目接口即可收到微信正常回调。
微信公众号开发那些事_第4张图片

1.2.菜单栏的添加

毕竟是测试账号,不是正规军,它没有提供可视化的公众号菜单或按钮的编辑,所以如果你需要在测试号上添加相应菜单栏,那你只能通过在线通过 微信公众平台在线调试工具页面.去做添加和删除

1.2.1.获取公众号access_token

填写好测试号的appid与secret,点击检查问题请求获取
微信公众号开发那些事_第5张图片
请求结果如下,根据返回的access_token就可以去调用其它接口了
微信公众号开发那些事_第6张图片

1.2.2.根据access_token创建菜单

根据1.2.1请求结果获取的access_token,调用菜单栏创建接口,菜单栏请求body为json格式,可参考自定义菜单创建接口文档中的格式.创建想要的菜单或按钮格式,微信规定最多只能创建3个菜单,每个菜单最多只能有5个子菜单,以下为创建示例,编辑和删除大同小异,这里就不展开了
微信公众号开发那些事_第7张图片
请求结果
微信公众号开发那些事_第8张图片

1.2.2.查看效果

申请的微信测试号即使是通过自己微信号申请,自己也可以扫码关注,通过微信扫码关注就可以看到菜单栏创建的效果,点击可以根据body参数中配置url跳转
微信公众号开发那些事_第9张图片

2.踩过的坑

2.1.1.回调地址url问题

项目测试其,曾把回调地址配置成服务端地址,报错了:redirect_uri域名与后台配置不一致,提示已经很明显了,如果是在PC端访问或者安卓端访问会出现页面跳转不过去,只有ios会有这个错误的弹窗提示,所以如果是非ios可能还不好定位到问题。由于项目是前后端分离,且前端部署在公众平台配置的域名服务器中,而服务端部署在另外的服务器下导致,这种情况解决的办法有两种,将回调改成前端页面回调方式,或者配置nginx代理。

2.1.2.JS-SDK签名url问题

(1).当调用服务端获取签名数据时,正常返回的签名数据,而当前端使用数据调用微信JS分享提示 invalid signature 错误,由于使用的是比较成熟的SDK,且核对了配置可以基本排除以下几个可能:

  1. .config 中的 appid 与用来获取 jsapi_ticket 的 appid 不一致
  2. config中nonceStr, timestamp与用以签名中的对应noncestr, timestamp不一致
  3. 没有缓存access_token和jsapi_ticket

(2).接下来是利用 在线微信JS接口签名校验工具 验证签名是否正确

微信公众号开发那些事_第10张图片
jsapi_ticket 可以通过微信sdk服务打印获取:

String ticket = wxMpService.getTicket(TicketType.JSAPI, false);

通过验证,生成的签名和接口中返回的签名是一致的,排除签名错误的可能

(3).确认微信公众平台是否配置了IP白名单

获取access_token服务端的IP和前端的IP需要配置在白名单中,否则也会出现该为题

(4).JS签名数据接口中,参数url问题

排除里以上问题,可以说基本是url的问题了,一开始核实了以上都没有问题,且在微信的开发文档中按照要求也不觉得是url问题,文档中也说明 url不进行转义,所以在创建时我一直是原参数url创建,此时已经要脑裂,也翻阅了好多网上资料,没有找到问题,最后试了下 将url encode一次后再进行签名创建,结果成功了。也有可能我的url后面是带参数的问题导致,但是文档中并没有提示要进行encode。

你可能感兴趣的:(spring,boot,java)