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 baseUrl: String = "https://auth.dev.xuqinmin.com/",
|
||||||
val deviceName: String? = null,
|
val deviceName: String? = null,
|
||||||
val cacheWindowMs: Long = 10 * 60 * 1000L, // 10 minutes
|
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:
|
* Check license status. This will:
|
||||||
* 1. Return cached OK status if within cache window
|
* 1. Return cached OK status if within cache window
|
||||||
* 2. Try to verify existing token
|
* 2. Validate package name (locally for license-file flow; via server for appKey flow)
|
||||||
* 3. Register new device if no token exists
|
* 3. Try to verify existing token
|
||||||
|
* 4. Register new device if no token exists
|
||||||
*
|
*
|
||||||
* @return LicenseResult.Success if license is valid, LicenseResult.Error otherwise
|
* @return LicenseResult.Success if license is valid, LicenseResult.Error otherwise
|
||||||
*/
|
*/
|
||||||
@ -100,6 +101,26 @@ object LicenseSDK {
|
|||||||
return@withContext LicenseResult.Success("Cached")
|
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 deviceId = getOrCreateDeviceId()
|
||||||
val token = store.token
|
val token = store.token
|
||||||
|
|
||||||
@ -251,6 +272,13 @@ object LicenseSDK {
|
|||||||
val ctx = LicenseContextHolder.appContext ?: return false
|
val ctx = LicenseContextHolder.appContext ?: return false
|
||||||
val licenseFile = LicenseFileReader.read(ctx) ?: return false
|
val licenseFile = LicenseFileReader.read(ctx) ?: return false
|
||||||
val appKey = licenseFile.appKey.takeIf { it.isNotBlank() } ?: 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
|
appContext = ctx
|
||||||
store = LicenseStore(ctx)
|
store = LicenseStore(ctx)
|
||||||
if (store.appKey != null && store.appKey != appKey) {
|
if (store.appKey != null && store.appKey != appKey) {
|
||||||
@ -261,6 +289,7 @@ object LicenseSDK {
|
|||||||
appKey = appKey,
|
appKey = appKey,
|
||||||
baseUrl = normalizeBaseUrl(licenseFile.baseUrl ?: DEFAULT_BASE_URL),
|
baseUrl = normalizeBaseUrl(licenseFile.baseUrl ?: DEFAULT_BASE_URL),
|
||||||
deviceName = DeviceInfoProvider.getDeviceName(),
|
deviceName = DeviceInfoProvider.getDeviceName(),
|
||||||
|
fromLicenseFile = true,
|
||||||
)
|
)
|
||||||
apiService = LicenseHttpClient.create(LicenseApiService::class.java, config.baseUrl)
|
apiService = LicenseHttpClient.create(LicenseApiService::class.java, config.baseUrl)
|
||||||
initialized = true
|
initialized = true
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
package com.xuqm.sdk.license.api
|
package com.xuqm.sdk.license.api
|
||||||
|
|
||||||
import com.xuqm.sdk.license.model.ApiResponse
|
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.RegisterRequest
|
||||||
import com.xuqm.sdk.license.model.RegisterResponse
|
import com.xuqm.sdk.license.model.RegisterResponse
|
||||||
import com.xuqm.sdk.license.model.VerifyRequest
|
import com.xuqm.sdk.license.model.VerifyRequest
|
||||||
import com.xuqm.sdk.license.model.VerifyResponse
|
import com.xuqm.sdk.license.model.VerifyResponse
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface LicenseApiService {
|
interface LicenseApiService {
|
||||||
|
|
||||||
@ -15,4 +18,7 @@ interface LicenseApiService {
|
|||||||
|
|
||||||
@POST("api/license/verify")
|
@POST("api/license/verify")
|
||||||
suspend fun verify(@Body request: VerifyRequest): ApiResponse<VerifyResponse>
|
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("email") val email: String? = null,
|
||||||
@SerializedName("phone") val phone: 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
|
||||||
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户