项目完整代码请访问github:https://github.com/liaozq0426/wx.git
微信公众号配置表设计
在公众号的开发过程中,很多接口都需要读取配置,如appId、appSecret、菜单配置等,因此在开发之前,配置表的设计尤为重要。创建表名为wx_cfg
的配置表,建表语句如下
CREATE TABLE `wx_cfg` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`type` varchar(20) NOT NULL COMMENT '配置项类型',
`name` varchar(20) NOT NULL COMMENT '配置项名称',
`value` varchar(255) DEFAULT NULL COMMENT '配置项值',
`parent_id` int(11) DEFAULT NULL COMMENT '父ID',
`sort` tinyint(4) DEFAULT NULL COMMENT '排序',
`platform` varchar(36) NOT NULL COMMENT '公众号标识',
`wx_type` varchar(15) DEFAULT NULL COMMENT '公众号类型,service:服务号 , subscribe : 订阅号',
`enabled` tinyint(1) DEFAULT '1' COMMENT '是否可用,1:可用,0:不可用',
`desc` varchar(255) DEFAULT NULL COMMENT '描述信息',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
建表语句中每个字段含义都有注释说明,其中以下两个字段详细说明一下
1)parent_id
,这个字段用于配置公众号菜单时,如果有多级菜单,那么子菜单就需要用parent_id字段标识父菜单是谁
2)platform
,当同一配置表用于多公众号开发时,这个字段就可以用于区分不同公众号的配置
配置表创建好后,插入appId、appSecret、Token
INSERT INTO `wx_cfg` (`id`, `type`, `name`, `value`, `parent_id`, `platform`, `enabled`, `desc`) VALUES ('1', 'wx_token', 'wxToken', 'YmRjYjAyN2M1ZjMaYUzI1Mzg1ZmU1MGYaIzBY=', NULL, 'gavin', '1', NULL);
INSERT INTO `wx_cfg` (`id`, `type`, `name`, `value`, `parent_id`, `platform`, `enabled`, `desc`) VALUES ('2', 'wx_key', 'appId', 'd3hiODU5ODYwMDY0NmM1MKL1', NULL, 'gavin', '1', NULL);
INSERT INTO `wx_cfg` (`id`, `type`, `name`, `value`, `parent_id`, `platform`, `enabled`, `desc`) VALUES ('3', 'wx_key', 'appSecret', 'NjYzNzhkYTVlN2QxMDdhZmUyZTg3MTMwYjRlUGAZI=', NULL, 'gavin', '1', NULL);
注意
1)在开发的时候需要使用自己申请的微信公众号配置信息。
2)appId、appSecret、Token都使用了Base64加密。
插入完成后可以看一下此时wx_cfg表中已经有了三条记录
接入校验接口开发
接下来编写mapper来操作配置表,mapper接口中需要一个查询方法
package com.gavin.mapper;
import java.util.List;
import com.gavin.pojo.WxCfg;
public interface WxCfgMapper {
public List<WxCfg> select(WxCfg wxCfg);
}
WxCfgMapper.xml实现如下
<mapper namespace="com.gavin.mapper.WxCfgMapper">
<resultMap type="com.gavin.pojo.WxCfg" id="WxCfgMap">
<id column="id" property="id" />
<result column="type" property="type" />
<result column="name" property="name" />
<result column="value" property="value" />
<result column="parent_id" property="parentId" />
<result column="platform" property="platform" />
<result column="wx_type" property="wxType" />
<result column="enabled" property="enabled" jdbcType="TINYINT" javaType="Boolean" />
<result column="desc" property="desc" />
</resultMap>
<select id="select" parameterType="com.gavin.pojo.WxCfg" resultMap="WxCfgMap">
select id
,type
,name
,IF(type='menu' or type = 'url',concat((SELECT value FROM wx_cfg WHERE type = 'app_url' AND enabled = 1 AND platform = #{platform}) , value) , value) AS value
,parent_id
,platform
,wx_type
,enabled
,a.desc
from wx_cfg a
<trim prefix="where" suffixOverrides="and|AND|or|OR">
<if test="id != null and id ">> 0 ">
id = #{id} and
</if>
<if test="type != null and type !='' ">
type = #{type} and
</if>
<if test="name != null and name !='' ">
name = #{name} and
</if>
<if test="parentId == null">
parent_id is null and
</if>
<if test="parentId != null and parentId > 0">
parent_id = #{parentId} and
</if>
<if test="platform != null and platform !='' ">
platform = #{platform} and
</if>
<if test="enabled != null">
enabled = #{enabled} and
</if>
</trim>
<if test="orderBy != null and orderBy != '' ">
order by ${orderBy}
</if>
</select>
</mapper>
接下来编写service接口,service接口中有三个方法,分别用来查询列表和单条记录,以及微信公众号的key信息
package com.gavin.service;
import java.util.List;
import com.gavin.pojo.Wechat;
import com.gavin.pojo.WxCfg;
public interface WxCfgService {
List<WxCfg> select(WxCfg cfg) throws Exception;
WxCfg selectOne(WxCfg cfg) throws Exception;
Wechat selectWechat(String platform) throws Exception;
}
实现代码如下
package com.gavin.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.gavin.cfg.WxCfgEnum;
import com.gavin.mapper.WxCfgMapper;
import com.gavin.pojo.Wechat;
import com.gavin.pojo.WxCfg;
import com.gavin.service.WxCfgService;
@Service
public class WxCfgServiceImpl implements WxCfgService{
@Autowired
private WxCfgMapper wxCfgMapper;
/**
* @title 查询WxCfg集合
* @author gavin
* @date 2019年11月25日
*/
@Override
public List<WxCfg> select(WxCfg cfg) throws Exception {
return wxCfgMapper.select(cfg);
}
/**
* @title 查询WxCfg单条记录
* @author gavin
* @date 2019年11月25日
*/
@Override
public WxCfg selectOne(WxCfg cfg) throws Exception {
List<WxCfg> cfgList = select(cfg);
if(cfgList.size() == 1)
return cfgList.get(0);
return null;
}
/**
* @title 查询微信公众号appId和appSecret
* @author gavin
* @date 2019年5月24日
* @param platform
* @return
* @throws Exception
*/
@Override
public Wechat selectWechat(String platform) throws Exception {
WxCfg cfgParam = new WxCfg();
cfgParam.setPlatform(platform);
cfgParam.setType(WxCfgEnum.WX_KEY_APPID.getType());
List<WxCfg> cfgList = wxCfgMapper.select(cfgParam);
Wechat wechat = new Wechat();
for(WxCfg cfg : cfgList) {
String name = cfg.getName();
if(WxCfgEnum.WX_KEY_APPID.getName().equals(name)) {
// 获取appId
String appId = cfg.getValue();
wechat.setAppId(appId);
}else if(WxCfgEnum.WX_KEY_APPSECRET.getName().equals(name)) {
// 获取appSecret
String appSecret = cfg.getValue();
wechat.setAppSecret(appSecret);
}
}
return wechat;
}
}
上述代码中使用到了一个枚举WxCfgEnum,代码如下
package com.gavin.cfg;
public enum WxCfgEnum {
APP_URL("app_url" , "appUrl")
,WX_TOKEN("wx_token" , "wxToken")
,WX_KEY_APPID("wx_key" , "appId")
,WX_KEY_APPSECRET("wx_key" , "appSecret")
,MENU("menu")
;
private String type; // 对应wx_cfg表中的type字段
private String name; // 对应wx_cfg表中的name字段
private WxCfgEnum(String type , String name) {
this.type = type;
this.name = name;
}
private WxCfgEnum(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
接下来编写controller,实现接入接口
package com.gavin.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.gavin.cfg.WxAccessVo;
import com.gavin.service.WxCfgService;
@RestController
public class WxController {
@Autowired
private WxCfgService wxCfgService;
@GetMapping("wxIn")
public String wxIn(WxAccessVo accessVo) {
try {
return wxCfgService.wxInVerify(accessVo);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
代码中用到的WxAccessVo定义如下,用于接收微信传过来的参数
package com.gavin.pojo;
/**
* @title 公众号接口校验bean
* @author gavin
* @date 2019年11月25日
*/
public class WxAccessVo {
private String signature;
private String timestamp;
private String nonce;
private String echostr;
private String platform;
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
public String getEchostr() {
return echostr;
}
public void setEchostr(String echostr) {
this.echostr = echostr;
}
}
在WxCfgService新增wxInVerify接口
String wxInVerify(WxAccessVo accessVo) throws Exception;
实现代码如下
/**
* @title 微信公众号接入认证
* @author gavin
* @date 2019年11月25日
*/
@Override
public String wxInVerify(WxAccessVo accessVo) throws Exception{
boolean result = false;
if(accessVo == null)
return null;
String platform = accessVo.getPlatform();
if (!StringUtils.isBlank(platform)) {
// 从数据库中查询Token
WxCfg wxCfg = new WxCfg();
wxCfg.setEnabled(true);
wxCfg.setType(WxCfgEnum.WX_TOKEN.getType());
wxCfg.setPlatform(platform);
WxCfg wxCfgA = this.selectOne(wxCfg);
String token = wxCfgA.getValue();
token = EncryptionUtil.base64Decode(token);
// 校验签名
result = WxUtil.verifySignature(accessVo.getSignature(), token, accessVo.getNonce(), accessVo.getTimestamp());
}
System.out.println(accessVo.getPlatform() + "配置微信公众号" + (result ? "成功" : "失败"));
// 如果校验成功则返回echostr,否则返回空
return result ? accessVo.getEchostr() : null;
}
其中签名校验封装成了WxUtil类的一个方法,verifySignature代码如下
/**
* @title 微信公众号服务器配置校验
* @param signatureWx
* @param token
* @param nonce
* @param timestamp
* @return
*/
public static boolean verifySignature(String signatureWx , String token , String nonce , String timestamp) throws Exception{
if(StringUtils.isBlank(signatureWx))
return false;
String[] arr = {token , nonce , timestamp};
StringBuffer sb = new StringBuffer();
// 排序
Arrays.sort(arr);
// 拼接字符串
sb.append(arr[0]).append(arr[1]).append(arr[2]);
// sha1算法加密
String signature = DigestUtils.sha1Hex(sb.toString());
return signatureWx.equals(signature);
}
到这里,接入认证接口就开发完成了。接下来开启natapp内网穿透,登录到微信公众号后台,在基本配置
中修改配置,在URL
中输入刚刚开发好的wxIn接口,并带上platform参数,如下图
输入完成后,点击下方提交
按钮,如果校验成功页面会弹出如下提示
如果校验失败页面会弹出如下提示