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.maxDevices(),
req.expiresAt(), req.expiresAt(),
req.isActive(), req.isActive(),
req.remark()); req.remark(),
req.androidPackageName(),
req.iosBundleId(),
req.harmonyBundleName());
return ResponseEntity.ok(ApiResponse.success(license)); return ResponseEntity.ok(ApiResponse.success(license));
} }
@ -99,6 +102,9 @@ public class LicenseInternalController {
Integer maxDevices, Integer maxDevices,
LocalDateTime expiresAt, LocalDateTime expiresAt,
Boolean isActive, 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) { public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
DeviceService.RegisterResult result = deviceService.register( DeviceService.RegisterResult result = deviceService.register(
req.appKey(), req.appKey(),
req.packageName(),
req.deviceId(), req.deviceId(),
req.deviceName(), req.deviceName(),
req.deviceModel(), req.deviceModel(),
@ -46,7 +47,7 @@ public class LicensePublicController {
@PostMapping("/verify") @PostMapping("/verify")
public ResponseEntity<ApiResponse<Map<String, Object>>> verify(@Valid @RequestBody VerifyRequest req) { 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<>(); Map<String, Object> data = new java.util.LinkedHashMap<>();
data.put("valid", result.valid()); data.put("valid", result.valid());
if (result.error() != null) { if (result.error() != null) {
@ -57,6 +58,7 @@ public class LicensePublicController {
public record RegisterRequest( public record RegisterRequest(
@NotBlank String appKey, @NotBlank String appKey,
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId, @NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@JsonProperty("deviceName") @JsonAlias("device_name") String deviceName, @JsonProperty("deviceName") @JsonAlias("device_name") String deviceName,
@JsonProperty("deviceModel") @JsonAlias("device_model") String deviceModel, @JsonProperty("deviceModel") @JsonAlias("device_model") String deviceModel,
@ -67,6 +69,7 @@ public class LicensePublicController {
public record VerifyRequest( public record VerifyRequest(
@NotBlank String appKey, @NotBlank String appKey,
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId, @NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
@NotBlank String token, @NotBlank String token,
@JsonProperty("userInfo") @JsonAlias("user_info") JsonNode userInfo @JsonProperty("userInfo") @JsonAlias("user_info") JsonNode userInfo

查看文件

@ -17,6 +17,15 @@ public class AppLicenseEntity {
@Column(nullable = false, length = 255) @Column(nullable = false, length = 255)
private String name; 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") @Column(nullable = false, name = "max_devices")
private Integer maxDevices = 1; private Integer maxDevices = 1;
@ -44,6 +53,15 @@ public class AppLicenseEntity {
public String getName() { return name; } public String getName() { return name; }
public void setName(String name) { this.name = 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 Integer getMaxDevices() { return maxDevices; }
public void setMaxDevices(Integer maxDevices) { this.maxDevices = maxDevices; } public void setMaxDevices(Integer maxDevices) { this.maxDevices = maxDevices; }

查看文件

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

查看文件

@ -43,9 +43,11 @@ public class DeviceService {
} }
@Transactional @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, String deviceModel, String deviceVendor, String osVersion,
JsonNode userInfo) { JsonNode userInfo) {
validatePackageName(appKey, packageName);
// Check if device already registered // Check if device already registered
Optional<DeviceEntity> existingOpt = findByDeviceId(deviceId); Optional<DeviceEntity> existingOpt = findByDeviceId(deviceId);
if (existingOpt.isPresent()) { if (existingOpt.isPresent()) {
@ -108,7 +110,12 @@ public class DeviceService {
} }
@Transactional @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)) { if (!licenseAuthService.verifyTokenPayload(token, appKey, deviceId)) {
return new VerifyResult(false, "Token mismatch"); return new VerifyResult(false, "Token mismatch");
} }
@ -155,6 +162,32 @@ public class DeviceService {
appLicenseService.incrementRegisteredDevices(device.getAppKey()); 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) { public static String hashToken(String token) {
try { try {
MessageDigest digest = MessageDigest.getInstance("SHA-256"); 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.common.exception.BusinessException;
import com.xuqm.common.model.ApiResponse; import com.xuqm.common.model.ApiResponse;
import com.xuqm.tenant.config.PrivateDeploymentProperties; import com.xuqm.tenant.config.PrivateDeploymentProperties;
import com.xuqm.tenant.entity.AppEntity; import com.xuqm.tenant.entity.AppEntity;
@ -57,9 +58,11 @@ public class SdkConfigController {
@GetMapping("/config") @GetMapping("/config")
public ResponseEntity<ApiResponse<SdkConfigResponse>> getConfig( public ResponseEntity<ApiResponse<SdkConfigResponse>> getConfig(
@RequestParam String appKey, @RequestParam String appKey,
@RequestParam String packageName,
@RequestParam(required = false, defaultValue = "ANDROID") FeatureServiceEntity.Platform platform) { @RequestParam(required = false, defaultValue = "ANDROID") FeatureServiceEntity.Platform platform) {
AppEntity app = sdkAppProvisioningService.resolveApp(appKey); AppEntity app = sdkAppProvisioningService.resolveApp(appKey);
validatePackageName(app, platform, packageName);
List<FeatureServiceEntity> features = featureServiceRepository.findByAppKey(app.getAppKey()); List<FeatureServiceEntity> features = featureServiceRepository.findByAppKey(app.getAppKey());
// In private deployments, intersect DB feature flags with deployment-level service availability // In private deployments, intersect DB feature flags with deployment-level service availability
@ -129,6 +132,17 @@ public class SdkConfigController {
JsonNode pushConfig 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) { private JsonNode parseConfig(String config) {
if (config == null || config.isBlank()) { if (config == null || config.isBlank()) {
return objectMapper.createObjectNode(); return objectMapper.createObjectNode();

查看文件

@ -192,7 +192,8 @@ public class FeatureServiceManager {
} }
if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) { if (req.getServiceType() == FeatureServiceEntity.ServiceType.LICENSE) {
appRepository.findByAppKey(normalizedAppId).ifPresent(app -> 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); sendActivationImNotification(req.getAppKey(), req.getServiceType().name(), "APPROVED", reviewNote);
return req; return req;

查看文件

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