微信公众号沙箱测试环境登录地址
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文件转成易处理的map
final Map map = ParseXmlUtils.parseXml(request);
//开发者微信号
final String toUserName = map.get("ToUserName");
//OpenId
final String fromUserName = map.get("FromUserName");
//消息创建时间 (整型)
final String createTime = map.get("CreateTime");
//消息类型,event
final 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 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 parseXml(HttpServletRequest request) throws Exception {
//存放所有的子节点
Map map = new HashMap();
//获取到文档对象
InputStream inputStream = request.getInputStream();
SAXReader reader = new SAXReader();
//读取输入流,获取文档对象
Document document = reader.read(inputStream);
//根据文档对象获取根节点
Element root = document.getRootElement();
//获取根节点的所有子节点
List 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 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 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 parseXml, String content) {
BaseMessage msg = null;
//获取到消息的类型,如果还有其他类型的消息直接加case就可以
String type = parseXml.get("MsgType");
//调用处理文本消息的方法
if (type.equals("text") || type.equals("event")) {
msg = dealTextMessage(parseXml, content);
}
//如果该消息不为null
if (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 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 data = new HashMap<>();
data.put("expire_seconds", 3600);
data.put("action_name", "QR_STR_SCENE");
Map actionInfo = new HashMap<>();
Map 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_credential
String 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 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(() => {});
},