From 0a267c5f70062396e5b67b42f7557ec5b536c0e7 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Fri, 22 May 2026 16:41:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=A1=E9=AA=8C=20SDK=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E6=97=B6=20packageName=20=E4=B8=8E=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E9=85=8D=E7=BD=AE=E7=9A=84=20appKey=20=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - im/push/update 三个服务登录/检查更新接口新增必填参数 packageName - 调用对应服务的 tenant-service 内部接口获取 platformInfo,与传入包名比对,不匹配返回 403 - update 服务按 platform 字段精确匹配(ANDROID/IOS/HARMONY 各用对应字段) - im/push 服务对三端包名任一匹配即通过 - ImAppSecretClient / PushAppSecretClient 新增 getPlatformInfo 缓存方法 - 新增 UpdateTenantClient 用于 update-service 调用 tenant-service platformInfo 接口 Co-Authored-By: Claude Sonnet 4.6 --- .../xuqm/im/controller/AuthController.java | 24 +++++++- .../xuqm/im/service/ImAppSecretClient.java | 32 ++++++++++- .../push/controller/PushAuthController.java | 30 ++++++++-- .../push/service/PushAppSecretClient.java | 32 ++++++++++- .../controller/AppVersionController.java | 22 +++++++- .../update/service/UpdateTenantClient.java | 55 +++++++++++++++++++ 6 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 update-service/src/main/java/com/xuqm/update/service/UpdateTenantClient.java diff --git a/im-service/src/main/java/com/xuqm/im/controller/AuthController.java b/im-service/src/main/java/com/xuqm/im/controller/AuthController.java index aef3657..cfd956d 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/AuthController.java +++ b/im-service/src/main/java/com/xuqm/im/controller/AuthController.java @@ -1,7 +1,9 @@ package com.xuqm.im.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.im.service.ImAccountService; +import com.xuqm.im.service.ImAppSecretClient; import jakarta.validation.constraints.NotBlank; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -16,20 +18,38 @@ import java.util.Map; public class AuthController { private final ImAccountService accountService; + private final ImAppSecretClient appSecretClient; - public AuthController(ImAccountService accountService) { + public AuthController(ImAccountService accountService, ImAppSecretClient appSecretClient) { this.accountService = accountService; + this.appSecretClient = appSecretClient; } @PostMapping("/login") public ResponseEntity>> login( @RequestParam @NotBlank String appKey, @RequestParam @NotBlank String userId, - @RequestParam @NotBlank String userSig) { + @RequestParam @NotBlank String userSig, + @RequestParam @NotBlank String packageName) { if (userSig.isBlank()) { return ResponseEntity.status(401).body(ApiResponse.error(401, "Missing userSig")); } + validatePackageName(appKey, packageName); ImAccountService.LoginResult result = accountService.loginWithUserSig(appKey, userId, userSig); return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token(), "admin", result.admin()))); } + + private void validatePackageName(String appKey, String packageName) { + ImAppSecretClient.PlatformInfo info = appSecretClient.getPlatformInfo(appKey); + String android = info.androidPackageName(); + String ios = info.iosBundleId(); + String harmony = info.harmonyBundleName(); + boolean anyConfigured = hasText(android) || hasText(ios) || hasText(harmony); + if (anyConfigured) { + boolean matches = packageName.equals(android) || packageName.equals(ios) || packageName.equals(harmony); + if (!matches) throw new BusinessException(403, "包名与应用配置不匹配"); + } + } + + private static boolean hasText(String s) { return s != null && !s.isBlank(); } } diff --git a/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java b/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java index de9ab32..0dc793e 100644 --- a/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java +++ b/im-service/src/main/java/com/xuqm/im/service/ImAppSecretClient.java @@ -18,8 +18,11 @@ import java.util.concurrent.ConcurrentHashMap; @Component public class ImAppSecretClient { + public record PlatformInfo(String androidPackageName, String iosBundleId, String harmonyBundleName) {} + private final RestTemplate restTemplate = new RestTemplate(); - private final Map cache = new ConcurrentHashMap<>(); + private final Map secretCache = new ConcurrentHashMap<>(); + private final Map platformInfoCache = new ConcurrentHashMap<>(); @Value("${im.tenant-service-url:http://127.0.0.1:8081}") private String tenantServiceUrl; @@ -28,7 +31,11 @@ public class ImAppSecretClient { private String internalToken; public String getAppSecret(String appKey) { - return cache.computeIfAbsent(appKey, this::fetchAppSecret); + return secretCache.computeIfAbsent(appKey, this::fetchAppSecret); + } + + public PlatformInfo getPlatformInfo(String appKey) { + return platformInfoCache.computeIfAbsent(appKey, this::fetchPlatformInfo); } private String fetchAppSecret(String appKey) { @@ -56,4 +63,25 @@ public class ImAppSecretClient { } throw new BusinessException(502, "Failed to resolve app secret for appKey: " + appKey); } + + private PlatformInfo fetchPlatformInfo(String appKey) { + String url = UriComponentsBuilder.fromHttpUrl(tenantServiceUrl) + .path("/api/internal/sdk/apps/{appKey}/platform-info") + .buildAndExpand(appKey) + .toUriString(); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Internal-Token", internalToken); + try { + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), JsonNode.class); + JsonNode data = response.getBody() == null ? null : response.getBody().path("data"); + if (response.getStatusCode().is2xxSuccessful() && data != null && !data.isMissingNode()) { + return new PlatformInfo( + data.path("androidPackageName").asText(null), + data.path("iosBundleId").asText(null), + data.path("harmonyBundleName").asText(null)); + } + } catch (RestClientException ignored) {} + return new PlatformInfo(null, null, null); + } } diff --git a/push-service/src/main/java/com/xuqm/push/controller/PushAuthController.java b/push-service/src/main/java/com/xuqm/push/controller/PushAuthController.java index 91a774b..f8350c2 100644 --- a/push-service/src/main/java/com/xuqm/push/controller/PushAuthController.java +++ b/push-service/src/main/java/com/xuqm/push/controller/PushAuthController.java @@ -1,8 +1,10 @@ package com.xuqm.push.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.push.entity.DeviceTokenEntity; import com.xuqm.push.service.PushAccountService; +import com.xuqm.push.service.PushAppSecretClient; import com.xuqm.push.service.PushDispatcher; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -18,21 +20,21 @@ public class PushAuthController { private final PushAccountService accountService; private final PushDispatcher pushDispatcher; + private final PushAppSecretClient appSecretClient; - public PushAuthController(PushAccountService accountService, PushDispatcher pushDispatcher) { + public PushAuthController(PushAccountService accountService, PushDispatcher pushDispatcher, + PushAppSecretClient appSecretClient) { this.accountService = accountService; this.pushDispatcher = pushDispatcher; + this.appSecretClient = appSecretClient; } - /** - * Login with userSig. On success, automatically registers the device push token. - * Returns a pushToken (JWT) used to authenticate subsequent push service requests. - */ @PostMapping("/login") public ResponseEntity>> login( @RequestParam String appKey, @RequestParam String userId, @RequestParam String userSig, + @RequestParam String packageName, @RequestParam DeviceTokenEntity.Vendor vendor, @RequestParam String token, @RequestParam(required = false) String platform, @@ -42,6 +44,7 @@ public class PushAuthController { @RequestParam(required = false) String osVersion, @RequestParam(required = false) String appVersion) { + validatePackageName(appKey, packageName); PushAccountService.LoginResult result = accountService.loginWithUserSig(appKey, userId, userSig); pushDispatcher.registerToken(appKey, userId, vendor, token, platform, deviceId, brand, model, osVersion, appVersion); @@ -53,4 +56,21 @@ public class PushAuthController { ); return ResponseEntity.ok(ApiResponse.success(response)); } + + private void validatePackageName(String appKey, String packageName) { + if (packageName == null || packageName.isBlank()) { + throw new BusinessException(403, "packageName is required"); + } + PushAppSecretClient.PlatformInfo info = appSecretClient.getPlatformInfo(appKey); + String android = info.androidPackageName(); + String ios = info.iosBundleId(); + String harmony = info.harmonyBundleName(); + boolean anyConfigured = hasText(android) || hasText(ios) || hasText(harmony); + if (anyConfigured) { + boolean matches = packageName.equals(android) || packageName.equals(ios) || packageName.equals(harmony); + if (!matches) throw new BusinessException(403, "包名与应用配置不匹配"); + } + } + + private static boolean hasText(String s) { return s != null && !s.isBlank(); } } diff --git a/push-service/src/main/java/com/xuqm/push/service/PushAppSecretClient.java b/push-service/src/main/java/com/xuqm/push/service/PushAppSecretClient.java index bf0c41e..aa99840 100644 --- a/push-service/src/main/java/com/xuqm/push/service/PushAppSecretClient.java +++ b/push-service/src/main/java/com/xuqm/push/service/PushAppSecretClient.java @@ -18,8 +18,11 @@ import java.util.concurrent.ConcurrentHashMap; @Component public class PushAppSecretClient { + public record PlatformInfo(String androidPackageName, String iosBundleId, String harmonyBundleName) {} + private final RestTemplate restTemplate = new RestTemplate(); - private final Map cache = new ConcurrentHashMap<>(); + private final Map secretCache = new ConcurrentHashMap<>(); + private final Map platformInfoCache = new ConcurrentHashMap<>(); @Value("${push.tenant-service-url:http://127.0.0.1:8081}") private String tenantServiceUrl; @@ -28,7 +31,11 @@ public class PushAppSecretClient { private String internalToken; public String getAppSecret(String appKey) { - return cache.computeIfAbsent(appKey, this::fetchAppSecret); + return secretCache.computeIfAbsent(appKey, this::fetchAppSecret); + } + + public PlatformInfo getPlatformInfo(String appKey) { + return platformInfoCache.computeIfAbsent(appKey, this::fetchPlatformInfo); } private String fetchAppSecret(String appKey) { @@ -56,4 +63,25 @@ public class PushAppSecretClient { } throw new BusinessException(502, "Failed to resolve app secret for appKey: " + appKey); } + + private PlatformInfo fetchPlatformInfo(String appKey) { + String url = UriComponentsBuilder.fromHttpUrl(tenantServiceUrl) + .path("/api/internal/sdk/apps/{appKey}/platform-info") + .buildAndExpand(appKey) + .toUriString(); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Internal-Token", internalToken); + try { + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), JsonNode.class); + JsonNode data = response.getBody() == null ? null : response.getBody().path("data"); + if (response.getStatusCode().is2xxSuccessful() && data != null && !data.isMissingNode()) { + return new PlatformInfo( + data.path("androidPackageName").asText(null), + data.path("iosBundleId").asText(null), + data.path("harmonyBundleName").asText(null)); + } + } catch (RestClientException ignored) {} + return new PlatformInfo(null, null, null); + } } diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java index 4917066..6e6983f 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java @@ -21,6 +21,8 @@ import com.xuqm.update.service.UpdateAssetService; import com.xuqm.update.service.PublishConfigService; import com.xuqm.update.service.AppStoreService; import com.xuqm.update.service.ImPushUserClient; +import com.xuqm.update.service.UpdateTenantClient; +import com.xuqm.common.exception.BusinessException; @RestController @RequestMapping("/api/v1/updates") @@ -33,21 +35,23 @@ public class AppVersionController { private final PublishConfigService publishConfigService; private final AppStoreService appStoreService; private final UpdateOperationLogService operationLogService; - private final ImPushUserClient imPushUserClient; + private final UpdateTenantClient tenantClient; public AppVersionController(AppVersionRepository versionRepository, UpdateAssetService updateAssetService, PublishConfigService publishConfigService, AppStoreService appStoreService, UpdateOperationLogService operationLogService, - ImPushUserClient imPushUserClient) { + ImPushUserClient imPushUserClient, + UpdateTenantClient tenantClient) { this.versionRepository = versionRepository; this.updateAssetService = updateAssetService; this.publishConfigService = publishConfigService; this.appStoreService = appStoreService; this.operationLogService = operationLogService; this.imPushUserClient = imPushUserClient; + this.tenantClient = tenantClient; } @GetMapping("/app/check") @@ -55,8 +59,10 @@ public class AppVersionController { @RequestParam String appKey, @RequestParam AppVersionEntity.Platform platform, @RequestParam int currentVersionCode, + @RequestParam @jakarta.validation.constraints.NotBlank String packageName, @RequestParam(required = false) String userId) { + validatePackageName(appKey, platform, packageName); boolean allowAnonymousCheck = publishConfigService.allowAnonymousUpdateCheck(appKey); Optional latest = versionRepository @@ -489,6 +495,18 @@ public class AppVersionController { return currentStatus == AppVersionEntity.PublishStatus.PUBLISHED ? "PUBLISH" : "SAVE_DRAFT"; } + private void validatePackageName(String appKey, AppVersionEntity.Platform platform, String packageName) { + UpdateTenantClient.PlatformInfo info = tenantClient.getPlatformInfo(appKey); + String registered = switch (platform) { + case IOS -> info.iosBundleId(); + case HARMONY -> info.harmonyBundleName(); + default -> info.androidPackageName(); + }; + if (hasText(registered) && !registered.equals(packageName)) { + throw new BusinessException(403, "包名与应用配置不匹配"); + } + } + private boolean hasText(String value) { return value != null && !value.isBlank(); } diff --git a/update-service/src/main/java/com/xuqm/update/service/UpdateTenantClient.java b/update-service/src/main/java/com/xuqm/update/service/UpdateTenantClient.java new file mode 100644 index 0000000..51c07d5 --- /dev/null +++ b/update-service/src/main/java/com/xuqm/update/service/UpdateTenantClient.java @@ -0,0 +1,55 @@ +package com.xuqm.update.service; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class UpdateTenantClient { + + public record PlatformInfo(String androidPackageName, String iosBundleId, String harmonyBundleName) {} + + private final RestTemplate restTemplate = new RestTemplate(); + private final Map cache = new ConcurrentHashMap<>(); + + @Value("${sdk.tenant-service-url:http://xuqm-tenant-service:9001}") + private String tenantServiceUrl; + + @Value("${sdk.internal-token:xuqm-internal-token}") + private String internalToken; + + public PlatformInfo getPlatformInfo(String appKey) { + return cache.computeIfAbsent(appKey, this::fetchPlatformInfo); + } + + private PlatformInfo fetchPlatformInfo(String appKey) { + String url = UriComponentsBuilder.fromHttpUrl(tenantServiceUrl) + .path("/api/internal/sdk/apps/{appKey}/platform-info") + .buildAndExpand(appKey) + .toUriString(); + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Internal-Token", internalToken); + try { + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), JsonNode.class); + JsonNode data = response.getBody() == null ? null : response.getBody().path("data"); + if (response.getStatusCode().is2xxSuccessful() && data != null && !data.isMissingNode()) { + return new PlatformInfo( + data.path("androidPackageName").asText(null), + data.path("iosBundleId").asText(null), + data.path("harmonyBundleName").asText(null)); + } + } catch (RestClientException ignored) {} + return new PlatformInfo(null, null, null); + } +}