基于HarmonyOS Next的营销防薅羊毛设计:Device Certificate Kit的活动防护策略

159 阅读14分钟

本文旨在深入探讨华为鸿蒙HarmonyOS Next系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。

在电商和营销活动日益频繁的今天,黑灰产利用各种手段薅羊毛的现象愈发猖獗,给企业带来了巨大的经济损失。基于华为鸿蒙HarmonyOS Next系统,我们可以借助Device Certificate Kit等强大工具构建一套有效的活动防护策略,确保营销活动的公平性和安全性。

一、场景描述

在电商平台的促销活动(如限时折扣、满减活动、抽奖等)以及各类营销推广活动中,黑灰产常常通过使用模拟器或非合法应用批量注册账号、自动参与活动,以获取不正当利益。例如,在限时抢购活动中,他们利用自动化脚本在模拟器上快速下单,抢购大量优惠商品,导致真正的用户无法享受到活动福利;在抽奖活动中,通过伪造请求或重放合法请求来增加中奖概率。因此,我们需要一种可靠的机制来识别并阻止这些非法行为,保障营销活动的正常进行。

二、架构设计

为了实现营销活动的安全防护,我们利用Device Certificate Kit和随机数生成机制,构建了一套多层验证的架构。

  1. Device Certificate Kit
  • 负责生成和管理应用的证书链,其中包含应用的公钥等关键信息。在应用安装时,为应用生成唯一的证书,并将证书相关信息安全存储。

  • 提供证书验证功能,用于在服务端验证请求设备或应用的证书链的有效性和真实性,确保请求来自合法的应用和设备。

  1. 随机数生成机制
  • 在服务端生成一次性的随机挑战值(challenge),并将其发送给应用端。这个挑战值在每次活动请求中都是唯一的,用于增加请求的随机性和不可预测性,防止重放攻击。

  • 应用端将挑战值与业务请求数据结合,使用证书中的私钥进行签名,然后将签名后的请求发送回服务端。服务端通过验证签名和挑战值的一致性,确保请求在传输过程中未被篡改。

三、实现步骤

  1. 为应用生成随机挑战值,用于验证活动请求的真实性
  • 服务端在收到活动请求前,使用安全的随机数生成算法(例如,基于Crypto Architecture Kit中的随机数生成功能)生成一个随机挑战值。示例代码如下:

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

  


async function generateChallenge() {

let randomData = await cryptoFramework.generateRandomBytes(16);

return randomData.data;

}

  


let challenge = await generateChallenge();

console.log('Generated challenge:', challenge);

  • 服务端将挑战值发送给应用端,可以通过安全的通信协议(如HTTPS)进行传输,确保挑战值在传输过程中不被窃取或篡改。
  1. 应用端使用证书和私钥生成签名,确保请求未被篡改
  • 应用端接收到挑战值后,获取存储在设备中的证书私钥(通过Universal Keystore Kit)。示例代码如下:

import { huks } from '@kit.UniversalKeystoreKit';

import { BusinessError } from '@kit.BasicServicesKit';

  


let keyAlias = 'your_app_key_alias';

  


async function getPrivateKey() {

let options: huks.HuksOptions = {

properties: []

}

try {

let key = await huks.getKeyItem(keyAlias, options);

return key;

} catch (err) {

let e: BusinessError = err as BusinessError;

console.error(`getPrivateKey failed, error: ` + err.message);

}

}

  


let privateKey = await getPrivateKey();

  • 然后,应用端将挑战值与业务请求数据(如用户参与活动的信息、时间戳等)组合在一起,使用私钥对组合后的数据进行签名。假设业务请求数据为activityRequestData

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

import { util } from '@kit.ArkTS';

  


let combinedData = new Uint8Array([...challenge,...new Uint8Array(textEncoder.encodeInto(activityRequestData).data)]);

  


async function signRequestData(data: Uint8Array) {

let signature = await cryptoFramework.sign({

key: privateKey,

data: data

});

console.log('Signature:', signature.data);

return signature;

}

  


