feat(core): 添加请求签名功能并扩展更新接口

- 在 SDKConfig 中新增 signingKey 配置项
- 在 ConfigFile 中添加 signingKey 序列化支持
- 实现 API 客户端请求签名拦截器,支持 HMAC-SHA256 签名
- 在 XuqmSDK 中管理 signingKey 的存储和读取
- 扩展 UpdateApi 接口增加 currentVersionName 参数
- 在 UpdateSDK 中集成设备信息和版本号上报功能
- 为 RN SDK 添加签名密钥管理和 HTTP 请求签名注入
- 实现跨平台的 HMAC-SHA256 签名算法支持
这个提交包含在:
XuqmGroup 2026-06-19 01:27:56 +08:00
父节点 e3d03c0ea7
当前提交 79463a5808
共有 6 个文件被更改,包括 52 次插入0 次删除

查看文件

@ -61,6 +61,10 @@ object XuqmSDK {
@Volatile var bugCollectEnabled: Boolean = false
private set
/** 请求签名密钥,从配置文件读取。 */
@Volatile var signingKey: String? = null
private set
private val pendingInitCallbacks = mutableListOf<() -> Unit>()
/**
@ -99,6 +103,7 @@ object XuqmSDK {
"Please download the correct config file for this app."
)
}
signingKey = configFile.signingKey
initialize(context, configFile.appKey, configFile.serverUrl, logLevel)
}
@ -119,6 +124,7 @@ object XuqmSDK {
Log.w("XuqmSDK", "autoInitializeAsync: package name mismatch, skipping")
return
}
signingKey = configFile.signingKey
// 异步TokenStore / DeviceId / ApiClient / 远端配置
sdkScope.launch {
runCatching {
@ -155,6 +161,8 @@ object XuqmSDK {
return@launch
}
signingKey = configFile.signingKey
// 2. 初始化 sdk-core
initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel)

查看文件

@ -3,6 +3,7 @@ package com.xuqm.sdk.core
data class SDKConfig(
val appKey: String,
val logLevel: LogLevel = LogLevel.WARN,
val signingKey: String? = null,
)
enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE }

查看文件

@ -16,6 +16,7 @@ internal data class ConfigFile(
@SerializedName(value = "harmonyBundleName", alternate = ["harmony_bundle_name"]) val harmonyBundleName: String? = null,
@SerializedName(value = "baseUrl", alternate = ["base_url"]) val baseUrl: String? = null,
@SerializedName(value = "serverUrl", alternate = ["server_url"]) val serverUrl: String? = null,
@SerializedName(value = "signingKey", alternate = ["signing_key"]) val signingKey: String? = null,
@SerializedName(value = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null,
@SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: String? = null,
)

查看文件

@ -1,6 +1,7 @@
package com.xuqm.sdk.network
import android.util.Log
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.core.LogLevel
@ -22,7 +23,10 @@ import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object ApiClient {
@ -82,6 +86,32 @@ object ApiClient {
} else chain.request()
chain.proceed(req)
}
.addInterceptor { chain ->
val request = chain.request()
val signingKey = XuqmSDK.signingKey
if (signingKey.isNullOrBlank()) {
chain.proceed(request)
} else {
val appKey = XuqmSDK.config.appKey
val timestamp = Instant.now().epochSecond.toString()
val nonce = UUID.randomUUID().toString()
val userId = XuqmSDK.getUserId() ?: ""
val payload = "$appKey\n$userId\n$timestamp\n$nonce"
val signature = hmacSha256(signingKey, payload)
val signedRequest = request.newBuilder()
.header("X-App-Key", appKey)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", signature)
.apply {
if (userId.isNotBlank()) {
header("X-User-Id", userId)
}
}
.build()
chain.proceed(signedRequest)
}
}
.build()
synchronized(this) {
@ -89,6 +119,13 @@ object ApiClient {
}
}
private fun hmacSha256(secret: String, data: String): String {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256"))
val digest = mac.doFinal(data.toByteArray(Charsets.UTF_8))
return android.util.Base64.encodeToString(digest, android.util.Base64.URL_SAFE or android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP)
}
fun <T : Any> create(service: Class<T>, baseUrl: String = ServiceEndpointRegistry.controlBaseUrl): T {
val retrofit = synchronized(this) {
retrofitCache[baseUrl] ?: Retrofit.Builder()

查看文件

@ -185,6 +185,9 @@ object UpdateSDK {
runCatching {
val deviceInfo = com.xuqm.sdk.utils.DeviceUtils.getDeviceInfo()
val versionName = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: ""
} catch (_: Exception) { "" }
val response = api.checkUpdate(
appKey = XuqmSDK.appKey,
platform = "ANDROID",
@ -194,6 +197,7 @@ object UpdateSDK {
model = deviceInfo["model"],
osVersion = deviceInfo["osVersion"],
vendor = deviceInfo["vendor"],
currentVersionName = versionName,
)
if (response.code == 40404) {
throw IllegalStateException("更新服务未开通 (code=40404) [appKey=${XuqmSDK.appKey}]")

查看文件

@ -44,5 +44,6 @@ internal interface UpdateApi {
@Query("model") model: String? = null,
@Query("osVersion") osVersion: String? = null,
@Query("vendor") vendor: String? = null,
@Query("currentVersionName") currentVersionName: String? = null,
): ApiResponse<UpdateInfoDto>
}