feat(core): 添加请求签名功能并扩展更新接口
- 在 SDKConfig 中新增 signingKey 配置项 - 在 ConfigFile 中添加 signingKey 序列化支持 - 实现 API 客户端请求签名拦截器,支持 HMAC-SHA256 签名 - 在 XuqmSDK 中管理 signingKey 的存储和读取 - 扩展 UpdateApi 接口增加 currentVersionName 参数 - 在 UpdateSDK 中集成设备信息和版本号上报功能 - 为 RN SDK 添加签名密钥管理和 HTTP 请求签名注入 - 实现跨平台的 HMAC-SHA256 签名算法支持
这个提交包含在:
父节点
71eeada147
当前提交
0cf264cdf6
@ -18,6 +18,8 @@ export interface XuqmConfig {
|
||||
// 崩溃采集服务(rn-bugcollect 使用)
|
||||
bugCollectApiUrl: string
|
||||
bugCollectEnabled: boolean
|
||||
// 请求签名密钥(从配置文件读取)
|
||||
signingKey?: string
|
||||
}
|
||||
|
||||
export interface XuqmUserInfo {
|
||||
@ -54,6 +56,7 @@ export interface XuqmRemoteConfig {
|
||||
export function initConfigFromRemote(
|
||||
options: XuqmInitOptions,
|
||||
remote: XuqmRemoteConfig,
|
||||
signingKey?: string,
|
||||
): void {
|
||||
const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? ''
|
||||
_config = {
|
||||
@ -68,6 +71,7 @@ export function initConfigFromRemote(
|
||||
licenseEnabled: remote.licenseEnabled ?? false,
|
||||
bugCollectApiUrl: remote.bugCollectApiUrl ?? '',
|
||||
bugCollectEnabled: remote.bugCollectEnabled ?? false,
|
||||
signingKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ export interface DecryptedConfig {
|
||||
companyName?: string
|
||||
baseUrl?: string
|
||||
serverUrl?: string
|
||||
signingKey?: string
|
||||
issuedAt?: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { DEFAULT_TENANT_PLATFORM_URL } from './constants'
|
||||
import { getConfig } from './config'
|
||||
import { getSigningKey } from './sdk'
|
||||
|
||||
const TOKEN_KEY = '@xuqm:token'
|
||||
let _baseUrl = DEFAULT_TENANT_PLATFORM_URL
|
||||
@ -26,6 +28,52 @@ export async function _clearToken(): Promise<void> {
|
||||
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>(
|
||||
path: string,
|
||||
options: {
|
||||
@ -53,6 +101,29 @@ export async function apiRequest<T>(
|
||||
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) {
|
||||
console.debug('[XuqmSDK][HTTP] request', {
|
||||
method: options.method ?? 'GET',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// 自动初始化(对齐 Android ContentProvider 模式)
|
||||
import './autoInit'
|
||||
|
||||
export { XuqmSDK } from './sdk'
|
||||
export { XuqmSDK, getSigningKey } from './sdk'
|
||||
export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config'
|
||||
export {
|
||||
getConfig,
|
||||
|
||||
@ -6,6 +6,7 @@ import { decryptConfigFile } from './configCrypto'
|
||||
let _initPromise: Promise<void> | null = null
|
||||
let _initResolve: (() => void) | null = null
|
||||
let _initReject: ((e: unknown) => void) | null = null
|
||||
let _signingKey: string | undefined = undefined
|
||||
|
||||
function ensureInitPromise(): Promise<void> {
|
||||
if (!_initPromise) {
|
||||
@ -65,7 +66,7 @@ export const XuqmSDK = {
|
||||
licenseEnabled: remote.licenseEnabled as boolean | undefined,
|
||||
bugCollectApiUrl: remote.bugCollectApiUrl as string | undefined,
|
||||
bugCollectEnabled: remote.bugCollectEnabled as boolean | undefined,
|
||||
})
|
||||
}, _signingKey)
|
||||
configureHttp({
|
||||
baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl,
|
||||
debug: options.debug,
|
||||
@ -90,6 +91,10 @@ export const XuqmSDK = {
|
||||
const file = await decryptConfigFile(encryptedContent)
|
||||
// 配置文件解密后包含 appKey 和 platformUrl(字段名 serverUrl 或 baseUrl)
|
||||
const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined
|
||||
// 保存 signingKey 供后续请求签名使用
|
||||
if (file.signingKey) {
|
||||
_signingKey = file.signingKey
|
||||
}
|
||||
await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug })
|
||||
},
|
||||
|
||||
@ -122,3 +127,7 @@ export async function awaitInitialization(): Promise<void> {
|
||||
if (isInitialized()) return
|
||||
await ensureInitPromise()
|
||||
}
|
||||
|
||||
export function getSigningKey(): string | undefined {
|
||||
return _signingKey
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
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 { md5 } from './md5'
|
||||
|
||||
@ -228,10 +228,16 @@ export const UpdateSDK = {
|
||||
}
|
||||
|
||||
const config = getConfig()
|
||||
const deviceInfo = await getDeviceInfo()
|
||||
const params: Record<string, string> = {
|
||||
appKey: config.appKey,
|
||||
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
|
||||
currentVersionCode: String(getAppVersionCode()),
|
||||
currentVersionName: getAppVersionName() ?? '',
|
||||
deviceId: deviceInfo.deviceId,
|
||||
model: deviceInfo.model,
|
||||
osVersion: deviceInfo.osVersion,
|
||||
vendor: deviceInfo.pushVendor,
|
||||
}
|
||||
const userId = getUserId()?.trim()
|
||||
if (userId) params.userId = userId
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户