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;// 获取accessTokenString 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);// 获取ticketString 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);// 获取ticketString 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;// 获取accessTokenString 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")@Passpublic 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@Passpublic 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")@Passpublic 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);}}