diff --git a/im-service/src/main/java/com/xuqm/im/config/WebSocketConfig.java b/im-service/src/main/java/com/xuqm/im/config/WebSocketConfig.java index c590cfb..6b76841 100644 --- a/im-service/src/main/java/com/xuqm/im/config/WebSocketConfig.java +++ b/im-service/src/main/java/com/xuqm/im/config/WebSocketConfig.java @@ -63,9 +63,15 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { token = token.substring(7); if (jwtUtil.isValid(token)) { String userId = jwtUtil.getSubject(token); + String appId = ""; + try { + Object claim = jwtUtil.parse(token).get("appId"); + if (claim != null) appId = claim.toString(); + } catch (Exception ignored) {} UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + auth.setDetails(java.util.Map.of("appId", appId)); accessor.setUser(auth); userPresenceService.markOnline(userId); } diff --git a/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java b/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java index d340512..908755e 100644 --- a/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java +++ b/im-service/src/main/java/com/xuqm/im/service/ImFeatureConfigClient.java @@ -77,6 +77,14 @@ public class ImFeatureConfigClient { return readConfig(appId).path("allowMultiDeviceLogin").asBoolean(true); } + public String multiDeviceLoginMode(String appId) { + String mode = readConfig(appId).path("multiDeviceLoginMode").asText("MULTI_DEVICE_FREE"); + return switch (mode.toUpperCase()) { + case "SAME_PLATFORM_ONE", "SINGLE_DEVICE" -> mode.toUpperCase(); + default -> "MULTI_DEVICE_FREE"; + }; + } + private JsonNode readConfig(String appId) { String url = UriComponentsBuilder.fromHttpUrl(tenantServiceUrl) .path("/api/internal/sdk/apps/{appId}/services/{platform}/{serviceType}") diff --git a/im-service/src/main/java/com/xuqm/im/ws/ImSessionKickListener.java b/im-service/src/main/java/com/xuqm/im/ws/ImSessionKickListener.java new file mode 100644 index 0000000..c2153be --- /dev/null +++ b/im-service/src/main/java/com/xuqm/im/ws/ImSessionKickListener.java @@ -0,0 +1,68 @@ +package com.xuqm.im.ws; + +import com.xuqm.im.service.ImFeatureConfigClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.simp.user.SimpUserRegistry; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionConnectedEvent; + +import java.security.Principal; +import java.util.Map; + +@Component +public class ImSessionKickListener { + + private static final Logger log = LoggerFactory.getLogger(ImSessionKickListener.class); + + private final SimpMessagingTemplate messagingTemplate; + private final SimpUserRegistry userRegistry; + private final ImFeatureConfigClient featureConfigClient; + + public ImSessionKickListener(SimpMessagingTemplate messagingTemplate, + SimpUserRegistry userRegistry, + ImFeatureConfigClient featureConfigClient) { + this.messagingTemplate = messagingTemplate; + this.userRegistry = userRegistry; + this.featureConfigClient = featureConfigClient; + } + + @EventListener + public void onSessionConnected(SessionConnectedEvent event) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage()); + Principal principal = accessor.getUser(); + if (!(principal instanceof UsernamePasswordAuthenticationToken auth)) { + return; + } + String userId = auth.getName(); + String sessionId = accessor.getSessionId(); + + String appId = null; + Object details = auth.getDetails(); + if (details instanceof Map detailsMap) { + Object v = detailsMap.get("appId"); + if (v != null) appId = v.toString(); + } + if (appId == null || appId.isBlank()) { + return; + } + + String mode = featureConfigClient.multiDeviceLoginMode(appId); + if ("MULTI_DEVICE_FREE".equals(mode)) { + return; + } + + var user = userRegistry.getUser(userId); + if (user == null || user.getSessions().size() <= 1) { + return; + } + + log.info("Sending kick to userId={} appId={} mode={} newSessionId={}", userId, appId, mode, sessionId); + Map payload = Map.of("type", "KICKED", "sessionId", sessionId, "reason", mode.toLowerCase()); + messagingTemplate.convertAndSendToUser(userId, "/queue/system", payload); + } +} diff --git a/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java b/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java index 78cfe2e..e008577 100644 --- a/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java +++ b/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java @@ -154,18 +154,24 @@ public class PushDispatcher { if (devices.isEmpty()) { return List.of(); } - if (!imConfigClient.allowMultiDeviceLogin(appId)) { - return List.of(devices.get(0)); - } - return devices.stream() - .collect(Collectors.toMap( - DeviceTokenEntity::getVendor, - device -> device, - (first, ignored) -> first, - java.util.LinkedHashMap::new)) - .values() - .stream() - .toList(); + String mode = imConfigClient.multiDeviceLoginMode(appId); + return switch (mode) { + case "SINGLE_DEVICE" -> List.of(devices.get(0)); + case "SAME_PLATFORM_ONE" -> devices.stream() + .collect(Collectors.toMap( + d -> d.getPlatform() != null ? d.getPlatform() : d.getVendor().name(), + d -> d, + (first, ignored) -> first, + java.util.LinkedHashMap::new)) + .values().stream().toList(); + default -> devices.stream() + .collect(Collectors.toMap( + DeviceTokenEntity::getVendor, + d -> d, + (first, ignored) -> first, + java.util.LinkedHashMap::new)) + .values().stream().toList(); + }; } public void registerToken(String appId, diff --git a/push-service/src/main/java/com/xuqm/push/service/TenantImConfigClient.java b/push-service/src/main/java/com/xuqm/push/service/TenantImConfigClient.java index eea9d0d..d3a7b37 100644 --- a/push-service/src/main/java/com/xuqm/push/service/TenantImConfigClient.java +++ b/push-service/src/main/java/com/xuqm/push/service/TenantImConfigClient.java @@ -30,7 +30,7 @@ public class TenantImConfigClient { this.objectMapper = objectMapper; } - public boolean allowMultiDeviceLogin(String appId) { + public String multiDeviceLoginMode(String appId) { try { HttpHeaders headers = new HttpHeaders(); headers.set("X-Internal-Token", internalToken); @@ -42,11 +42,25 @@ public class TenantImConfigClient { JsonNode body = response.getBody(); String config = body == null ? "" : body.path("data").path("config").asText(""); if (!config.isBlank()) { - return objectMapper.readTree(config).path("allowMultiDeviceLogin").asBoolean(true); + JsonNode tree = objectMapper.readTree(config); + String mode = tree.path("multiDeviceLoginMode").asText(""); + if (!mode.isBlank()) { + return switch (mode.toUpperCase()) { + case "SAME_PLATFORM_ONE", "SINGLE_DEVICE" -> mode.toUpperCase(); + default -> "MULTI_DEVICE_FREE"; + }; + } + // backward compat: allowMultiDeviceLogin=false → SINGLE_DEVICE + boolean multi = tree.path("allowMultiDeviceLogin").asBoolean(true); + return multi ? "MULTI_DEVICE_FREE" : "SINGLE_DEVICE"; } } catch (Exception e) { log.warn("load tenant im config failed appId={} reason={}", appId, e.getMessage()); } - return true; + return "MULTI_DEVICE_FREE"; + } + + public boolean allowMultiDeviceLogin(String appId) { + return !"SINGLE_DEVICE".equals(multiDeviceLoginMode(appId)); } }