微信公众号开发
一、准备
1.1、内网穿透NATAPP
下载链接: https://natapp.cn/
购买隧道
配置隧道
根据系统版本下载对应的客户端
解压,在同级目录下创建config.ini文件,并添加相应的配置
authtoken更改为自己的authtoken
#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=2427122cd5c09be9 #对应一条隧道的authtoken
clienttoken= #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy= #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空
启动,双击natapp.exe
1.2、微信公众号测试号
地址: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
1.2.1、微信扫码登录
可以得到appID和appsecret,后面会用到
在这里插入图片描述
1.2.2、接口配置信息
URL为NATAPP生成的域名+/wechat/checkToken
1.2.3、JS接口安全域名
域名是NATAPP生成的域名
1.2.4、网页账号
二、开发
2.1、配置
2.1.1、添加依赖
<dependencies>
<!-- springboot整合freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>4.5.3</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.1.2、yml配置
server:
port: 9090
spring:
freemarker:
suffix: .html # 设置模板后缀名
content-type: text/html # 设置文档类型
charset: UTF-8 # 设置页面编码格式
cache: false # 设置页面缓存
template-loader-path: classpath:/templates # 设置页面文件路径
settings:
numberFormat: 0.## #数字格式
datetimeFormat: yyyy-MM-dd HH:mm:ss #日期时间格式
dateFormat: yyyy-MM-dd #日期格式
timeFormat: HH:mm:ss #时间格式
tagSyntax: autoDetect #自动检测标签语法
urlEscapingCharset: UTF-8 #URL转码字符集
classicCompatible: true #解决前台使用${}赋值值为空的情况
localizedLookup: false
redis:
host: localhost
port: 6379
password:
database: 1
# 微信
wechat:
# APPNAT生成的域名
server: http://xndj83.natappfree.cc
appid: 换成你自己的appid
appsecret: 换成你自己的appsecret
2.2、前端配置
在templates目录下新建success.html和subscribe.html文件
subscribe.html
<!doctype html>
<html class="height1">
<head>
<title>关注公众号</title>
</head>
<body>
<div>
<div>
<div>
<img src="${static}/images/img.png" alt="公众号二维码">
<div>请先关注【用生命研发技术】公众号 再进行扫描 (长按图片识别)</div>
</div>
</div>
</div>
</body>
</html>
success.html
<!doctype html>
<html class="height1">
<head>
<title>关注公众号</title>
</head>
<body>
<div>
<div>
<div>
<img src="${static}/images/img.png" alt="公众号二维码">
<div>请先关注【用生命研发技术】公众号 再进行扫描 (长按图片识别)</div>
</div>
</div>
</div>
</body>
</html>
在static目录下新建images目录,将微信公众号的二维码复制到images目录下,并重命名为img.png
注意:测试号不关注公众号无法进行操作,并且测试号最多100人关注
2.3、Java开发
新建WechatController类
@RestController
@RequestMapping("/wechat")
public class WechatController {
//openId key
private static final String WECHAT_OPEN_ID_SESSION_KEY = "WECHAT_OPEN_ID_SESSION_KEY";
@Value("${wechat.server}")
private String SERVER;
@Value("${wechat.appid}")
private String appid;
@Value("${wechat.appsecret}")
private String appsecret;
@Autowired
private RedisService redisService;
/**
* @param request
* @param response
* @throws IOException
* @Author: xuwendong
* Token验证 发者接入验证 确认请求来自微信服务器
*/
@GetMapping("/checkToken")
public void checkToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
//消息来源可靠性验证
String signature = request.getParameter("signature");// 微信加密签名
String timestamp = request.getParameter("timestamp");// 时间戳
String nonce = request.getParameter("nonce"); // 随机数
String echostr = request.getParameter("echostr");//成为开发者验证
//确认此次GET请求来自微信服务器,原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败
PrintWriter out = response.getWriter();
if (WxUtil.checkToken(signature, timestamp, nonce)) {
System.out.println("=======请求校验成功======" + echostr);
out.print(echostr);
}
out.close();
out = null;
}
/**
* @Author: xuwendong
* Access token
* access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token
* access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
*/
@GetMapping("/getAccessToken")
public synchronized String getAccessToken() {
String accessToken = (String) redisService.get("ACCESS_TOKEN");
if (accessToken == null) {
String result = WxUtil.getAccessToken(appid, appsecret);
JSONObject jsonObject = JSONUtil.parseObj(result);
//根据key获取json对象的值
String access_token = Convert.toStr(jsonObject.get("access_token"));
//根据key获取json对象的值
Long expires_in = Convert.toLong(jsonObject.get("expires_in"));
//将key存储到redis中,并设置过期时间
redisService.set("ACCESS_TOKEN", access_token, expires_in, TimeUnit.SECONDS);
System.out.println("往redis中存入 access_token = " + access_token);
return access_token;
}
return accessToken;
}
/**
* @Author: xuwendong
* 获取微信API接口 IP地址
* 参数 是否必须 说明
* access_token 是 公众号的access_token
*/
@GetMapping("getWechatAPI")
public String getWechatAPI() {
String accessToken = getAccessToken();
System.out.println("从redis中取access_token: " + accessToken);
String wechatAPI = WxUtil.getWechatAPI(accessToken);
return wechatAPI;
}
/**
* @param response
* @throws IOException
* @Author: xuwendong
* @Author: xuwendong
* 微信授权接口
*/
@GetMapping("/authorize")
public void authorize(HttpServletResponse response) throws IOException {
String returnUrl = "";
try {
returnUrl = URLEncoder.encode(SERVER + "/wechat/userInfo", "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 按照文档要求拼接访问地址
String url = "https://open.weixin.qq.com/connect/oauth2/authorize" +
"?appid=" + appid
+ "&redirect_uri=" + returnUrl
+ "&response_type=code"
+ "&scope=snsapi_userinfo"
+ "&state=STATE#wechat_redirect";
System.out.println("redirectUrl=" + url);
// 跳转到要访问的地址;
response.sendRedirect(url);
}
/**
* @param code
* @param state
* @return
* @Author: xuwendong
* 获取微信openId接口
*/
@GetMapping("/userInfo")
public ModelAndView userInfo(@RequestParam("code") String code, @RequestParam("state") String state) {
System.out.println("code = " + code);
ModelAndView view = new ModelAndView();
//通过code换取网页授权access_token
String tokenJsonStr = WxUtil.getWechatAccessToken(appid, appsecret, code);
//String转JSON对象
JSONObject tokenJsonObj = JSONUtil.parseObj(tokenJsonStr);
//获取openId
String openId = tokenJsonObj.getStr("openid");
//获取Access token
String accessToken = getAccessToken();
//微信公众号获取用户信息
String userInfoJsonStr = WxUtil.getWechatUserInfo(accessToken, openId);
//String转JSON对象
JSONObject userInfoJsonObj = JSONUtil.parseObj(userInfoJsonStr);
System.out.println("userInfoJsonObj = " + userInfoJsonObj);
// 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息
int subscribe = userInfoJsonObj.getInt("subscribe");
if (subscribe == 0) {
// 跳转未关注公众号界面,让用户去关注公众号
view.setViewName("subscribe");
return view;
}
HttpServletRequest request = getRequest();
HttpSession session = request.getSession();
session.setAttribute(WECHAT_OPEN_ID_SESSION_KEY, openId);
//获取回调地址
//String redirect_uri = request.getParameter("redirect_uri");
//System.out.println("redirect_uri = " + redirect_uri);
//view.setViewName("redirect:" + SERVER+"/wechat/success");
//这里是测试使用
String country = userInfoJsonObj.getStr("country");
String province = userInfoJsonObj.getStr("province");
String city = userInfoJsonObj.getStr("city");
String openid = userInfoJsonObj.getStr("openid");
String nickname = userInfoJsonObj.getStr("nickname");
String headimgurl = userInfoJsonObj.getStr("headimgurl");
view.addObject("country",country);
view.addObject("province",province);
view.addObject("city",city);
view.addObject("openid",openid);
view.addObject("nickname",nickname);
view.addObject("headimgurl",headimgurl);
view.setViewName("success");
return view;
}
/**
* @Author xuwendong
* 获取request
*/
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
return request;
}
}
新建WxUtil类
public class WxUtil {
//wxToken验证
private static String wxToken = "wxToken";
/**
* @Author: xuwendong
* Token验证
* @param signature
* @param timestamp
* @param nonce
*/
public static boolean checkToken(String signature, String timestamp, String nonce) {
List<String> params = new ArrayList<String>();
params.add(wxToken);
params.add(timestamp);
params.add(nonce);
// 1. 将token、timestamp、nonce三个参数进行字典序排序
Collections.sort(params, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
// 2.将三个参数字符串拼接成一个字符串进行sha1加密
String temp = SHA1.encode(params.get(0) + params.get(1) + params.get(2));
// 3. 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
return temp.equals(signature);
}
/**
* @Author: xuwendong
* 通过code换取网页授权access_token
* @param appid 公众号的唯一标识
* @param secret 公众号的唯一标识
* @param code 获取的code参数
* @return
*/
public static String getWechatAccessToken(String appid, String secret, String code) {
String url = "https://api.weixin.qq.com/sns/oauth2/access_token";
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("appid", appid);
paramMap.put("secret", secret);
paramMap.put("code", code);
paramMap.put("grant_type", "authorization_code");
return HttpUtil.get(url, paramMap);
}
/**
* @Author: xuwendong
* 微信公众号获取用户信息
* @param access_token
* @param openid
* 返回参数
* 参数 描述
* openid 用户的唯一标识
* nickname 用户昵称
* sex 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
* province 用户个人资料填写的省份
* city 普通用户个人资料填写的城市
* country 国家,如中国为CN
* headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
* privilege 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
* unionid 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
*/
public static String getWechatUserInfo(String access_token, String openid) {
String url = "https://api.weixin.qq.com/cgi-bin/user/info";
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("access_token", access_token);
paramMap.put("openid", openid);
paramMap.put("lang", "zh_CN");
return HttpUtil.get(url, paramMap);
}
/**
* @Author: xuwendong
* 获取微信API接口 IP地址
* @param accessToken
*/
public static String getWechatAPI(String accessToken) {
String url = "https://api.weixin.qq.com/cgi-bin/get_api_domain_ip";
Map<String, Object> paramMap = new HashMap<>(1);
paramMap.put("access_token", accessToken);
return HttpUtil.get(url, paramMap);
}
/**
* @Author: xuwendong
* 请求接口, 获取Access token
* @param appid
* @param appsecret
* @return
*/
public synchronized static String getAccessToken(String appid, String appsecret) {
String url = "https://api.weixin.qq.com/cgi-bin/token";
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("appid", appid);
paramMap.put("secret", appsecret);
paramMap.put("grant_type", "client_credential");
return HttpUtil.get(url, paramMap);
}
}
新建SHA1类
public class SHA1 {
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
// 把密文转换成十六进制的字符串形式
private static String getFormattedText(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
// 把密文转换成十六进制的字符串形式
for (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}
public static String encode(String str) {
if (str == null) {
return null;
}
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(str.getBytes());
return getFormattedText(messageDigest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
三、微信自定义菜单
查询菜单
在这里插入图片描述
添加菜单并发布
四、测试
关注公众号,测试