feat(wxofficial): 增加微信扫码登录和消息解密功能

- 新增微信消息加密解密逻辑,支持AES加密消息的解析
- 实现公众号扫码关注事件处理,获取并注册微信用户信息
- 完善扫码登录流程,支持扫码确认登录状态更新Redis
- 增加生成带参数的二维码API,支持7天有效期带scene参数二维码
- 优化用户关注并绑定微信OAuth账号的业务流程与日志输出
- 使用配置常量替代硬编码token和EncodingAESKey,提高安全性和可维护性
This commit is contained in:
2026-04-06 18:09:58 +08:00
parent 7927559b70
commit aa910870cc

View File

@@ -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,92 +110,240 @@ 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);
if (StrUtil.isNotBlank(openId)) {
// 获取用户基本信息(UnionID机制) // 获取 ticket扫码事件专用
final String userStr = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + getAccessToken() + "&openid=" + openId + "&lang=zh_CN"); Element ticketElement = XmlUtil.getElement(rootElement, "Ticket");
// 保存第三方用户信息表shop_user_oauth String ticket = ticketElement != null ? ticketElement.getTextContent() : "";
final JSONObject jsonObject = JSONObject.parseObject(userStr); System.out.println("ticket = " + ticket);
final String unionid = jsonObject.getString("unionid");
final String subscribe = jsonObject.getString("subscribe"); // 处理扫码关注事件
System.out.println("unionid = " + unionid); if ("event".equals(msgType) && ("subscribe".equals(event) || "SCAN".equals(event))) {
sendTemplateMessage(openId); System.out.println("========== 处理扫码关注事件 ==========");
// 关注操作
if (subscribe != null && subscribe.equals("1")) { // 获取扫码的 token从 EventKey 中提取格式qrscene_xxx 或直接是 xxx
final int count = userOauthService.count(new LambdaQueryWrapper<UserOauth>().eq(UserOauth::getOauthType, MP_OFFICIAL).eq(UserOauth::getUnionid, unionid).eq(UserOauth::getTenantId, tenantId)); String token = "";
System.out.println("count = " + count); if (StrUtil.isNotBlank(eventKey)) {
if (count == 0) { if (eventKey.startsWith("qrscene_")) {
// 其他平台是否有注册过 token = eventKey.substring(8); // 去掉 qrscene_ 前缀
final List<UserOauth> list = userOauthService.list(new LambdaQueryWrapper<UserOauth>().eq(UserOauth::getUnionid, unionid).eq(UserOauth::getDeleted, 0)); } else {
final int size = list.size(); token = eventKey;
// 新用户注册 }
if (size == 0) { }
User user = new User(); System.out.println("扫码登录token = " + token);
user.setStatus(0);
user.setUsername("wxoff_".concat(RandomUtil.randomString(12))); // 获取用户信息
user.setStatus(0); if (StrUtil.isNotBlank(openId)) {
user.setNickname("微信公众号用户"); // 获取用户基本信息(UnionID机制)
user.setPlatform(MP_OFFICIAL); final String userStr = HttpUtil.get("https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + getAccessToken() + "&openid=" + openId + "&lang=zh_CN");
user.setGradeId(1); final JSONObject jsonObject = JSONObject.parseObject(userStr);
user.setPassword(userService.encodePassword(CommonUtil.randomUUID16())); final String unionid = jsonObject.getString("unionid");
user.setTenantId(tenantId); final String subscribe = jsonObject.getString("subscribe");
user.setRecommend(0); System.out.println("unionid = " + unionid);
final RoleParam roleParam = new RoleParam(); System.out.println("subscribe = " + subscribe);
roleParam.setTenantId(tenantId);
roleParam.setRoleCode("guest"); Integer userId = processWxUser(tenantId, openId, unionid, subscribe);
Role role = roleService.getByRoleCode(roleParam);
user.setRoleId(role.getRoleId()); // 如果有关联的扫码登录token完成登录
if (userService.saveUser(user)) { if (StrUtil.isNotBlank(token) && userId != null && userId > 0) {
userId = user.getUserId(); completeQrLogin(token, userId, tenantId);
// 添加用户角色
final UserRole userRole = new UserRole();
userRole.setUserId(user.getUserId());
userRole.setTenantId(user.getTenantId());
userRole.setRoleId(user.getRoleId());
userRoleService.save(userRole);
// 同步到 websopy
userSyncService.syncUserToWebsopy(user);
}
System.out.println("新微信公众号用户 = " + userId);
}
// 更新
if (!CollectionUtils.isEmpty(list)) {
for (UserOauth item : list) {
if (item.getUserId() != null) {
userId = item.getUserId();
}
}
System.out.println("其他平台有注册过 = " + userId);
}
// 保存第三方用户记录
final UserOauth userOauth = new UserOauth();
userOauth.setOauthType(MP_OFFICIAL);
userOauth.setUnionid(unionid);
userOauth.setOauthId(openId);
userOauth.setUserId(userId);
userOauth.setTenantId(tenantId);
boolean save = userOauthService.save(userOauth);
System.out.println("关注微信公众号 = " + save);
} }
} }
} }
return null;
// 返回 success 表示处理成功
return "success";
}
/**
* 处理微信用户(关注/注册/登录)
*/
private Integer processWxUser(Integer tenantId, String openId, String unionid, String subscribe) {
Integer userId = 0;
// 关注操作
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));
System.out.println("已绑定用户数量 = " + count);
if (count == 0) {
// 检查其他平台是否有注册过
final List<UserOauth> list = userOauthService.list(
new LambdaQueryWrapper<UserOauth>()
.eq(UserOauth::getUnionid, unionid)
.eq(UserOauth::getDeleted, 0));
final int size = list.size();
// 新用户注册
if (size == 0) {
User user = new User();
user.setStatus(0);
user.setUsername("wxoff_".concat(RandomUtil.randomString(12)));
user.setNickname("微信公众号用户");
user.setPlatform(MP_OFFICIAL);
user.setGradeId(1);
user.setPassword(userService.encodePassword(CommonUtil.randomUUID16()));
user.setTenantId(tenantId);
user.setRecommend(0);
final RoleParam roleParam = new RoleParam();
roleParam.setTenantId(tenantId);
roleParam.setRoleCode("guest");
Role role = roleService.getByRoleCode(roleParam);
user.setRoleId(role.getRoleId());
if (userService.saveUser(user)) {
userId = user.getUserId();
// 添加用户角色
final UserRole userRole = new UserRole();
userRole.setUserId(user.getUserId());
userRole.setTenantId(user.getTenantId());
userRole.setRoleId(user.getRoleId());
userRoleService.save(userRole);
// 同步到 websopy
userSyncService.syncUserToWebsopy(user);
}
System.out.println("新微信公众号用户 userId = " + userId);
}
// 更新
if (!CollectionUtils.isEmpty(list)) {
for (UserOauth item : list) {
if (item.getUserId() != null) {
userId = item.getUserId();
}
}
System.out.println("其他平台有注册过 userId = " + userId);
}
// 保存第三方用户记录
final UserOauth userOauth = new UserOauth();
userOauth.setOauthType(MP_OFFICIAL);
userOauth.setUnionid(unionid);
userOauth.setOauthId(openId);
userOauth.setUserId(userId);
userOauth.setTenantId(tenantId);
boolean save = userOauthService.save(userOauth);
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());
}
} }
private void sendTemplateMessage(String openId) { private void sendTemplateMessage(String openId) {