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>
这个提交包含在:
父节点
138360b760
当前提交
4c0db6e9b7
@ -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
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户