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
父节点 71eeada147
当前提交 0cf264cdf6
共有 6 个文件被更改,包括 94 次插入3 次删除

查看文件

@ -18,6 +18,8 @@ export interface XuqmConfig {
// 崩溃采集服务rn-bugcollect 使用) // 崩溃采集服务rn-bugcollect 使用)
bugCollectApiUrl: string bugCollectApiUrl: string
bugCollectEnabled: boolean bugCollectEnabled: boolean
// 请求签名密钥(从配置文件读取)
signingKey?: string
} }
export interface XuqmUserInfo { export interface XuqmUserInfo {
@ -54,6 +56,7 @@ export interface XuqmRemoteConfig {
export function initConfigFromRemote( export function initConfigFromRemote(
options: XuqmInitOptions, options: XuqmInitOptions,
remote: XuqmRemoteConfig, remote: XuqmRemoteConfig,
signingKey?: string,
): void { ): void {
const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? '' const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? ''
_config = { _config = {
@ -68,6 +71,7 @@ export function initConfigFromRemote(
licenseEnabled: remote.licenseEnabled ?? false, licenseEnabled: remote.licenseEnabled ?? false,
bugCollectApiUrl: remote.bugCollectApiUrl ?? '', bugCollectApiUrl: remote.bugCollectApiUrl ?? '',
bugCollectEnabled: remote.bugCollectEnabled ?? false, bugCollectEnabled: remote.bugCollectEnabled ?? false,
signingKey,
} }
} }

查看文件

@ -24,6 +24,7 @@ export interface DecryptedConfig {
companyName?: string companyName?: string
baseUrl?: string baseUrl?: string
serverUrl?: string serverUrl?: string
signingKey?: string
issuedAt?: string issuedAt?: string
expiresAt?: string expiresAt?: string
} }

查看文件

@ -1,5 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { DEFAULT_TENANT_PLATFORM_URL } from './constants' import { DEFAULT_TENANT_PLATFORM_URL } from './constants'
import { getConfig } from './config'
import { getSigningKey } from './sdk'
const TOKEN_KEY = '@xuqm:token' const TOKEN_KEY = '@xuqm:token'
let _baseUrl = DEFAULT_TENANT_PLATFORM_URL let _baseUrl = DEFAULT_TENANT_PLATFORM_URL
@ -26,6 +28,52 @@ export async function _clearToken(): Promise<void> {
return AsyncStorage.removeItem(TOKEN_KEY) return AsyncStorage.removeItem(TOKEN_KEY)
} }
function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
}
async function hmacSha256(secret: string, data: string): Promise<string> {
// 使用 react-native-quick-crypto 或降级到纯 JS 实现
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const qc = require('react-native-quick-crypto') as { default?: { createHmac?: Function }; createHmac?: Function }
const createHmac = qc.createHmac ?? qc.default?.createHmac
if (createHmac) {
const hmac = createHmac('sha256', secret)
hmac.update(data)
return hmac.digest('base64url')
}
} catch {
// 降级到纯 JS 实现
}
// 纯 JS HMAC-SHA256 实现(用于没有 react-native-quick-crypto 的环境)
// 注意:这是一个简化实现,生产环境建议使用 react-native-quick-crypto
const encoder = new TextEncoder()
const keyData = encoder.encode(secret)
const dataBytes = encoder.encode(data)
// 使用 SubtleCrypto API如果可用
if (typeof globalThis.crypto?.subtle !== 'undefined') {
const key = await globalThis.crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await globalThis.crypto.subtle.sign('HMAC', key, dataBytes)
return btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
throw new Error('[XuqmSDK] No HMAC-SHA256 implementation available')
}
export async function apiRequest<T>( export async function apiRequest<T>(
path: string, path: string,
options: { options: {
@ -53,6 +101,29 @@ export async function apiRequest<T>(
if (token) headers['Authorization'] = `Bearer ${token}` if (token) headers['Authorization'] = `Bearer ${token}`
} }
// 注入签名头
try {
const signingKey = getSigningKey()
if (signingKey) {
const config = getConfig()
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = generateUUID()
const userId = config.appKey // 使用 appKey 作为 userId 的默认值
const payload = `${config.appKey}\n${userId}\n${timestamp}\n${nonce}`
const signature = await hmacSha256(signingKey, payload)
headers['X-App-Key'] = config.appKey
headers['X-Timestamp'] = timestamp
headers['X-Nonce'] = nonce
headers['X-Signature'] = signature
headers['X-User-Id'] = userId
}
} catch (e) {
// 签名失败不应阻止请求
if (_debugHttp) {
console.debug('[XuqmSDK][HTTP] signature injection failed:', e)
}
}
if (_debugHttp) { if (_debugHttp) {
console.debug('[XuqmSDK][HTTP] request', { console.debug('[XuqmSDK][HTTP] request', {
method: options.method ?? 'GET', method: options.method ?? 'GET',

查看文件

@ -1,7 +1,7 @@
// 自动初始化(对齐 Android ContentProvider 模式) // 自动初始化(对齐 Android ContentProvider 模式)
import './autoInit' import './autoInit'
export { XuqmSDK } from './sdk' export { XuqmSDK, getSigningKey } from './sdk'
export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config' export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config'
export { export {
getConfig, getConfig,

查看文件

@ -6,6 +6,7 @@ import { decryptConfigFile } from './configCrypto'
let _initPromise: Promise<void> | null = null let _initPromise: Promise<void> | null = null
let _initResolve: (() => void) | null = null let _initResolve: (() => void) | null = null
let _initReject: ((e: unknown) => void) | null = null let _initReject: ((e: unknown) => void) | null = null
let _signingKey: string | undefined = undefined
function ensureInitPromise(): Promise<void> { function ensureInitPromise(): Promise<void> {
if (!_initPromise) { if (!_initPromise) {
@ -65,7 +66,7 @@ export const XuqmSDK = {
licenseEnabled: remote.licenseEnabled as boolean | undefined, licenseEnabled: remote.licenseEnabled as boolean | undefined,
bugCollectApiUrl: remote.bugCollectApiUrl as string | undefined, bugCollectApiUrl: remote.bugCollectApiUrl as string | undefined,
bugCollectEnabled: remote.bugCollectEnabled as boolean | undefined, bugCollectEnabled: remote.bugCollectEnabled as boolean | undefined,
}) }, _signingKey)
configureHttp({ configureHttp({
baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl, baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl,
debug: options.debug, debug: options.debug,
@ -90,6 +91,10 @@ export const XuqmSDK = {
const file = await decryptConfigFile(encryptedContent) const file = await decryptConfigFile(encryptedContent)
// 配置文件解密后包含 appKey 和 platformUrl字段名 serverUrl 或 baseUrl // 配置文件解密后包含 appKey 和 platformUrl字段名 serverUrl 或 baseUrl
const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined
// 保存 signingKey 供后续请求签名使用
if (file.signingKey) {
_signingKey = file.signingKey
}
await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug }) await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug })
}, },
@ -122,3 +127,7 @@ export async function awaitInitialization(): Promise<void> {
if (isInitialized()) return if (isInitialized()) return
await ensureInitPromise() await ensureInitPromise()
} }
export function getSigningKey(): string | undefined {
return _signingKey
}

查看文件

@ -1,6 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { Linking, Platform } from 'react-native' import { Linking, Platform } from 'react-native'
import { apiRequest, getConfig, getUserId, _registerUserInfoHandler } from '@xuqm/rn-common' import { apiRequest, getConfig, getUserId, getDeviceInfo, _registerUserInfoHandler } from '@xuqm/rn-common'
import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion'
import { md5 } from './md5' import { md5 } from './md5'
@ -228,10 +228,16 @@ export const UpdateSDK = {
} }
const config = getConfig() const config = getConfig()
const deviceInfo = await getDeviceInfo()
const params: Record<string, string> = { const params: Record<string, string> = {
appKey: config.appKey, appKey: config.appKey,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersionCode: String(getAppVersionCode()), currentVersionCode: String(getAppVersionCode()),
currentVersionName: getAppVersionName() ?? '',
deviceId: deviceInfo.deviceId,
model: deviceInfo.model,
osVersion: deviceInfo.osVersion,
vendor: deviceInfo.pushVendor,
} }
const userId = getUserId()?.trim() const userId = getUserId()?.trim()
if (userId) params.userId = userId if (userId) params.userId = userId