let signature = await signRequestData(combinedData);

  • 最后,应用端将签名、挑战值(或其哈希值,用于减少数据传输量)和业务请求数据一起发送给服务端。
  1. 服务端对请求进行挑战值和证书链的多层验证
  • 服务端首先验证证书链的有效性,确保请求来自合法的应用和设备。使用Device Certificate Kit的相关API进行证书链验证,示例代码如下:

import { cert } from '@kit.DeviceCertificateKit';

import { BusinessError } from '@kit.BasicServicesKit';

import { util } from '@kit.ArkTS';

  


// 假设这是设备证书链数据(实际应用中需从请求中获取)

let deviceCertChainData = "-----BEGIN CERTIFICATE-----\n" +

"MIID6jCCAtKgAwIBAgIIIM2q/TmRoLcwDQYJKoZIhvcNAQELBQAwWjELMAkGA1\n" +

"UEBhMCRU4xEDAOBgNVBAgTB0VuZ2xhbmQxDzANBgNVBAcTBkxvbmRvbjEMMA\n" +

"oGA1UEChMDdHMyMQwwCgYDVQQLEwN0czIxDDAKBgNVBAMTA3RzMjAeFw0yMzEy\n" +

"MDUwNzM5MDBaFw0yNDEwMzEyMzU5MDBaMGExCzAJBgNVBAYTAkNOMRAwDgYDVQQI\n" +

"EwdKaWFuZ3N1MRAwDgYDVQQHEwdOYW5qaW5nMQwwCgYDVQQKEwN0czMxDDAKBg\n" +

"NVBAsTA3RzMzESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" +

"MIIBCgKCAQEAtt+2QxUevbolYLp51QGcUpageI4fwGLIqv4fj4aoVnHFOOBqVOVpfCLR\n" +

"p26LFV/F8ebwPyo8YEBKSwXzMD1573rMSbaH9BalscH5lZYAbetXoio6YRvzlcmc\n" +

"rVvLBNMeVnxY86xHpo0MTNyP7W024rZsxWO98xFQVdoiaBC+7+midlisx2Y+7u0\n" +

"zT9GjeUP6JLdLFUZJKUPSTK3jVzw9v1eZQZKYoNfU6vFMd6ndtwW6qEnwpzmmX\n" +

"/UT+p5ThAMH593zszlz330nTSXBjIsGkyvOz9gSB0Z0LAuJj06XUNhGL5xKJYKbdI3\n" +

"8MFQFJKvRHfgTAvVsvAvpBUM2DuBKwIDAQABo4GsMIGpMAkGA1UdEwQCMAA\n" +

"wHQYDVR0OBBYEFDfsHTMZwoA6eaDFlBUyDpka+sYtMAsGA1UdDwQEAwID+DAnBgN\n" +

"VHSUEIDAeBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMEMBQGA1UdEQQNM\n" +

"AuCCTEyNy4wLjAuMTARBglghkgBhvhCAQEEBAMCBkAwHgYJYIZIAYb4QgENBBEWD3hj\n" +

"YSBjZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEAp5vTvXrt8ZpgRJVtzv9ss0lJ\n" +

"izp1fJf+ft5cDXrs7TSD5oHrSW2vk/ZieIMhexU4LFwhs4OE7jK6pgI48Dseqxx7\n" +

"B/KktxhVMJUmVXd9Ayjp6f+BtZlIk0cArPuoXToXjsV8caTGBXHRdzxpAk/w9syc\n" +

"GYrbH9TrdNMuTizOb+k268oKXUageZNxHmd7YvOXkcNgrd29jzwXKDYYiUa1DI\n" +

"SzDnYaJOgPt0B/5izhoWNK7GhJDy9KEuLURcTSWFysbbnljwO9INPT9MmlS83PdAg\n" +

"NiS8VXF4pce1W9U5jH7d7k0JDVSXybebe1iPFphsZpYM/NE+jap+mPy1nTCbf9g==\n" +

"-----END CERTIFICATE-----\n" +

"-----BEGIN CERTIFICATE-----\n" +

"MIIC0zCCAoWgAwIBAgIIXpLoPpQVWnkwBQYDK2VwMFoxCzAJBgNVBAYTAkV\n" +

