feat(sdk-license): client-side package name validation
Package name matching is now done entirely in the SDK before any network call: - License file flow (tryAutoInitialize): compare licenseFile.packageName with context.packageName; refuse initialization if mismatch - appKey-only flow (checkLicense): fetch GET /api/license/app-info and compare configured package names locally before register/verify - LicenseConfig: add fromLicenseFile flag to distinguish the two flows - LicenseModels: add AppInfoResponse with matchesPackageName helper - LicenseApiService: add getAppInfo() endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
63da94c670
当前提交
eb27e8f112
@ -5,4 +5,6 @@ data class LicenseConfig(
|
||||
val baseUrl: String = "https://auth.dev.xuqinmin.com/",
|
||||
val deviceName: String? = null,
|
||||
val cacheWindowMs: Long = 10 * 60 * 1000L, // 10 minutes
|
||||
/** True when initialized from a license file; package name validated locally before any network call. */
|
||||
val fromLicenseFile: Boolean = false,
|
||||
)
|
||||
|
||||
@ -84,8 +84,9 @@ object LicenseSDK {
|
||||
/**
|
||||
* Check license status. This will:
|
||||
* 1. Return cached OK status if within cache window
|
||||
* 2. Try to verify existing token
|
||||
* 3. Register new device if no token exists
|
||||
* 2. Validate package name (locally for license-file flow; via server for appKey flow)
|
||||
* 3. Try to verify existing token
|
||||
* 4. Register new device if no token exists
|
||||
*
|
||||
* @return LicenseResult.Success if license is valid, LicenseResult.Error otherwise
|
||||
*/
|
||||
@ -100,6 +101,26 @@ object LicenseSDK {
|
||||
return@withContext LicenseResult.Success("Cached")
|
||||
}
|
||||
|
||||
// appKey-only flow: fetch configured package names from server and validate locally
|
||||
if (!config.fromLicenseFile) {
|
||||
val actualPackage = appContext.packageName
|
||||
try {
|
||||
val infoResp = apiService.getAppInfo(config.appKey)
|
||||
val appInfo = infoResp.data
|
||||
if (appInfo != null) {
|
||||
val anyConfigured = !appInfo.androidPackageName.isNullOrBlank()
|
||||
|| !appInfo.iosBundleId.isNullOrBlank()
|
||||
|| !appInfo.harmonyBundleName.isNullOrBlank()
|
||||
if (anyConfigured && !appInfo.matchesPackageName(actualPackage)) {
|
||||
persistStatus(STATUS_DENIED)
|
||||
return@withContext LicenseResult.Error("Package name not authorized: $actualPackage")
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// If app-info fetch fails due to network, fall through and let register/verify handle it
|
||||
}
|
||||
}
|
||||
|
||||
val deviceId = getOrCreateDeviceId()
|
||||
val token = store.token
|
||||
|
||||
@ -251,6 +272,13 @@ object LicenseSDK {
|
||||
val ctx = LicenseContextHolder.appContext ?: return false
|
||||
val licenseFile = LicenseFileReader.read(ctx) ?: return false
|
||||
val appKey = licenseFile.appKey.takeIf { it.isNotBlank() } ?: return false
|
||||
|
||||
// Validate package name locally before any network call
|
||||
val configuredPackageName = licenseFile.packageName
|
||||
if (!configuredPackageName.isNullOrBlank() && configuredPackageName != ctx.packageName) {
|
||||
return false
|
||||
}
|
||||
|
||||
appContext = ctx
|
||||
store = LicenseStore(ctx)
|
||||
if (store.appKey != null && store.appKey != appKey) {
|
||||
@ -261,6 +289,7 @@ object LicenseSDK {
|
||||
appKey = appKey,
|
||||
baseUrl = normalizeBaseUrl(licenseFile.baseUrl ?: DEFAULT_BASE_URL),
|
||||
deviceName = DeviceInfoProvider.getDeviceName(),
|
||||
fromLicenseFile = true,
|
||||
)
|
||||
apiService = LicenseHttpClient.create(LicenseApiService::class.java, config.baseUrl)
|
||||
initialized = true
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
package com.xuqm.sdk.license.api
|
||||
|
||||
import com.xuqm.sdk.license.model.ApiResponse
|
||||
import com.xuqm.sdk.license.model.AppInfoResponse
|
||||
import com.xuqm.sdk.license.model.RegisterRequest
|
||||
import com.xuqm.sdk.license.model.RegisterResponse
|
||||
import com.xuqm.sdk.license.model.VerifyRequest
|
||||
import com.xuqm.sdk.license.model.VerifyResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface LicenseApiService {
|
||||
|
||||
@ -15,4 +18,7 @@ interface LicenseApiService {
|
||||
|
||||
@POST("api/license/verify")
|
||||
suspend fun verify(@Body request: VerifyRequest): ApiResponse<VerifyResponse>
|
||||
|
||||
@GET("api/license/app-info")
|
||||
suspend fun getAppInfo(@Query("appKey") appKey: String): ApiResponse<AppInfoResponse>
|
||||
}
|
||||
|
||||
@ -43,3 +43,12 @@ data class LicenseUserInfo(
|
||||
@SerializedName("email") val email: String? = null,
|
||||
@SerializedName("phone") val phone: String? = null,
|
||||
)
|
||||
|
||||
data class AppInfoResponse(
|
||||
@SerializedName("androidPackageName") val androidPackageName: String? = null,
|
||||
@SerializedName("iosBundleId") val iosBundleId: String? = null,
|
||||
@SerializedName("harmonyBundleName") val harmonyBundleName: String? = null,
|
||||
) {
|
||||
fun matchesPackageName(packageName: String): Boolean =
|
||||
packageName == androidPackageName || packageName == iosBundleId || packageName == harmonyBundleName
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户