diff --git a/src/main/java/com/imutil/service/impl/CallbackServiceImpl.java b/src/main/java/com/imutil/service/impl/CallbackServiceImpl.java index 3805b4a..972fcfc 100644 --- a/src/main/java/com/imutil/service/impl/CallbackServiceImpl.java +++ b/src/main/java/com/imutil/service/impl/CallbackServiceImpl.java @@ -26,7 +26,10 @@ import java.time.ZoneId; /** * 腾讯回调处理服务实现 *
- * 流程:识别租户 → 消息类回调落 im_message(幂等)→ 所有回调写 dist_queue 分发业务系统。
+ * 流程:
+ * 1. 发单聊消息之前回调(C2C.CallbackBeforeSendMsg):跨租户拦截决策(对齐设计 5.4)——
+ * 同租户放行;跨租户查 cross_tenant_grant,命中授权放行并写审计,未命中返回 FAIL(腾讯不投递);
+ * 2. 其他回调(含发消息之后回调):识别租户 → 消息类回调落 im_message(幂等)→ 按 from 租户写 dist_queue 分发。
* {@code @Tran} 保证消息落库与分发入队同事务:要么同时成功,要么都不入库(避免半写)。
*
* @author imutil
@@ -70,7 +73,13 @@ public class CallbackServiceImpl implements CallbackService {
return ok();
}
- // 1. 识别租户
+ // 1. 发单聊消息之前回调:跨租户拦截决策(对齐设计 5.4)
+ // 未授权的跨租户消息在此返回 FAIL,腾讯不投递、也不触发 after 回调 → 从源头阻断串扰
+ if (isBeforeSendMsg(callbackCommand)) {
+ return handleBeforeSendMsg(node);
+ }
+
+ // 2. 识别租户
String tenantId = identifyTenant(callbackCommand, node);
if (tenantId == null) {
// 无法识别租户(如腾讯系统消息 administrator),不落库不分发
@@ -78,28 +87,21 @@ public class CallbackServiceImpl implements CallbackService {
return ok();
}
- // 2. 消息类回调落库 im_message(幂等:msg_key 存在则跳过)
- // msg_key = command:from:convTarget:msgSeq:msgRandom,含 MsgSeq+MsgRandom 全局唯一,单独作幂等键;
+ // 3. 消息类回调(发消息后)落库 im_message(幂等:msg_key 存在则跳过)
+ // msg_key = from:convTarget:msgSeq:msgRandom,含 MsgSeq+MsgRandom 全局唯一,单独作幂等键;
// 不依赖 MsgTimeStamp(避免腾讯回调时间戳偏差导致漏判)。
// DB 主键 (msg_key, msg_time) 因分区表约束保留 msg_time,作兜底防护。
String msgKey = null;
- // 跨租户授权命中的对端账号(非空表示该消息为授权放行的跨租户消息,需额外分发+审计)
- String crossToImUser = null;
- Long crossGrantId = null;
if (isMessageCallback(callbackCommand)) {
ImMessage msg = parseMessage(callbackCommand, node, tenantId);
if (msg != null) {
msgKey = msg.getMsgKey();
- // 跨租户授权检查(仅 C2C;群消息跨租户语义为群成员维度,本设计不处理)
+ // 跨租户标记:能到达 after 的跨租户 C2C 消息必经 before 放行(有授权);
+ // 未授权的已被 before 拦截(腾讯不投递、不触发 after),故 after 见到跨租户即标记。
if (msg.getConvType() != null && msg.getConvType() == 1) {
String toTenant = parsePrefix(msg.getConvId());
if (toTenant != null && !toTenant.equals(tenantId)) {
- crossGrantId = crossTenantService.checkSendMsgGrant(
- tenantId, msg.getFromAccount(), toTenant, msg.getConvId());
- if (crossGrantId != null) {
- msg.setIsCrossTenant(true);
- crossToImUser = msg.getConvId();
- }
+ msg.setIsCrossTenant(true);
}
}
long exists = imMessageMapper.selectCount(Wrappers.
+ * 同租户放行;跨租户查 cross_tenant_grant,命中授权则放行并写审计,未命中返回 FAIL(腾讯不投递消息)。
+ *
+ * @param node 回调体
+ * @return 放行 ok() 或拦截 fail()
+ */
+ private String handleBeforeSendMsg(ONode node) {
+ String from = node.get("FromAccount").getString();
+ String to = node.get("ToAccount").getString();
+ if (to == null || to.isEmpty()) {
+ to = node.get("ToPlayerId").getString();
+ }
+ String fromTenant = parsePrefix(from);
+ String toTenant = parsePrefix(to);
+ // 同租户或任一租户无法识别(如系统消息):不拦截,放行正常通讯
+ if (fromTenant == null || toTenant == null || fromTenant.equals(toTenant)) {
+ return ok();
+ }
+ // 跨租户:查 send_msg 授权
+ Long grantId = crossTenantService.checkSendMsgGrant(fromTenant, from, toTenant, to);
+ if (grantId == null) {
+ log.info("跨租户通讯未授权,拦截 from={} to={}", from, to);
+ return fail();
+ }
+ // 命中授权:放行 + 写审计(before 阶段记录放行决策,msgKey 便于追溯)
+ long msgSeq = node.get("MsgSeq").getLong();
+ long msgRandom = node.get("MsgRandom").getLong();
+ String msgKey = MsgKeys.build(from, to, msgSeq, msgRandom);
+ crossTenantService.audit(grantId, msgKey, from, to);
+ log.info("跨租户授权放行 from={} to={} grant={}", from, to, grantId);
+ return ok();
+ }
+
+ /**
+ * 是否为发单聊消息之前回调(跨租户拦截决策入口)
+ */
+ private boolean isBeforeSendMsg(String command) {
+ return command != null && command.contains("CallbackBeforeSendMsg");
+ }
+
+ /**
+ * 返回腾讯回调拦截响应(消息不投递,客户端收到 ErrorCode + ErrorInfo)
+ */
+ private String fail() {
+ return "{\"ActionStatus\":\"FAIL\",\"ErrorCode\":80001,\"ErrorInfo\":\"cross-tenant not authorized\"}";
+ }
+
/**
* 租户配置了回调地址则入队分发
*