feat: validate packageName against appKey on SDK and license init

SdkConfigController: require packageName param; reject with 403 if it doesn't
match the platform-specific name registered for the app (skipped when app has
no name configured yet).

LicensePublicController: add required packageName to register/verify requests.
DeviceService: validatePackageName() checks against android/ios/harmony names
stored on AppLicenseEntity; rejects if any are configured and none match.
AppLicenseEntity: add android_package_name, ios_bundle_id, harmony_bundle_name
columns (auto-migrated via ddl-auto=update).
LicenseInternalController/AppLicenseService: accept and persist package names
via upsert endpoint.
LicenseServiceClient/FeatureServiceManager: pass app package names when syncing
license records to license-service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-05-22 16:31:50 +08:00
父节点 138360b760
当前提交 4c0db6e9b7
共有 8 个文件被更改,包括 106 次插入12 次删除

查看文件

@ -73,7 +73,10 @@ public class LicenseInternalController {
req.maxDevices(),
req.expiresAt(),
req.isActive(),
req.remark());
req.remark(),
req.androidPackageName(),
req.iosBundleId(),
req.harmonyBundleName());
return ResponseEntity.ok(ApiResponse.success(license));
}
@ -99,6 +102,9 @@ public class LicenseInternalController {
Integer maxDevices,
LocalDateTime expiresAt,
Boolean isActive,
String remark
String remark,
String androidPackageName,
String iosBundleId,
String harmonyBundleName
) {}
}

查看文件

