微信公众号沙箱测试环境登录地址
https://open.weixin.qq.com/connect/qrconnect?appid=wx39c379788eb1286a&scope=snsapi_login&redirect_uri=http%3A%2F%2Fmp.weixin.qq.com%2Fdebug%2Fcgi-bin%2Fsandbox%3Ft%3Dsandbox%2Flogin
账号和地址配置:
1. 获取appID 和 appsecret
2. 配置接口信息:天界URL回调地址(需要后台先写好接口不然添加不成功),token(加解密需要)
验证url接口
/** @param signature 微信加密签名,signature结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce参数。* @param timestamp 时间戳* @param nonce 这是个随机数* @param echostr 随机字符串,验证成功后原样返回*/@GetMapping("/wx/event")public void get(@RequestParam(required = false) String signature,@RequestParam(required = false) String timestamp,@RequestParam(required = false) String nonce,@RequestParam(required = false) String echostr,HttpServletResponse response) throws IOException {log.info("接受事件=====>{}", echostr);response.setCharacterEncoding("UTF-8");response.getWriter().write(echostr);response.getWriter().flush();response.getWriter().close();}
处理微信推送事件接口
//处理微信推送事件@PostMapping("/wx/event")public void post(final HttpServletRequest request, HttpServletResponse response) {// TODO 做验签操作log.info("接受事件公众号事件");try {// 微信加密签名final String signature = request.getParameter("signature");// 时间戳final String timestamp = request.getParameter("timestamp");// 随机数final String nonce = request.getParameter("nonce");// 随机字符串final String echostr = request.getParameter("echostr");//将xml文件转成易处理的mapfinal Map<String, String> map = ParseXmlUtils.parseXml(request);//开发者微信号final String toUserName = map.get("ToUserName");//OpenIdfinal String fromUserName = map.get("FromUserName");//消息创建时间 (整型)final String createTime = map.get("CreateTime");//消息类型,eventfinal String msgType = map.get("MsgType");//事件类型final String event = map.get("Event");String sceneStr = "";String msg = "";if ("event".equals(msgType)) {if (event.equals("subscribe")) {final String ticket = map.get("Ticket");if (ticket != null) {sceneStr = map.get("EventKey").replace("qrscene_", "");}msg = ReplyModel.responseReply(map, "欢迎您使用测试公众号");}//注:事件类型为SCAN即已关注else if (event.equals("SCAN")) {final String ticket = map.get("Ticket");if (ticket != null) {sceneStr = map.get("EventKey");}msg = ReplyModel.responseReply(map, "扫码登录");}} else {// 用户行为msg = ReplyModel.responseReply(map, "欢迎关注该公众号");}log.info("event:{}" + event);log.info("场景值:{}" + sceneStr);log.info("openId:{}" + fromUserName);log.info("ToUserName:{}" + toUserName);// 如果scene_str 不为空代表用户已经扫描并且关注了公众号if (StringUtils.isNotEmpty(sceneStr)) {QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();queryWrapper.eq("open_id", fromUserName);SysUser sysUser = sysUserMapper.selectOne(queryWrapper);if (sysUser == null) {// 保存用户微信的信息 ,为了测试我直接在数据库添加
// sysUser = new SysUser();
// sysUser.setOpenId(fromUserName);
// sysUser.setUsername(fromUserName);
// sysUser.setRealname(null);
// String salt = oConvertUtils.randomGen(8);
// String passwordEncode = PasswordUtil.encrypt(fromUserName, salt, salt);
// sysUser.setSalt(salt);
// sysUser.setMemberLevelId("1");
// sysUser.setPassword(passwordEncode);
// sysUser.setStatus(1);
// sysUser.setDelFlag(CommonConstant.DEL_FLAG_0);
// sysUserMapper.insertUser(sysUser);}// 将微信公众号用户ID缓存到redis中,标记用户已经扫码完成,执行登录逻辑。redisCache.setCacheObject(sceneStr, fromUserName, 60, TimeUnit.SECONDS);}log.info("打印消息体=========>{}", msg);response.setCharacterEncoding("UTF-8");PrintWriter out = response.getWriter();out.print(msg);out.flush();out.close();} catch (Exception e) {e.printStackTrace();}}
ParseXmlUtils # xml解析工具类
package com.spark.common.utils;import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author Jerry* @date 2024-01-26 10:36* 解析微信公众号服务器返回的xml文件*/
public class ParseXmlUtils {public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {//存放所有的子节点Map<String, String> map = new HashMap<String, String>();//获取到文档对象InputStream inputStream = request.getInputStream();SAXReader reader = new SAXReader();//读取输入流,获取文档对象Document document = reader.read(inputStream);//根据文档对象获取根节点Element root = document.getRootElement();//获取根节点的所有子节点List<Element> elementList = root.elements();//遍历所有的子节点for (Element e : elementList) {//所有的子节点放入map中map.put(e.getName(), e.getText());}//关闭流inputStream.close();return map;}}
解析微信公众号服务器返回的xml文件相关类:
BaseMessage # 基础消息类
package com.spark.common.wx;import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;import java.util.Map;/*** @author Jerry* @date 2024-01-26 15:37*/
@Data
@XStreamAlias("xml")
public class BaseMessage {// 开发者微信号private String ToUserName;// 发送方帐号(一个OpenID)private String FromUserName;// 消息创建时间 (整型)private String CreateTime;// 消息类型(text/image/location/link)private String MsgType;public BaseMessage(Map<String, String> requestMap) {this.ToUserName = requestMap.get("FromUserName");this.FromUserName = requestMap.get("ToUserName");this.CreateTime = System.currentTimeMillis() / 1000 + "";}}
TextMessage # 文本消息类型
package com.spark.common.wx;import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;import java.util.Map;/*** @author Jerry* @date 2024-01-26 15:39*/
@Data
@XStreamAlias("xml")
public class TextMessage extends BaseMessage{// 消息内容private String Content;public TextMessage(Map<String, String> requestMap, String Content) {super(requestMap);this.setMsgType("text");this.Content = Content;}
}
ReplyModel #处理组装返回xml
package com.spark.common.wx;import com.thoughtworks.xstream.XStream;import java.util.Map;/*** @author Jerry* @date 2024-01-26 15:40* 解析微信公众号服务器返回的xml文件*/
public class ReplyModel {public static String responseReply(Map<String, String> parseXml, String content) {BaseMessage msg = null;//获取到消息的类型,如果还有其他类型的消息直接加case就可以String type = parseXml.get("MsgType");//调用处理文本消息的方法if (type.equals("text") || type.equals("event")) {msg = dealTextMessage(parseXml, content);}//如果该消息不为nullif (msg != null) {//调用把消息对象处理为xml数据包的方法return beanToxml(msg);}return null;}///把消息对象处理为xml数据包的方法private static String beanToxml(BaseMessage msg) {XStream xStream = new XStream();xStream.processAnnotations(TextMessage.class);String xml = xStream.toXML(msg);return xml;}//处理文本消息private static BaseMessage dealTextMessage(Map<String, String> parseXml, String content) {//将键值对的map直接转为对象,并为Content参数赋值return new TextMessage(parseXml, content);}}
获取登录二维码
官方文档:微信开放文档
/*** 获取二维码** @param sceneStr* @return*/@GetMapping("/getTempQrCode")public AjaxResult getTempQrCode(@RequestParam(name = "sceneStr") String sceneStr) {String result = createTempQrCode(getAccessToken(), sceneStr);return AjaxResult.success(result);}public static String createTempQrCode(String accessToken, String sceneStr) {String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken;Map<String, Object> data = new HashMap<>();data.put("expire_seconds", 3600);data.put("action_name", "QR_STR_SCENE");Map<String, Object> actionInfo = new HashMap<>();Map<String, Object> scene = new HashMap<>();scene.put("scene_str", sceneStr);actionInfo.put("scene", scene);data.put("action_info", actionInfo);String json = HttpUtil.createPost(url).header("Content-Type", "application/json").body(JSONUtil.toJsonStr(data)).execute().body();System.out.println("json = " + json);String qrcode = (String) JSONUtil.getByPath(JSONUtil.parse(json), "ticket");return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + qrcode;}public String getAccessToken() {String accessToken = "";//获取access_token填写client_credentialString grantType = "client_credential";String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grantType + "&appid=" + wxProperties.getAppId() + "&secret=" + wxProperties.getAppSecret();try {URL urlGet = new URL(url);HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();// 必须是get方式请求http.setRequestMethod("GET");http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");http.setDoOutput(true);http.setDoInput(true);System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒http.connect();InputStream is = http.getInputStream();int size = is.available();byte[] jsonBytes = new byte[size];is.read(jsonBytes);String message = new String(jsonBytes, "UTF-8");JSONObject demoJson = JSONObject.parseObject(message);System.out.println("JSON字符串:" + demoJson);accessToken = demoJson.getString("access_token");is.close();} catch (Exception e) {e.printStackTrace();}return accessToken;}
调用链路:微信扫码 ===> 微信后台回调我们后台判断事件类型,将随机字符串和openid写入缓存中,前端定时扫码缓存接口,有则跳转登录。
后端检查是否扫码接口:
/ * 检查扫码状态** @param sceneStr 随机字符串* @return*/@GetMapping("/checkScanState")public AjaxResult wechatLogin(@RequestParam(name = "sceneStr") String sceneStr) {Object openId = redisCache.getCacheObject(sceneStr);if (openId == null) {return AjaxResult.success("未登入");}QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();queryWrapper.eq("open_id", openId);SysUser sysUser = sysUserMapper.selectOne(queryWrapper);if (sysUser != null) {AjaxResult ajax = AjaxResult.success();String token = sysLoginService.autoLogin(sysUser.getUserName(), (String) openId);ajax.put(Constants.TOKEN, token);return ajax;}return AjaxResult.success("未登入");}
前端代码:
// 微信公众号授权登录wechatLogin() {this.isWechatLogin = true;this.sceneStr = this.generateRandomString(16);this.getLoginQrCode();},// 账号密码登录userNamePassLogin() {this.isWechatLogin = false;},// 获取登录二维码getLoginQrCode() {// todo 需要改成随机字符串let data = {sceneStr: this.sceneStr}getQrCode(data).then(res => {this.qrImageUrl = res.msg;setInterval(() => {this.check();}, 1000);})},// 随机数generateRandomString(length) {const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';let result = '';for (let i = 0; i < length; i++) {const randomIndex = Math.floor(Math.random() * characters.length);result += characters[randomIndex];}return result;},check() {if (!this.isWechatLogin) return false;let data = {sceneStr: this.sceneStr}this.$store.dispatch("CheckScanState", data).then((res) => {if (res.token) {this.isWechatLogin = false;}console.log(res);console.log("check....");this.$router.push({path: this.redirect || "/"}).catch(() => {});}).catch(() => {});},