refactor(license): remove server-side package name validation
Package name matching is now entirely the SDK's responsibility. - DeviceService: drop packageName param from register/verify, delete validatePackageName() - LicensePublicController: remove matchesPackageName check in resolveAppKey(), remove packageName from service calls, add GET /api/license/app-info for SDK appKey-only flow to fetch configured package names for local comparison Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
26261263a0
当前提交
b7c2f0144f
@ -6,15 +6,15 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.xuqm.common.exception.BusinessException;
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.common.security.LicenseFileCrypto;
|
import com.xuqm.common.security.LicenseFileCrypto;
|
||||||
|
import com.xuqm.license.entity.AppLicenseEntity;
|
||||||
|
import com.xuqm.license.service.AppLicenseService;
|
||||||
import com.xuqm.license.service.DeviceService;
|
import com.xuqm.license.service.DeviceService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -22,24 +22,25 @@ import java.util.Map;
|
|||||||
public class LicensePublicController {
|
public class LicensePublicController {
|
||||||
|
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final AppLicenseService appLicenseService;
|
||||||
|
|
||||||
public LicensePublicController(DeviceService deviceService) {
|
public LicensePublicController(DeviceService deviceService, AppLicenseService appLicenseService) {
|
||||||
this.deviceService = deviceService;
|
this.deviceService = deviceService;
|
||||||
|
this.appLicenseService = appLicenseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
|
public ResponseEntity<ApiResponse<Map<String, Object>>> register(@Valid @RequestBody RegisterRequest req) {
|
||||||
String resolvedAppKey = resolveAppKey(req.appKey(), req.packageName(), req.licenseFile());
|
String resolvedAppKey = resolveAppKey(req.appKey(), req.licenseFile());
|
||||||
DeviceService.RegisterResult result = deviceService.register(
|
DeviceService.RegisterResult result = deviceService.register(
|
||||||
resolvedAppKey,
|
resolvedAppKey,
|
||||||
req.packageName(),
|
|
||||||
req.deviceId(),
|
req.deviceId(),
|
||||||
req.deviceName(),
|
req.deviceName(),
|
||||||
req.deviceModel(),
|
req.deviceModel(),
|
||||||
req.deviceVendor(),
|
req.deviceVendor(),
|
||||||
req.osVersion(),
|
req.osVersion(),
|
||||||
req.userInfo());
|
req.userInfo());
|
||||||
Map<String, Object> data = new java.util.LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("success", result.success());
|
data.put("success", result.success());
|
||||||
data.put("token", result.token());
|
data.put("token", result.token());
|
||||||
if (result.message() != null) {
|
if (result.message() != null) {
|
||||||
@ -50,9 +51,9 @@ 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) {
|
||||||
String resolvedAppKey = resolveAppKey(req.appKey(), req.packageName(), req.licenseFile());
|
String resolvedAppKey = resolveAppKey(req.appKey(), req.licenseFile());
|
||||||
DeviceService.VerifyResult result = deviceService.verify(resolvedAppKey, req.packageName(), req.deviceId(), req.token(), req.userInfo());
|
DeviceService.VerifyResult result = deviceService.verify(resolvedAppKey, req.deviceId(), req.token(), req.userInfo());
|
||||||
Map<String, Object> data = new java.util.LinkedHashMap<>();
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
data.put("valid", result.valid());
|
data.put("valid", result.valid());
|
||||||
if (result.error() != null) {
|
if (result.error() != null) {
|
||||||
data.put("error", result.error());
|
data.put("error", result.error());
|
||||||
@ -60,12 +61,28 @@ public class LicensePublicController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(data));
|
return ResponseEntity.ok(ApiResponse.success(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String resolveAppKey(String appKey, String packageName, String licenseFile) {
|
/**
|
||||||
|
* Returns configured package names for the given appKey.
|
||||||
|
* Used by the SDK (appKey-only flow) to validate the caller's package name client-side.
|
||||||
|
*/
|
||||||
|
@GetMapping("/app-info")
|
||||||
|
public ResponseEntity<ApiResponse<Map<String, Object>>> appInfo(@RequestParam String appKey) {
|
||||||
|
AppLicenseEntity license;
|
||||||
|
try {
|
||||||
|
license = appLicenseService.getByAppKey(appKey);
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw new BusinessException(404, "App not found");
|
||||||
|
}
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("androidPackageName", license.getAndroidPackageName());
|
||||||
|
data.put("iosBundleId", license.getIosBundleId());
|
||||||
|
data.put("harmonyBundleName", license.getHarmonyBundleName());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveAppKey(String appKey, String licenseFile) {
|
||||||
if (licenseFile != null && !licenseFile.isBlank()) {
|
if (licenseFile != null && !licenseFile.isBlank()) {
|
||||||
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
LicenseFileCrypto.LicensePayload payload = LicenseFileCrypto.decrypt(licenseFile);
|
||||||
if (!payload.matchesPackageName(packageName)) {
|
|
||||||
throw new BusinessException(403, "包名与应用配置不匹配");
|
|
||||||
}
|
|
||||||
return payload.appKey();
|
return payload.appKey();
|
||||||
}
|
}
|
||||||
if (appKey == null || appKey.isBlank()) {
|
if (appKey == null || appKey.isBlank()) {
|
||||||
@ -76,7 +93,7 @@ public class LicensePublicController {
|
|||||||
|
|
||||||
public record RegisterRequest(
|
public record RegisterRequest(
|
||||||
String appKey,
|
String appKey,
|
||||||
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
@JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
||||||
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
||||||
@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,
|
||||||
@ -88,7 +105,7 @@ public class LicensePublicController {
|
|||||||
|
|
||||||
public record VerifyRequest(
|
public record VerifyRequest(
|
||||||
String appKey,
|
String appKey,
|
||||||
@NotBlank @JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
@JsonProperty("packageName") @JsonAlias("package_name") String packageName,
|
||||||
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
@JsonProperty("licenseFile") @JsonAlias("license_file") String licenseFile,
|
||||||
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
|
@NotBlank @JsonProperty("deviceId") @JsonAlias("device_id") String deviceId,
|
||||||
@NotBlank String token,
|
@NotBlank String token,
|
||||||
|
|||||||
@ -43,11 +43,9 @@ public class DeviceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public RegisterResult register(String appKey, String packageName, String deviceId, String deviceName,
|
public RegisterResult register(String appKey, 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 for this app
|
// Check if device already registered for this app
|
||||||
Optional<DeviceEntity> existingOpt = deviceRepository.findByAppKeyAndDeviceId(appKey, deviceId);
|
Optional<DeviceEntity> existingOpt = deviceRepository.findByAppKeyAndDeviceId(appKey, deviceId);
|
||||||
if (existingOpt.isPresent()) {
|
if (existingOpt.isPresent()) {
|
||||||
@ -116,12 +114,7 @@ public class DeviceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public VerifyResult verify(String appKey, String packageName, String deviceId, String token, JsonNode userInfo) {
|
public VerifyResult verify(String appKey, 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");
|
||||||
}
|
}
|
||||||
@ -168,28 +161,6 @@ 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) {
|
private static boolean hasText(String s) {
|
||||||
return s != null && !s.isBlank();
|
return s != null && !s.isBlank();
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户