"OMRAwDgYDVQQIEwdFbmdsYW5kMQ8wDQYDVQQHEwZMb25kb24xDDAKBgNVBAoT\n" +

"A3RzMTEMMAoGA1UECxMDdHMxMQwwCgYDVQQDEwN0czEwHhcNMjMxMjA1MDczNzA\n" +

"wWhcNMjQwOTAxMjM1OTAwWjBaMQswCQYDVQQGEwJFTjEQMA4GA1UECBMHRW5nbGFu\n" +

"ZDEPMA0GA1UEBxMGTG9uZG9uMQwwCgYDVQQKEwN0czIxDDAKBgNVBAsTA3RzMjEMM\n" +

"AoGA1UEAxMDdHMyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" +

"MIIBCgKCAQEAtt+2QxUevbolYLp51QGcUpageI4fwGLIqv4fj4aoVnHFOOBqVOVpfCLRp26LFV/F8ebwPyo8YEBK\n" +

"SwXzMD1573rMSbaH9BalscH5lZYAbetXoio6YRvzlcmcrVvLBNMeVnxY86xHpo0\n" +

"MNTyP7W024rZsxWO98xFQVdoiaBC+7+midlisx2Y+7u0jzT9GjeUP6JLdLFUZJKUP\n" +

"STK3jVzw9v1eZQZKYoNfU6vFMd6ndtwW6qEnwpzmmX/UT+p5ThAMH593zszlz\n" +

"330nTSXBjIsGkyvOz9gSB0Z0LAuJj06XUNhGL5xKJYKbdI38MFQFJKvRHfgTAvVsvAv\n" +

"pBUM2DuBKwIDAQABo28wbTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBQ3\n" +

"7B0zGcKAOnmgxZQVMg6ZGvrGLTALBgNVHQ8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAg\n" +

"AHMB4GCGCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwBQYDK2VwA0EAuasLBe\n" +

"55YgvFb4wmHeohylc9r8cFGS1LNQ5UcSn3cGqMYf6ehnef16NLuCW6upHCs8Sui4iAMvs\n" +

"uKPWR9dKBA==\n" +

"-----END CERTIFICATE-----\n";

  


let textEncoder = new util.TextEncoder();

let encodingBlob: cert.EncodingBlob = {

data: textEncoder.encodeInto(deviceCertChainData),

encodingFormat: cert.EncodingFormat.FORMAT_PEM

};

  


let x509CertChain: cert.X509CertChain = {} as cert.X509CertChain;

  


try {

x509CertChain = await cert.createX509CertChain(encodingBlob);

} catch (err) {

let e: BusinessError = err as BusinessError;

console.error(`createX509CertChain failed, errCode: ${e.code}, errMsg: ${e.message}`);

}

  


// 证书链校验数据(假设,实际需根据真实情况配置)

const param: cert.CertChainValidationParameters = {

date: '20231212080000Z',

trustAnchors: [{

CAPubKey: new Uint8Array([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70,

0x03, 0x21, 0x00, 0xbb, 0x16, 0x9d, 0x8f, 0x5c, 0x30, 0xd0, 0xba, 0x8f, 0x37, 0x6e,

0x33, 0xaf, 0x6f, 0x23, 0x71, 0x23, 0xa5, 0x49, 0x60, 0x1e, 0xd1, 0x07, 0x4b, 0xc9,

0x11, 0x7e, 0x66, 0x01, 0xba, 0x92, 0x52]),

CASubject: new Uint8Array([0x30, 0x5a, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55,

0x04, 0x06, 0x13, 0x02, 0x45, 0x4e, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04,

0x08, 0x13, 0x07, 0x45, 0x6e, 0x67, 0x6c, 0x61, 0x6e, 0x64, 0x31, 0x0f, 0x30, 0x0d,

0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x06, 0x4c, 0x6f, 0x6e, 0x64, 0x6f, 0x6e, 0x31,

0x0c, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x03, 0x74, 0x73, 0x31, 0x31,

0x0c, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x03, 0x74, 0x73, 0x31, 0x31,

0x0c, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x03, 0x74, 0x73, 0x31])

}]

};

  


