feat: 校验 SDK 初始化时 packageName 与平台配置的 appKey 是否匹配
- 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 <noreply@anthropic.com>
这个提交包含在:
父节点
4c0db6e9b7
当前提交
0a267c5f70
@ -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<ApiResponse<Map<String, Object>>> 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(); }
|
||||
}
|
||||
|
||||
@ -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<String, String> cache = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> secretCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, PlatformInfo> 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<JsonNode> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ApiResponse<Map<String, Object>>> 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(); }
|
||||
}
|
||||
|
||||
@ -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<String, String> cache = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> secretCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, PlatformInfo> 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<JsonNode> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<AppVersionEntity> 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();
|
||||
}
|
||||
|
||||
@ -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<String, PlatformInfo> 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<JsonNode> 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);
|
||||
}
|
||||
}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户