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
|
@Volatile var bugCollectEnabled: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
/** 请求签名密钥,从配置文件读取。 */
|
||||||
|
@Volatile var signingKey: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
private val pendingInitCallbacks = mutableListOf<() -> Unit>()
|
private val pendingInitCallbacks = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,6 +103,7 @@ object XuqmSDK {
|
|||||||
"Please download the correct config file for this app."
|
"Please download the correct config file for this app."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
signingKey = configFile.signingKey
|
||||||
initialize(context, configFile.appKey, configFile.serverUrl, logLevel)
|
initialize(context, configFile.appKey, configFile.serverUrl, logLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,6 +124,7 @@ object XuqmSDK {
|
|||||||
Log.w("XuqmSDK", "autoInitializeAsync: package name mismatch, skipping")
|
Log.w("XuqmSDK", "autoInitializeAsync: package name mismatch, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
signingKey = configFile.signingKey
|
||||||
// 异步:TokenStore / DeviceId / ApiClient / 远端配置
|
// 异步:TokenStore / DeviceId / ApiClient / 远端配置
|
||||||
sdkScope.launch {
|
sdkScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
@ -155,6 +161,8 @@ object XuqmSDK {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingKey = configFile.signingKey
|
||||||
|
|
||||||
// 2. 初始化 sdk-core
|
// 2. 初始化 sdk-core
|
||||||
initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel)
|
initialize(ctx, configFile.appKey, configFile.serverUrl, logLevel)
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.xuqm.sdk.core
|
|||||||
data class SDKConfig(
|
data class SDKConfig(
|
||||||
val appKey: String,
|
val appKey: String,
|
||||||
val logLevel: LogLevel = LogLevel.WARN,
|
val logLevel: LogLevel = LogLevel.WARN,
|
||||||
|
val signingKey: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE }
|
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 = "harmonyBundleName", alternate = ["harmony_bundle_name"]) val harmonyBundleName: String? = null,
|
||||||
@SerializedName(value = "baseUrl", alternate = ["base_url"]) val baseUrl: String? = null,
|
@SerializedName(value = "baseUrl", alternate = ["base_url"]) val baseUrl: String? = null,
|
||||||
@SerializedName(value = "serverUrl", alternate = ["server_url"]) val serverUrl: 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 = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null,
|
||||||
@SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: String? = null,
|
@SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.xuqm.sdk.network
|
package com.xuqm.sdk.network
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.auth.TokenStore
|
import com.xuqm.sdk.auth.TokenStore
|
||||||
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||||
import com.xuqm.sdk.core.LogLevel
|
import com.xuqm.sdk.core.LogLevel
|
||||||
@ -22,7 +23,10 @@ import java.time.Instant
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
|
|
||||||
@ -82,6 +86,32 @@ object ApiClient {
|
|||||||
} else chain.request()
|
} else chain.request()
|
||||||
chain.proceed(req)
|
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()
|
.build()
|
||||||
|
|
||||||
synchronized(this) {
|
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 {
|
fun <T : Any> create(service: Class<T>, baseUrl: String = ServiceEndpointRegistry.controlBaseUrl): T {
|
||||||
val retrofit = synchronized(this) {
|
val retrofit = synchronized(this) {
|
||||||
retrofitCache[baseUrl] ?: Retrofit.Builder()
|
retrofitCache[baseUrl] ?: Retrofit.Builder()
|
||||||
|
|||||||
@ -185,6 +185,9 @@ object UpdateSDK {
|
|||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val deviceInfo = com.xuqm.sdk.utils.DeviceUtils.getDeviceInfo()
|
val deviceInfo = com.xuqm.sdk.utils.DeviceUtils.getDeviceInfo()
|
||||||
|
val versionName = try {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: ""
|
||||||
|
} catch (_: Exception) { "" }
|
||||||
val response = api.checkUpdate(
|
val response = api.checkUpdate(
|
||||||
appKey = XuqmSDK.appKey,
|
appKey = XuqmSDK.appKey,
|
||||||
platform = "ANDROID",
|
platform = "ANDROID",
|
||||||
@ -194,6 +197,7 @@ object UpdateSDK {
|
|||||||
model = deviceInfo["model"],
|
model = deviceInfo["model"],
|
||||||
osVersion = deviceInfo["osVersion"],
|
osVersion = deviceInfo["osVersion"],
|
||||||
vendor = deviceInfo["vendor"],
|
vendor = deviceInfo["vendor"],
|
||||||
|
currentVersionName = versionName,
|
||||||
)
|
)
|
||||||
if (response.code == 40404) {
|
if (response.code == 40404) {
|
||||||
throw IllegalStateException("更新服务未开通 (code=40404) [appKey=${XuqmSDK.appKey}]")
|
throw IllegalStateException("更新服务未开通 (code=40404) [appKey=${XuqmSDK.appKey}]")
|
||||||
|
|||||||
@ -44,5 +44,6 @@ internal interface UpdateApi {
|
|||||||
@Query("model") model: String? = null,
|
@Query("model") model: String? = null,
|
||||||
@Query("osVersion") osVersion: String? = null,
|
@Query("osVersion") osVersion: String? = null,
|
||||||
@Query("vendor") vendor: String? = null,
|
@Query("vendor") vendor: String? = null,
|
||||||
|
@Query("currentVersionName") currentVersionName: String? = null,
|
||||||
): ApiResponse<UpdateInfoDto>
|
): ApiResponse<UpdateInfoDto>
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户