try {

const validationRes = await x509CertChain.validate(param);

if (validationRes) {

console.log('X509CertChain validate success');

} catch (err) {

console.error('X509CertChain validate failed');

}

  • 接着,服务端验证签名的有效性。从证书中获取公钥,使用公钥对签名进行验证,确保请求数据在传输过程中未被篡改。示例代码如下:

import { cert } from '@kit.DeviceCertificateKit';

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

import { BusinessError } from '@kit.BasicServicesKit';

import { util } from '@kit.ArkTS';

  


// 假设这是从请求中获取的签名和挑战值(或其哈希值)以及业务请求数据

let signature = new Uint8Array([...]);

let challengeReceived = new Uint8Array([...]);

let activityRequestDataReceived = '...';

  


// 从证书链中获取证书(假设已经验证证书链有效)

let deviceCert = x509CertChain.getCertList()[0];

  


// 获取证书中的公钥

let pubKey = deviceCert.getPublicKey();

  


// 重新组合数据(根据之前签名时的组合方式)

let combinedDataForVerification = new Uint8Array([...challengeReceived,...new Uint8Array(textEncoder.encodeInto(activityRequestDataReceived).data)]);

  


try {

let result = await cryptoFramework.verify({

key: pubKey,

data: combinedDataForVerification,

signature: signature

});

if (result) {

console.log('Signature verification succeeded.');

} else {

console.log('Signature verification failed.');

}

} catch (error) {

let e: BusinessError = error as BusinessError;

console.error(`verifySignature failed, errCode: ${e.code}, errMsg:${e.message}`);

}

  • 最后,服务端验证挑战值的一致性。将收到的挑战值与之前生成并发送给应用端的挑战值进行比对,如果一致,则说明请求是新鲜的,不是重放攻击。如果挑战值不一致或者签名验证失败,服务端则拒绝该活动请求。

四、技术亮点

  1. 如何在业务请求中利用挑战值和签名机制防止攻击
  • 挑战值的随机性和一次性:每次生成的挑战值都是随机且唯一的,这使得攻击者难以预测和伪造。即使攻击者获取了之前的请求信息,由于挑战值的变化,他们无法使用旧的签名来通过验证。例如,在限时抢购活动中,不同用户在不同时间的请求将具有不同的挑战值,有效防止了批量自动化脚本的攻击。

  • 签名确保数据完整性:应用端使用私钥对包含挑战值和业务请求数据的组合进行签名,服务端通过公钥验证签名。这确保了数据在传输过程中未被篡改,因为任何对数据的修改都会导致签名验证失败。例如,在抽奖活动中,防止攻击者修改抽奖请求中的用户信息或抽奖次数等关键数据。

  1. 活动场景中防重放、伪造请求的实现与设计
  • 防重放攻击:通过挑战值的一次性和时间戳(可以与挑战值一起包含在请求中)的结合,服务端可以轻松识别重放攻击。如果收到的挑战值在之前已经被使用过,或者请求的时间戳与当前时间相差过大(超出合理范围),则认为是重放攻击并拒绝请求。例如,在一些需要频繁交互的营销活动中,如签到领积分活动,防止攻击者利用截获的合法请求进行多次签到。

  • 防止伪造请求:证书链验证和签名机制共同防止了伪造请求。只有拥有合法证书的应用才能生成有效的签名,而证书链的验证确保了证书的真实性和有效性。即使攻击者试图伪造请求,由于无法获取正确的私钥进行签名,且无法伪造有效的证书链,其伪造的请求在服务端验证时必然会失败。

五、示例代码

  1. 生成和验证挑战值核心代码
  • 服务端生成挑战值(上述已部分展示,补充完整错误处理):

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

  


async function generateChallenge() {

try {

let randomData = await cryptoFramework.generateRandomBytes(16);

return randomData.data;

} catch (error) {

console.error('Failed to generate challenge:', error);

}

}

  


let challenge = await generateChallenge();

console.log('Generated challenge:', challenge);

  • 服务端验证挑战值(假设在收到请求时进行验证):

function verifyChallenge(challengeReceived: Uint8Array, originalChallenge: Uint8Array) {

if (challengeReceived.every((value, index) => value === originalChallenge[index])) {

console.log('Challenge verification succeeded.');

return true;

} else {

console.log('Challenge verification failed.');

return false;

}

}

  1. 签名和证书链校验核心代码
  • 应用端签名生成(上述已部分展示,补充完整数据处理和错误处理):

import { huks } from '@kit.UniversalKeystoreKit';

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

import { util } from '@kit.ArkTS';

import { BusinessError } from '@kit.BasicServicesKit';

  


let keyAlias = 'your_app_key_alias';

  


async function getPrivateKey() {

let options: huks.HuksOptions = {

properties: []

}

try {

let key = await huks.getKeyItem(keyAlias, options);

return key;

} catch (err) {

let e: BusinessError = err as BusinessError;

console.error(`getPrivateKey failed, error: ` + err.message);

}

}

  


let privateKey = await getPrivateKey();

  


let combinedData = new Uint8Array([...challenge,...new Uint8Array(textEncoder.encodeInto(activityRequestData).data)]);

  


async function signRequestData(data: Uint8Array) {

try {

let signature = await cryptoFramework.sign({

key: privateKey,

data: data

});

console.log('Signature:', signature.data);

return signature;

} catch (error) {

let e: BusinessError = error as BusinessError;

console.error(`signRequestData failed, errCode: ${e.code}, errMsg:${e.message}`);

}

}

  


let signature = await signRequestData(combinedData);

  • 服务端签名验证和证书链校验(上述已部分展示,优化错误处理和流程整合):

import { cert } from '@kit.DeviceCertificateKit';

import { cryptoFramework } from '@kit.CryptoArchitectureKit';

import { BusinessError } from '@kit.BasicServicesKit';

import { util } from '@kit.ArkTS';

  


// 假设这是从请求中获取的签名和挑战值(或其哈希值)以及业务请求数据

let signature = new Uint8Array([...]);

let challengeReceived = new Uint8Array([...]);

let activityRequestDataReceived = '...';

  


// 从证书链中获取证书(假设已经验证证书链有效)

let deviceCert = x509CertChain.getCertList()[0];

  


// 获取证书中的公钥

let pubKey = deviceCert.getPublicKey();

  


// 重新组合数据(根据之前签名时的组合方式)

let combinedDataForVerification = new Uint8Array([...challengeReceived,...new Uint8Array(textEncoder.encodeInto(activityRequestDataReceived).data)]);

  


async function verifySignatureAndCertChain(signature: Uint8Array, combinedData: Uint8Array, deviceCert: cert.X509Cert) {

try {

// 验证签名

let result = await cryptoFramework.verify({

key: pubKey,

data: combinedData,

signature: signature

});

if (result) {

console.log('Signature verification succeeded.');

} else {

console.log('Signature verification failed.');

return false;

}

  


// 验证证书链(假设已经获取并验证了证书链,这里可以添加更详细的证书链验证逻辑)

//...

  


return true;

} catch (error) {

let e: BusinessError = error as BusinessError;

console.error(`verifySignatureAndCertChain failed, errCode: ${e.code}, errMsg:${e.message}`);

return false;

}

}

六、总结

基于HarmonyOS Next的营销防薅羊毛活动防护架构主要通过Device Certificate Kit和随机数生成机制实现了对活动请求的多层验证。在整个流程中,从服务端生成挑战值开始,到应用端使用证书和私钥生成签名,再到服务端对请求进行挑战值、签名和证书链的全面验证,每个环节都紧密协作,确保了活动请求的真实性、完整性和合法性。通过这种方式,我们能够有效地防止黑灰产利用模拟器或非合法应用参与营销活动,保障企业营销活动的公平性和经济效益,为用户提供一个健康、有序的营销环境。在实际应用中,我们可以根据具体的营销活动需求和业务场景,进一步优化和扩展该防护策略,提高系统的安全性和性能。希望这篇文章能为从事电商和营销活动相关开发的人员提供有益的参考和帮助,让我们共同抵制薅羊毛行为,维护良好的商业生态。如果在实现过程中遇到问题,不要气馁,仔细分析,参考相关文档和示例代码,相信你一定能够找到解决方案。加油!