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.alipay.api.internal.util.file.IOUtils;
|
||||
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.JSONUtil;
|
||||
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 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
|
||||
private UserService userService;
|
||||
@Resource
|
||||
@@ -84,7 +94,7 @@ public class WxOfficialController extends BaseController {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
String token = "gxwebsoft";
|
||||
String token = TOKEN;
|
||||
String[] array = new String[]{token, timestamp, nonce};
|
||||
// 将token、timestamp、nonce三个参数进行字典序排序
|
||||
Arrays.sort(array);
|
||||
@@ -100,45 +110,128 @@ public class WxOfficialController extends BaseController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Operation(summary = "接收微信的xml消息")
|
||||
@Operation(summary = "接收微信的消息推送")
|
||||
@Transactional(rollbackFor = {Exception.class})
|
||||
@RequestMapping("/{id}")
|
||||
@PostMapping("/{id}")
|
||||
@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);
|
||||
Integer userId = 0; // 用户ID
|
||||
System.out.println("msg_signature = " + msg_signature);
|
||||
|
||||
// 从请求中获取XML数据
|
||||
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数据
|
||||
Document document = XmlUtil.parseXml(xmlData);
|
||||
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");
|
||||
String openId = FromUserName.getTextContent();
|
||||
String openId = FromUserName != null ? FromUserName.getTextContent() : "";
|
||||
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)) {
|
||||
// 获取用户基本信息(UnionID机制)
|
||||
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 String unionid = jsonObject.getString("unionid");
|
||||
final String subscribe = jsonObject.getString("subscribe");
|
||||
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")) {
|
||||
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 = " + count);
|
||||
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 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.setStatus(0);
|
||||
user.setNickname("微信公众号用户");
|
||||
user.setPlatform(MP_OFFICIAL);
|
||||
user.setGradeId(1);
|
||||
@@ -161,8 +254,9 @@ public class WxOfficialController extends BaseController {
|
||||
// 同步到 websopy
|
||||
userSyncService.syncUserToWebsopy(user);
|
||||
}
|
||||
System.out.println("新微信公众号用户 = " + userId);
|
||||
System.out.println("新微信公众号用户 userId = " + userId);
|
||||
}
|
||||
|
||||
// 更新
|
||||
if (!CollectionUtils.isEmpty(list)) {
|
||||
for (UserOauth item : list) {
|
||||
@@ -170,8 +264,9 @@ public class WxOfficialController extends BaseController {
|
||||
userId = item.getUserId();
|
||||
}
|
||||
}
|
||||
System.out.println("其他平台有注册过 = " + userId);
|
||||
System.out.println("其他平台有注册过 userId = " + userId);
|
||||
}
|
||||
|
||||
// 保存第三方用户记录
|
||||
final UserOauth userOauth = new UserOauth();
|
||||
userOauth.setOauthType(MP_OFFICIAL);
|
||||
@@ -180,12 +275,75 @@ public class WxOfficialController extends BaseController {
|
||||
userOauth.setUserId(userId);
|
||||
userOauth.setTenantId(tenantId);
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user