feat(core): 添加请求签名功能并扩展更新接口
- 在 SDKConfig 中新增 signingKey 配置项 - 在 ConfigFile 中添加 signingKey 序列化支持 - 实现 API 客户端请求签名拦截器,支持 HMAC-SHA256 签名 - 在 XuqmSDK 中管理 signingKey 的存储和读取 - 扩展 UpdateApi 接口增加 currentVersionName 参数 - 在 UpdateSDK 中集成设备信息和版本号上报功能 - 为 RN SDK 添加签名密钥管理和 HTTP 请求签名注入 - 实现跨平台的 HMAC-SHA256 签名算法支持
这个提交包含在:
父节点
e3d03c0ea7
当前提交
79463a5808
@ -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>
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户