@ -29,6 +29,7 @@ public class LicensePublicController {
public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
DeviceService.RegisterResult result = deviceService.register(
req.appKey(),
req.packageName(),
req.deviceId(),
req.deviceName(),
req.deviceModel(),
@ -46,7 +47,7 @@ public class LicensePublicController {
@PostMapping("/verify")
public ResponseEntity<ApiResponse<Map<String, Object>>> verify(@Valid @RequestBody VerifyRequest req) {
DeviceService.VerifyResult result = deviceService.verify(req.appKey(), req.deviceId(), req.token(), req.userInfo());
DeviceService.VerifyResult result = deviceService.verify(req.appKey(), req.packageName(), req.deviceId(), req.token(), req.userInfo());
Map<String, Object> data = new java.util.LinkedHashMap<>();
data.put("valid", result.valid());
if (result.error() != null) {
@ -57,6 +58,7 @@ public class LicensePublicController {
public record RegisterRequest(
@NotBlank String appKey,
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@JsonProperty("deviceName") @JsonAlias("device_name") String deviceName,
@JsonProperty("deviceModel") @JsonAlias("device_model") String deviceModel,
@ -67,6 +69,7 @@ public class LicensePublicController {
public record VerifyRequest(
@NotBlank String appKey,
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@NotBlank String token,
@JsonProperty("userInfo") @JsonAlias("user_info") JsonNode userInfo

查看文件

@ -17,6 +17,15 @@ public class AppLicenseEntity {
@Column(nullable = false, length = 255)
private String name;
@Column(name = "android_package_name", length = 128)
private String androidPackageName;
@Column(name = "ios_bundle_id", length = 128)
private String iosBundleId;
@Column(name = "harmony_bundle_name", length = 128)
private String harmonyBundleName;
@Column(nullable = false, name = "max_devices")
private Integer maxDevices = 1;
@ -44,6 +53,15 @@ public class AppLicenseEntity {
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAndroidPackageName() { return androidPackageName; }
public void setAndroidPackageName(String androidPackageName) { this.androidPackageName = androidPackageName; }
public String getIosBundleId() { return iosBundleId; }
public void setIosBundleId(String iosBundleId) { this.iosBundleId = iosBundleId; }
public String getHarmonyBundleName() { return harmonyBundleName; }
public void setHarmonyBundleName(String harmonyBundleName) { this.harmonyBundleName = harmonyBundleName; }
public Integer getMaxDevices() { return maxDevices; }
public void setMaxDevices(Integer maxDevices) { this.maxDevices = maxDevices; }

查看文件

@ -24,15 +24,19 @@ public class AppLicenseService {
@Transactional
public AppLicenseEntity upsert(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, Boolean isActive, String remark) {
LocalDateTime expiresAt, Boolean isActive, String remark,
String androidPackageName, String iosBundleId, String harmonyBundleName) {
return repository.findById(appKey)
.map(license -> update(appKey, name, maxDevices, expiresAt, false, isActive, remark))
.orElseGet(() -> create(appKey, name, maxDevices, expiresAt, isActive, remark));
.map(license -> update(appKey, name, maxDevices, expiresAt, false, isActive, remark,
androidPackageName, iosBundleId, harmonyBundleName))
.orElseGet(() -> create(appKey, name, maxDevices, expiresAt, isActive, remark,
androidPackageName, iosBundleId, harmonyBundleName));
}
@Transactional
public AppLicenseEntity update(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, boolean clearExpiresAt, Boolean isActive, String remark) {
LocalDateTime expiresAt, boolean clearExpiresAt, Boolean isActive, String remark,
String androidPackageName, String iosBundleId, String harmonyBundleName) {
AppLicenseEntity license = getByAppKey(appKey);
if (name != null) license.setName(name);
if (maxDevices != null) license.setMaxDevices(maxDevices);
@ -43,6 +47,9 @@ public class AppLicenseService {
}
if (isActive != null) license.setIsActive(isActive);
if (remark != null) license.setRemark(remark);
if (androidPackageName != null) license.setAndroidPackageName(androidPackageName);
if (iosBundleId != null) license.setIosBundleId(iosBundleId);
if (harmonyBundleName != null) license.setHarmonyBundleName(harmonyBundleName);
license.setUpdatedAt(LocalDateTime.now());
return repository.save(license);
}
@ -73,7 +80,8 @@ public class AppLicenseService {
}
private AppLicenseEntity create(String appKey, String name, Integer maxDevices,
LocalDateTime expiresAt, Boolean isActive, String remark) {
LocalDateTime expiresAt, Boolean isActive, String remark,
String androidPackageName, String iosBundleId, String harmonyBundleName) {
AppLicenseEntity license = new AppLicenseEntity();
license.setAppKey(appKey);
license.setName(name);
@ -82,6 +90,9 @@ public class AppLicenseService {
license.setExpiresAt(expiresAt);
license.setIsActive(isActive != null ? isActive : true);
license.setRemark(remark);
license.setAndroidPackageName(androidPackageName);
license.setIosBundleId(iosBundleId);
license.setHarmonyBundleName(harmonyBundleName);
license.setCreatedAt(LocalDateTime.now());
license.setUpdatedAt(LocalDateTime.now());
return repository.save(license);

查看文件

@ -43,9 +43,11 @@ public class DeviceService {
}
@Transactional
public RegisterResult register(String appKey, String deviceId, String deviceName,
public RegisterResult register(String appKey, String packageName, String deviceId, String deviceName,
String deviceModel, String deviceVendor, String osVersion,
JsonNode userInfo) {
validatePackageName(appKey, packageName);
// Check if device already registered
Optional<DeviceEntity> existingOpt = findByDeviceId(deviceId);
if (existingOpt.isPresent()) {
@ -108,7 +110,12 @@ public class DeviceService {
}
@Transactional
public VerifyResult verify(String appKey, String deviceId, String token, JsonNode userInfo) {
public VerifyResult verify(String appKey, String packageName, String deviceId, String token, JsonNode userInfo) {
try {
validatePackageName(appKey, packageName);
} catch (BusinessException e) {
return new VerifyResult(false, e.getMessage());
}
if (!licenseAuthService.verifyTokenPayload(token, appKey, deviceId)) {
return new VerifyResult(false, "Token mismatch");
}
@ -155,6 +162,32 @@ public class DeviceService {
appLicenseService.incrementRegisteredDevices(device.getAppKey());
}
private void validatePackageName(String appKey, String packageName) {
if (packageName == null || packageName.isBlank()) {
throw new BusinessException(403, "packageName is required");
}
AppLicenseEntity license;
try {
license = appLicenseService.getByAppKey(appKey);
} catch (BusinessException e) {
throw new BusinessException(403, "App license not found");
}
String android = license.getAndroidPackageName();
String ios = license.getIosBundleId();
String harmony = license.getHarmonyBundleName();
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();
}
public static String hashToken(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");

查看文件

@ -2,6 +2,7 @@ package com.xuqm.tenant.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.common.exception.BusinessException;
import com.xuqm.common.model.ApiResponse;
import com.xuqm.tenant.config.PrivateDeploymentProperties;
import com.xuqm.tenant.entity.AppEntity;
@ -57,9 +58,11 @@ public class SdkConfigController {
@GetMapping("/config")
public ResponseEntity<ApiResponse<SdkConfigResponse>> getConfig(
@RequestParam String appKey,
@RequestParam String packageName,
@RequestParam(required = false, defaultValue = "ANDROID") FeatureServiceEntity.Platform platform) {
AppEntity app = sdkAppProvisioningService.resolveApp(appKey);
validatePackageName(app, platform, packageName);
List<FeatureServiceEntity> features = featureServiceRepository.findByAppKey(app.getAppKey());
// In private deployments, intersect DB feature flags with deployment-level service availability
@ -129,6 +132,17 @@ public class SdkConfigController {
JsonNode pushConfig
) {}
private void validatePackageName(AppEntity app, FeatureServiceEntity.Platform platform, String packageName) {
String registered = switch (platform) {
case IOS -> app.getIosBundleId();
case HARMONY -> app.getHarmonyBundleName();
default -> app.getPackageName();
};
if (registered != null && !registered.isBlank() && !registered.equals(packageName)) {
throw new BusinessException(403, "包名与应用配置不匹配");
}
}
private JsonNode parseConfig(String config) {
if (config == null || config.isBlank()) {
return objectMapper.createObjectNode();

查看文件

@ -192,7 +192,8 @@ public class FeatureServiceManager {
}
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
appRepository.findByAppKey(normalizedAppId).ifPresent(app ->
licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1, expiresAt));
licenseServiceClient.syncAppLicense(app.getAppKey(), app.getName(), 1, expiresAt,
app.getPackageName(), app.getIosBundleId(), app.getHarmonyBundleName()));
}
sendActivationImNotification(req.getAppKey(), req.getServiceType().name(), "APPROVED", reviewNote);
return req;

查看文件

@ -49,10 +49,15 @@ public class LicenseServiceClient {
}
public void syncAppLicense(String appKey, String name, Integer maxDevices) {
syncAppLicense(appKey, name, maxDevices, null);
syncAppLicense(appKey, name, maxDevices, null, null, null, null);
}
public void syncAppLicense(String appKey, String name, Integer maxDevices, String expiresAt) {
syncAppLicense(appKey, name, maxDevices, expiresAt, null, null, null);
}
public void syncAppLicense(String appKey, String name, Integer maxDevices, String expiresAt,
String androidPackageName, String iosBundleId, String harmonyBundleName) {
try {
Map<String, Object> body = new java.util.HashMap<>();
body.put("id", appKey);
@ -61,6 +66,9 @@ public class LicenseServiceClient {
if (expiresAt != null && !expiresAt.isBlank()) {
body.put("expiresAt", expiresAt);
}
if (androidPackageName != null) body.put("androidPackageName", androidPackageName);
if (iosBundleId != null) body.put("iosBundleId", iosBundleId);
if (harmonyBundleName != null) body.put("harmonyBundleName", harmonyBundleName);
callInternal("/api/license/internal/apps", HttpMethod.POST, body);
} catch (Exception e) {
// ignore