feat(wxofficial): 增加微信扫码登录和消息解密功能
- 新增微信消息加密解密逻辑,支持AES加密消息的解析 - 实现公众号扫码关注事件处理,获取并注册微信用户信息 - 完善扫码登录流程,支持扫码确认登录状态更新Redis - 增加生成带参数的二维码API,支持7天有效期带scene参数二维码 - 优化用户关注并绑定微信OAuth账号的业务流程与日志输出 - 使用配置常量替代硬编码token和EncodingAESKey,提高安全性和可维护性
This commit is contained in:
@@ -10,6 +10,7 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alipay.api.internal.util.file.IOUtils;
|
import com.alipay.api.internal.util.file.IOUtils;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.qq.weixin.mp.aes.WXBizJsonMsgCrypt;
|
||||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
import com.gxwebsoft.common.core.utils.CommonUtil;
|
||||||
import com.gxwebsoft.common.core.utils.JSONUtil;
|
import com.gxwebsoft.common.core.utils.JSONUtil;
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
@@ -63,6 +64,15 @@ public class WxOfficialController extends BaseController {
|
|||||||
private static final String miniAppid = "wx541db955e7a62709";
|
private static final String miniAppid = "wx541db955e7a62709";
|
||||||
// 创建公众号菜单
|
// 创建公众号菜单
|
||||||
private static final String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=";
|
private static final String MENU_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=";
|
||||||
|
// 生成二维码接口
|
||||||
|
private static final String QRCODE_CREATE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=";
|
||||||
|
// 查看二维码接口
|
||||||
|
private static final String QRCODE_SHOW_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
|
||||||
|
|
||||||
|
// 微信服务器配置(从配置文件读取或使用默认值)
|
||||||
|
private static final String TOKEN = "gxwebsoft";
|
||||||
|
private static final String ENCODING_AES_KEY = "ARve4au5GF2fE2cT13xpaHhuqS2yjE34gpVe8IZwd4C";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -84,7 +94,7 @@ public class WxOfficialController extends BaseController {
|
|||||||
if (tenantId == null) {
|
if (tenantId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String token = "gxwebsoft";
|
String token = TOKEN;
|
||||||
String[] array = new String[]{token, timestamp, nonce};
|
String[] array = new String[]{token, timestamp, nonce};
|
||||||
// 将token、timestamp、nonce三个参数进行字典序排序
|
// 将token、timestamp、nonce三个参数进行字典序排序
|
||||||
Arrays.sort(array);
|
Arrays.sort(array);
|
||||||
@@ -100,45 +110,128 @@ public class WxOfficialController extends BaseController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "接收微信的xml消息")
|
@Operation(summary = "接收微信的消息推送")
|
||||||
@Transactional(rollbackFor = {Exception.class})
|
@Transactional(rollbackFor = {Exception.class})
|
||||||
@RequestMapping("/{id}")
|
@PostMapping("/{id}")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public String receiveMessages(HttpServletRequest request, @PathVariable("id") Integer tenantId) throws IOException {
|
public String receiveMessages(HttpServletRequest request, @PathVariable("id") Integer tenantId,
|
||||||
|
@RequestParam(required = false) String msg_signature,
|
||||||
|
@RequestParam(required = false) String timestamp,
|
||||||
|
@RequestParam(required = false) String nonce) throws Exception {
|
||||||
|
System.out.println("========== 接收微信消息 ==========");
|
||||||
System.out.println("tenantId = " + tenantId);
|
System.out.println("tenantId = " + tenantId);
|
||||||
Integer userId = 0; // 用户ID
|
System.out.println("msg_signature = " + msg_signature);
|
||||||
|
|
||||||
// 从请求中获取XML数据
|
// 从请求中获取XML数据
|
||||||
String xmlData = IOUtils.toString(request.getInputStream(), "UTF-8");
|
String xmlData = IOUtils.toString(request.getInputStream(), "UTF-8");
|
||||||
System.out.println("xmlData = " + xmlData);
|
System.out.println("原始xmlData = " + xmlData);
|
||||||
|
|
||||||
|
// 如果有加密参数,进行解密
|
||||||
|
if (StrUtil.isNotBlank(msg_signature) && StrUtil.isNotBlank(xmlData) && xmlData.contains("Encrypt")) {
|
||||||
|
try {
|
||||||
|
WXBizJsonMsgCrypt crypt = new WXBizJsonMsgCrypt(TOKEN, ENCODING_AES_KEY, "");
|
||||||
|
xmlData = crypt.DecryptMsg(msg_signature, timestamp, nonce, xmlData);
|
||||||
|
System.out.println("解密后xmlData = " + xmlData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("消息解密失败: {}", e.getMessage());
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析XML数据
|
// 解析XML数据
|
||||||
Document document = XmlUtil.parseXml(xmlData);
|
Document document = XmlUtil.parseXml(xmlData);
|
||||||
Element rootElement = XmlUtil.getRootElement(document);
|
Element rootElement = XmlUtil.getRootElement(document);
|
||||||
|
|
||||||
|
// 获取消息类型
|
||||||
|
Element msgTypeElement = XmlUtil.getElement(rootElement, "MsgType");
|
||||||
|
String msgType = msgTypeElement != null ? msgTypeElement.getTextContent() : "";
|
||||||
|
System.out.println("msgType = " + msgType);
|
||||||
|
|
||||||
|
// 获取事件类型(如果是事件消息)
|
||||||
|
Element eventElement = XmlUtil.getElement(rootElement, "Event");
|
||||||
|
String event = eventElement != null ? eventElement.getTextContent() : "";
|
||||||
|
System.out.println("event = " + event);
|
||||||
|
|
||||||
|
// 获取事件KEY(用于判断是否是扫码事件)
|
||||||
|
Element eventKeyElement = XmlUtil.getElement(rootElement, "EventKey");
|
||||||
|
String eventKey = eventKeyElement != null ? eventKeyElement.getTextContent() : "";
|
||||||
|
System.out.println("eventKey = " + eventKey);
|
||||||
|
|
||||||
|
// 获取用户openid
|
||||||
Element FromUserName = XmlUtil.getElement(rootElement, "FromUserName");
|
Element FromUserName = XmlUtil.getElement(rootElement, "FromUserName");
|
||||||
String openId = FromUserName.getTextContent();
|
String openId = FromUserName != null ? FromUserName.getTextContent() : "";
|
||||||
System.out.println("openId = " + openId);
|
System.out.println("openId = " + openId);
|
||||||
|
|
||||||
|
// 获取 ticket(扫码事件专用)
|
||||||
|
Element ticketElement = XmlUtil.getElement(rootElement, "Ticket");
|
||||||
|
String ticket = ticketElement != null ? ticketElement.getTextContent() : "";
|
||||||
|
System.out.println("ticket = " + ticket);
|
||||||
|
|
||||||
|
// 处理扫码关注事件
|
||||||
|
if ("event".equals(msgType) && ("subscribe".equals(event) || "SCAN".equals(event))) {
|
||||||
|
System.out.println("========== 处理扫码关注事件 ==========");
|
||||||
|
|
||||||
|
// 获取扫码的 token(从 EventKey 中提取,格式:qrscene_xxx 或直接是 xxx)
|
||||||
|
String token = "";
|
||||||
|
if (StrUtil.isNotBlank(eventKey)) {
|
||||||
|
if (eventKey.startsWith("qrscene_")) {
|
||||||
|
token = eventKey.substring(8); // 去掉 qrscene_ 前缀
|
||||||
|
} else {
|
||||||
|
token = eventKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("扫码登录token = " + token);
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
if (StrUtil.isNotBlank(openId)) {
|
if (StrUtil.isNotBlank(openId)) {
|
||||||
// 获取用户基本信息(UnionID机制)
|
// 获取用户基本信息(UnionID机制)
|
||||||
final String userStr = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + getAccessToken() + "&openid=" + openId + "&lang=zh_CN");
|
final String userStr = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + getAccessToken() + "&openid=" + openId + "&lang=zh_CN");
|
||||||
// 保存第三方用户信息表shop_user_oauth
|
|
||||||
final JSONObject jsonObject = JSONObject.parseObject(userStr);
|
final JSONObject jsonObject = JSONObject.parseObject(userStr);
|
||||||
final String unionid = jsonObject.getString("unionid");
|
final String unionid = jsonObject.getString("unionid");
|
||||||
final String subscribe = jsonObject.getString("subscribe");
|
final String subscribe = jsonObject.getString("subscribe");
|
||||||
System.out.println("unionid = " + unionid);
|
System.out.println("unionid = " + unionid);
|
||||||
sendTemplateMessage(openId);
|
System.out.println("subscribe = " + subscribe);
|
||||||
|
|
||||||
|
Integer userId = processWxUser(tenantId, openId, unionid, subscribe);
|
||||||
|
|
||||||
|
// 如果有关联的扫码登录token,完成登录
|
||||||
|
if (StrUtil.isNotBlank(token) && userId != null && userId > 0) {
|
||||||
|
completeQrLogin(token, userId, tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 success 表示处理成功
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理微信用户(关注/注册/登录)
|
||||||
|
*/
|
||||||
|
private Integer processWxUser(Integer tenantId, String openId, String unionid, String subscribe) {
|
||||||
|
Integer userId = 0;
|
||||||
|
|
||||||
// 关注操作
|
// 关注操作
|
||||||
if (subscribe != null && subscribe.equals("1")) {
|
if (subscribe != null && subscribe.equals("1")) {
|
||||||
final int count = userOauthService.count(new LambdaQueryWrapper<UserOauth>().eq(UserOauth::getOauthType, MP_OFFICIAL).eq(UserOauth::getUnionid, unionid).eq(UserOauth::getTenantId, tenantId));
|
final int count = userOauthService.count(new LambdaQueryWrapper<UserOauth>()
|
||||||
System.out.println("count = " + count);
|
.eq(UserOauth::getOauthType, MP_OFFICIAL)
|
||||||
|
.eq(UserOauth::getUnionid, unionid)
|
||||||
|
.eq(UserOauth::getTenantId, tenantId));
|
||||||
|
System.out.println("已绑定用户数量 = " + count);
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
// 其他平台是否有注册过
|
// 检查其他平台是否有注册过
|
||||||
final List<UserOauth> list = userOauthService.list(new LambdaQueryWrapper<UserOauth>().eq(UserOauth::getUnionid, unionid).eq(UserOauth::getDeleted, 0));
|
final List<UserOauth> list = userOauthService.list(
|
||||||
|
new LambdaQueryWrapper<UserOauth>()
|
||||||
|
.eq(UserOauth::getUnionid, unionid)
|
||||||
|
.eq(UserOauth::getDeleted, 0));
|
||||||
final int size = list.size();
|
final int size = list.size();
|
||||||
|
|
||||||
// 新用户注册
|
// 新用户注册
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setStatus(0);
|
user.setStatus(0);
|
||||||
user.setUsername("wxoff_".concat(RandomUtil.randomString(12)));
|
user.setUsername("wxoff_".concat(RandomUtil.randomString(12)));
|
||||||
user.setStatus(0);
|
|
||||||
user.setNickname("微信公众号用户");
|
user.setNickname("微信公众号用户");
|
||||||
user.setPlatform(MP_OFFICIAL);
|
user.setPlatform(MP_OFFICIAL);
|
||||||
user.setGradeId(1);
|
user.setGradeId(1);
|
||||||
@@ -161,8 +254,9 @@ public class WxOfficialController extends BaseController {
|
|||||||
// 同步到 websopy
|
// 同步到 websopy
|
||||||
userSyncService.syncUserToWebsopy(user);
|
userSyncService.syncUserToWebsopy(user);
|
||||||
}
|
}
|
||||||
System.out.println("新微信公众号用户 = " + userId);
|
System.out.println("新微信公众号用户 userId = " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新
|
// 更新
|
||||||
if (!CollectionUtils.isEmpty(list)) {
|
if (!CollectionUtils.isEmpty(list)) {
|
||||||
for (UserOauth item : list) {
|
for (UserOauth item : list) {
|
||||||
@@ -170,8 +264,9 @@ public class WxOfficialController extends BaseController {
|
|||||||
userId = item.getUserId();
|
userId = item.getUserId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
System.out.println("其他平台有注册过 = " + userId);
|
System.out.println("其他平台有注册过 userId = " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存第三方用户记录
|
// 保存第三方用户记录
|
||||||
final UserOauth userOauth = new UserOauth();
|
final UserOauth userOauth = new UserOauth();
|
||||||
userOauth.setOauthType(MP_OFFICIAL);
|
userOauth.setOauthType(MP_OFFICIAL);
|
||||||
@@ -180,12 +275,75 @@ public class WxOfficialController extends BaseController {
|
|||||||
userOauth.setUserId(userId);
|
userOauth.setUserId(userId);
|
||||||
userOauth.setTenantId(tenantId);
|
userOauth.setTenantId(tenantId);
|
||||||
boolean save = userOauthService.save(userOauth);
|
boolean save = userOauthService.save(userOauth);
|
||||||
System.out.println("关注微信公众号 = " + save);
|
System.out.println("关注微信公众号保存结果 = " + save);
|
||||||
|
} else {
|
||||||
|
// 已绑定用户,获取userId
|
||||||
|
UserOauth existingUser = userOauthService.getOne(new LambdaQueryWrapper<UserOauth>()
|
||||||
|
.eq(UserOauth::getOauthType, MP_OFFICIAL)
|
||||||
|
.eq(UserOauth::getUnionid, unionid)
|
||||||
|
.eq(UserOauth::getTenantId, tenantId));
|
||||||
|
if (existingUser != null) {
|
||||||
|
userId = existingUser.getUserId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成扫码登录
|
||||||
|
*/
|
||||||
|
private void completeQrLogin(String token, Integer userId, Integer tenantId) {
|
||||||
|
try {
|
||||||
|
// 更新扫码登录状态
|
||||||
|
String redisKey = "QR_LOGIN_TOKEN:" + token;
|
||||||
|
JSONObject qrLoginData = new JSONObject();
|
||||||
|
qrLoginData.put("status", "confirmed");
|
||||||
|
qrLoginData.put("userId", userId);
|
||||||
|
qrLoginData.put("tenantId", tenantId);
|
||||||
|
qrLoginData.put("confirmTime", System.currentTimeMillis());
|
||||||
|
// 保存1分钟,给前端足够时间获取
|
||||||
|
redisUtil.set(redisKey, qrLoginData.toJSONString(), 60L, TimeUnit.SECONDS);
|
||||||
|
System.out.println("扫码登录完成,token=" + token + ", userId=" + userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("完成扫码登录失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "生成微信扫码登录二维码")
|
||||||
|
@GetMapping("/qrcode/{token}")
|
||||||
|
public ApiResult<?> generateQrCode(@PathVariable("token") String token) {
|
||||||
|
try {
|
||||||
|
// 生成带参数的二维码,scene 为 token
|
||||||
|
String url = QRCODE_CREATE_URL + getAccessToken();
|
||||||
|
|
||||||
|
// 创建临时二维码(有效期7天),scene_str 最大32个可见字符
|
||||||
|
JSONObject params = new JSONObject();
|
||||||
|
params.put("action_info", new JSONObject().put("scene", new JSONObject().put("scene_str", token)));
|
||||||
|
params.put("action_name", "QR_STR_SCENE");
|
||||||
|
params.put("expire_seconds", 604800); // 7天有效期
|
||||||
|
|
||||||
|
String result = HttpRequest.post(url)
|
||||||
|
.body(params.toJSONString())
|
||||||
|
.timeout(10000)
|
||||||
|
.execute().body();
|
||||||
|
|
||||||
|
System.out.println("生成二维码结果: " + result);
|
||||||
|
|
||||||
|
JSONObject jsonResult = JSONObject.parseObject(result);
|
||||||
|
if (jsonResult.containsKey("ticket")) {
|
||||||
|
String ticket = jsonResult.getString("ticket");
|
||||||
|
String qrCodeUrl = QRCODE_SHOW_URL + java.net.URLEncoder.encode(ticket, "UTF-8");
|
||||||
|
|
||||||
|
return success(qrCodeUrl);
|
||||||
|
} else {
|
||||||
|
return fail("生成二维码失败: " + result);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成二维码异常: {}", e.getMessage());
|
||||||
|
return fail("生成二维码异常: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendTemplateMessage(String openId) {
|
private void sendTemplateMessage(String openId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user