1.流程概述
1.1 申请公众号
1.2 创建带参数的公众号二维码,参数值为scen_id的值
1.3 微信基础配置接口编写,get方式的接口为微信测试接口,必须能正常访问,post方式的接口为扫码回调接口,从请求中获取微信返回的xml包数据,其中eventKey为二维码的scen_id参数值,event为事件类型,subscribe为关注事件,scan为扫码事件,unsubscribe为取消关注事件,根据自己的具体业务需求来处理即可,两个接口得请求路径必须相同
1.4 公众号接口基础配置
2.申请测试公众号
地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
访问该地址,直接微信扫码登录即可,扫码成功后进入如下界面:
3.获取带参数的公众号二维码
3.1 通过appID和appsecret获取accessToken,测试公众号每天只能获取2000次
以get方式向微信发送请求即可(https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET)
public static String getAccessToken(String appSecret,String appId) {
String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type
+ "&appid=" + appId + "&secret=" + appSecret;
// 获取accessToken
String accessTokenBody = cn.hutool.http.HttpUtil.get(accessTokenUrl);
log.info("获取accessToken{}",accessTokenBody);
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(accessTokenBody);
String accessToken = null;
try {
accessToken = (String) jsonObject.get("access_token");
log.info("accessToken:{}",accessToken);
} catch (Exception e) {
log.error("获取accessToken异常");
}
return accessToken;
}
3.2 通过上一步获取的accessToken创建二维码的ticket
二维码有临时二维码和永久二维码两种,创建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”}}}
这里要注意的是:场景scen里的属性scen_id的值,如果是以0开头,会将0忽略,文档里也没有具体说明,测试的结果是这样的,而且值的长度不能过长,否则扫码只会获取到的eventKey会获取不到,或则只有一部分。
String ticketUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;
JSONObject ticketParam = JSONUtil.createObj();
JSONObject scene = JSONUtil.createObj();
String key = "1" + GenerationSequenceUtil.getRandNum(8);
scene.put("scene_id", key);
log.info("scene:{}",scene);
JSONObject actionInfo = JSONUtil.createObj();
actionInfo.put("scene",scene);
ticketParam.put("action_info", actionInfo);
ticketParam.put("action_name", "QR_SCENE");
ticketParam.put("expire_seconds", expireTime);
log.info("ticketParam:{}",ticketParam);
// ticketParam转json字符串
String ticketParamToString = com.alibaba.fastjson.JSONObject.toJSONString(ticketParam);
// 获取ticket
String ticketBody = cn.hutool.http.HttpUtil.post(ticketUrl,ticketParamToString);
log.info("获取ticketBody{}",ticketBody);
com.alibaba.fastjson.JSONObject ticketContent = com.alibaba.fastjson.JSONObject.parseObject(ticketBody);
String ticket = (String) ticketContent.get("ticket");
3.3 拼接url,返回url到前端获取二维码
上一步拿到的ticket直接拼接为该url参数即可。https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKE
3.4获取二维码的完整代码
工具类:
package com.school.util;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.school.base.Constant;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class QRCodeUtil {
private static final String grant_type = "client_credential";
private static final Integer expireTime = 24*60*60*1000;
public static JSONObject createQRCode(String userId,String appId,String appSecret) {
String accessToken = getAccessToken(appSecret,appId);
String ticketUrl = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;
log.info("ticketUrl:{}",ticketUrl);
JSONObject ticketParam = JSONUtil.createObj();
JSONObject scene = JSONUtil.createObj();
// 生成一个随机值
String key = "1" + GenerationSequenceUtil.getRandNum(8);
scene.put("scene_id", key);
log.info("scene:{}",scene);
JSONObject actionInfo = JSONUtil.createObj();
actionInfo.put("scene",scene);
ticketParam.put("action_info", actionInfo);
ticketParam.put("action_name", "QR_SCENE");
ticketParam.put("expire_seconds", expireTime);
log.info("ticketParam:{}",ticketParam);
// ticketParam转json字符串
String ticketParamToString = com.alibaba.fastjson.JSONObject.toJSONString(ticketParam);
// 获取ticket
String ticketBody = cn.hutool.http.HttpUtil.post(ticketUrl,ticketParamToString);
log.info("获取ticketBody{}",ticketBody);
com.alibaba.fastjson.JSONObject ticketContent = com.alibaba.fastjson.JSONObject.parseObject(ticketBody);
String ticket = (String) ticketContent.get("ticket");
String QRCodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
JSONObject returnJson = JSONUtil.createObj();
returnJson.put("url",QRCodeUrl);
returnJson.put("key",key);
return returnJson;
}
public static String getAccessToken(String appSecret,String appId) {
String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type
+ "&appid=" + appId + "&secret=" + appSecret;
// 获取accessToken
String accessTokenBody = cn.hutool.http.HttpUtil.get(accessTokenUrl);
log.info("获取accessToken{}",accessTokenBody);
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(accessTokenBody);
String accessToken = null;
try {
accessToken = (String) jsonObject.get("access_token");
log.info("accessToken:{}",accessToken);
} catch (Exception e) {
log.error("获取accessToken异常");
}
return accessToken;
}
}
接口:
/**
* 获取公众号二维码
*/
@ApiOperation(value = "获取公众号二维码")
@GetMapping("/createQRCode")
@Pass
public ResponseModel<cn.hutool.json.JSONObject> createQRCode(@RequestParam(name = "userId",required = false) String userId) {
cn.hutool.json.JSONObject qrCode = QRCodeUtil.createQRCode(userId,appId,appSecret);
String key = (String) qrCode.get("key");
if (userId != null) {
redisTemplate.opsForValue().set(key + ":userId",userId);
} else {
redisTemplate.opsForValue().set(key, 0);
}
return ResponseHelper.succeed(qrCode);
}
解释:这里返回的是二维码和一个随机值,然后用redis以该随机值为key,value初始值为0存储到redis中,到这里二维码的创建就完成了,存储随机值的目的是,用户扫码之后,我们在微信调用回调接口时,处理业务逻辑之后,是将结果返回给微信,无法返回到前端,这里就需要前端根据刚才的随机值来获取扫码结果,我们将扫码后的登录结果存储到redis中即可
4.编写微信基础配置接口
前面已经将带参数的二维码生成了,这里需要两个接口,而且路径必须相同,一个是get方式的,这个接口是用于微信调用,测试接口是否畅通,如果该接口未通,测试公众号这边的基础配置就会配置失败,另一个为post请求,微信会给改接口发送xml包,接口接收到后根据xml包数据进行业务处理
/***
* 微信服务器触发get请求用于检测签名
* @return
*/
@GetMapping("/receiveWx")
@ResponseBody
@Pass
public void handleWxCheckSignature(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.getWriter().println(request.getParameter("echostr"));
}
/**
* 微信公众号回调事件
*
* @param request
* @return
*/
@Pass
@PostMapping(value = "/receiveWx")
public void checkWxToken(HttpServletRequest request,HttpServletResponse response) throws IOException, DocumentException {
Map<String, String> xmlToMap = xmlToMap(request);
log.info("map:{}",xmlToMap);
String eventKey = xmlToMap.get("EventKey");
String event = xmlToMap.get("Event");
String openId = xmlToMap.get("FromUserName");
String toUserName = xmlToMap.get("ToUserName");
String accessToken = QRCodeUtil.getAccessToken(appSecret,appId);
TextMessage text = new TextMessage();
text.setFromUserName(toUserName);
text.setToUserName(openId);
text.setCreateTime(System.currentTimeMillis() + "");
text.setMsgType("text");
// try {
if (ComUtil.isNotEmpty(event)) {
if (event.equals("subscribe")) {
eventKey = eventKey.replaceAll("qrscene_","");
log.info("eventKey:{}",eventKey);
Object value = redisTemplate.opsForValue().get(eventKey + ":userId");
String userId = null;
if (value != null) {
userId = (String) value;
}
User user = userService.getById(userId);
if (user != null) {
userService.bindWx(userId, openId);
// 用户绑定微信成功 赋值
redisTemplate.opsForValue().set(userId + ":isBindWx",openId,60,TimeUnit.SECONDS);
text.setContent("绑定微信成功");
} else {
String userName = "书院学员" + GenerationSequenceUtil.unRepeatSixCode();
Long id = IdWorker.getId();
user = User.builder().id(id)
.userName(userName)
.avatar("(NULL)")
.code(GenerationSequenceUtil.unRepeatSixCode())
.openId(openId)
.gender(Constant.GenericNumber.NUMBER_ZERO)
.status(Constant.GenericNumber.NUMBER_ONE)
.createTime(LocalDateTime.now())
.build();
userService.save(user);
// 添加用户学员关系
userToRoleService.saveLocal(id, Constant.RolesRelation.Student.getId());
text.setContent("注册成功,请再次扫描二维码登录");
}
} else if (event.equals("SCAN")) {
eventKey = eventKey.replaceAll("qrscene_","");
log.info("eventKey:{}",eventKey);
Object value = redisTemplate.opsForValue().get(eventKey + ":userId");
String userId = null;
if (value != null) {
userId = (String) value;
}
User user = userService.getById(userId);
if (user != null) {
userService.bindWx(userId, openId);
// 用户绑定微信成功 赋值
redisTemplate.opsForValue().set(userId + ":isBindWx",openId,60,TimeUnit.SECONDS);
text.setContent("绑定微信成功");
} else {
log.info("accessToken:{}", accessToken);
String userUrl = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN";
String userJson = HttpUtil.get(userUrl);
log.info("获取的userJson{}", userJson);
final WxMpController.UserInfo userInfo = JSON.parseObject(userJson, WxMpController.UserInfo.class);
log.info("获取的userInfo{}", userInfo);
WxUserVo wxUserVo = WxUserVo.builder()
.userName("书院学员")
.avatar("(NULL)")
.wxId(openId)
.build();
LoginVO loginVO = userService.loginByWxId(wxUserVo, Constant.LoginApp.tenant);
log.info("loginVO:{}", loginVO);
redisTemplate.opsForValue().set(eventKey, loginVO, 60 * 60, TimeUnit.SECONDS);
text.setContent("已登录" + loginVO.getUser().getUserName());
}
} else if (event.equals("unsubscribe")) {
User userByOpenId = userService.getUserByOpenId(openId);
if (userByOpenId != null) {
try {
userService.deleteByUserNo(String.valueOf(userByOpenId.getId()));
} catch (Exception e) {
log.info("删除用户失败");
}
}
}
}
String message = textMessageToxml(text);
response.setCharacterEncoding("UTF-8");
response.getWriter().println(message);
}
public Map<String ,String > xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
HashMap<String, String> map = new HashMap<>();
SAXReader saxReader = new SAXReader();
ServletInputStream inputStream = request.getInputStream();
Document read = saxReader.read(inputStream);
Element rootElement = read.getRootElement();
List<Element> elements = rootElement.elements();
for (Element e:elements) {
map.put(e.getName(),e.getText());
}
inputStream.close();
return map;
}
public String textMessageToxml(TextMessage textMessage) {
XStream xStream = new XStream();
xStream.alias("xml", textMessage.getClass());
return xStream.toXML(textMessage);
}
5.公众号接口基础配置
URL的域名,如果是http,端口就必须是80,如果是https,端口就必须是443,而且get请求的端口必须是可以正常访问的,否则配置失败;我这里本地调试用的natapp内网穿透,网站内有教程 https://natapp.cn/
6.前端调用获取登录结果接口
前端生成二维码之后,轮询该接口即可;或则前后端使用websoket进行通信也行
/**
* 获取登录返回对象
*/
@ApiOperation(value = "获取登录返回对象")
@GetMapping("/getLoginVO")
@Pass
public ResponseModel<Object> getLoginVO(@RequestParam(name = "key") String key) {
Object result = redisTemplate.opsForValue().get(key);
if (result != null) {
if (result instanceof String || result instanceof Integer) {
return ResponseHelper.succeed(Constant.GenericNumber.NUMBER_ZERO);
} else {
LoginVO loginVO = (LoginVO) result;
return ResponseHelper.succeed(loginVO);
}
} else {
return ResponseHelper.succeed(Constant.GenericNumber.NUMBER_ZERO);
}
}