跳到主要内容

Demo - NodeJS

使用 NodeJS 编程语言编写的请求示例。


rsa_util

import crypto, {constants, KeyObject, KeyPairKeyObjectResult, RsaPrivateKey, RsaPublicKey} from "crypto";

const keySize: number = 1024;

export function keyPair(length?: number): { rsaPrivateKey: RsaPrivateKey; rsaPublicKey: RsaPublicKey } {
const result: KeyPairKeyObjectResult = crypto.generateKeyPairSync("rsa", {
modulusLength: length ?? keySize,
});
const privateKey: KeyObject = result.privateKey;
const publicKey: KeyObject = result.publicKey;
const rsaPrivateKey: RsaPrivateKey = {
key: privateKey,
padding: constants.RSA_PKCS1_PADDING
};
const rsaPublicKey: RsaPublicKey = {
key: publicKey,
padding: constants.RSA_PKCS1_PADDING
};
return {rsaPrivateKey, rsaPublicKey};
}

export function publicKey(keyData: string): crypto.RsaPublicKey {
if (!keyData.startsWith('-----BEGIN PUBLIC KEY-----') && !keyData.endsWith('-----END PUBLIC KEY-----')) {
const matchedKeyData = keyData.match(/.{1,64}/g);
keyData = `-----BEGIN PUBLIC KEY-----\n${matchedKeyData ? matchedKeyData.join('\n') : ''}\n-----END PUBLIC KEY-----`;
}
const publicKey: KeyObject = crypto.createPublicKey(keyData);
return {
key: publicKey,
padding: constants.RSA_PKCS1_PADDING
};
}

export function privateKey(keyData: string): RsaPrivateKey {
if (!keyData.startsWith('-----BEGIN PRIVATE KEY-----') && !keyData.endsWith('-----END PRIVATE KEY-----')) {
const matchedKeyData = keyData.match(/.{1,64}/g);
keyData = `-----BEGIN PRIVATE KEY-----\n${matchedKeyData ? matchedKeyData.join('\n') : ''}\n-----END PRIVATE KEY-----`;
}
const privateKey: KeyObject = crypto.createPrivateKey(keyData);
return {
key: privateKey,
padding: constants.RSA_PKCS1_PADDING
};
}

export function encrypt(publicKey: RsaPublicKey, plains: string): string {
const block: number = (keySize / 8) - 11;
const buffer: Buffer = Buffer.from(plains);
const buffers: Buffer[] = splitBuffer(buffer, block);
let ciphers: Buffer[] = [];
buffers.forEach((buffer: Buffer): void => {
const cipher: Buffer = crypto.publicEncrypt(publicKey, buffer);
ciphers.push(cipher);
});
return Buffer.concat(ciphers).toString("base64");
}

export function decrypt(privateKey: RsaPrivateKey, cipher: string): string {
const block: number = keySize / 8;
const buffer: Buffer = Buffer.from(cipher, "base64");
const buffers: Buffer[] = splitBuffer(buffer, block);
let plains: Buffer[] = [];
buffers.forEach((buffer: Buffer): void => {
const plain: Buffer = crypto.privateDecrypt(privateKey, buffer);
plains.push(plain);
});
return Buffer.concat(plains).toString("utf-8");
}

export function sign(privateKey: RsaPrivateKey, cipher: string): string {
const buffer: Buffer = Buffer.from(cipher);
const sign: Buffer = crypto.sign("RSA-SHA256", buffer, privateKey.key);
return sign.toString("base64");
}

export function verify(publicKey: RsaPublicKey, cipher: string, sign: string): boolean {
const buffer: Buffer = Buffer.from(cipher);
return crypto.verify("RSA-SHA256", buffer, publicKey.key, Buffer.from(sign, "base64"));
}

function splitBuffer(buffer: Buffer, size: number): Buffer[] {
const buffers: Buffer[] = [];
for (let i: number = 0; i < buffer.length; i += size) {
buffers.push(buffer.subarray(i, i + size));
}
return buffers;
}

const RsaUtil = {
keyPair,
publicKey,
privateKey,
encrypt,
decrypt,
sign,
verify,
}

export default RsaUtil;


fire_demo

import {describe, it} from "mocha";
import {assert} from "chai";

import https from "https";
import http from "http";
import RsaUtil from "../../src/util/rsa_util.mjs";
import crypto, {RsaPrivateKey, RsaPublicKey} from "crypto";

/**
* 模拟 agency 向 ice-run 发送请求
*/
describe("fire_demo", (): void => {

/**
* 服务商(agency)的编号
* 此处使用 agency_no = 100000000000000000 作为模拟
* 真实请求场景,请咨询 ice-run 的技术支持获取正式的 服务商编号 agency_no
*/
const AGENCY_NO: string = "100000000000000000";

/**
* 安全算法 RSA
*/
const SECURITY: string = "RSA";

/**
* ice-run 的 公钥
*/
const PUBLIC_KEY: string = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC78QqLKTkVcwvB2Scm4IoImOXmIthVmx4HHr5Im09aol2r2UlMK3jl4lamdxktvjFswWx41hQBD4tyYFHTY+mQJsRK8Zwm7LZmLJDHcgibIuLxEddln+MI+rWwT+SOdOt7lBvd9PcO4nYr/3Nqkyk8L4PrT+GF7dpVk8iE/VpTwQIDAQAB";

/**
* agency 的 私钥
*/
const PRIVATE_KEY: string = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOyC76Y+/2NqkkIVUtAHkbZkBYI2XXkuqE9o4oIDkbyAY04jC5MLa3oVc3uGG7T/Elodo9dIbIxNh7/ir7GQLR/hY+ookHAewurJ1z4Jn5TQeRk3+nYQcX6a6orfUOMffwcn0Jtat8D+cNAu3uCi+N7Ez/yNpItNORYqTa8AkgzrAgMBAAECgYAS/rsfl3ysb+E6RHsnsQvvYZ4dpJ8iPfCPnCVg+sdoI8mV+3ORBkBGCFYDjDRKd5fyO+IuRqdNJ2bpLtwcfy9YchcV/x40GIuAlre6A6qt4Raq+0l4FJufaqgdmSiLxd5zPX79gdMvBqTbdYzCSDGP5Pf3pTqIS3iKvSixFTQBzQJBAO6k6rE6JE/EHrbRjVtZNitDZ8rouCL0s9nGsgIDBSfgOCClO+HlZKcHq6W9ry8j1gOFeGH2rF1yMslA/ZymRL0CQQD9tk/gk9Xs1Eg1if7SLwvNxUP+z96oEqQTF26tSudPRwOO88fnfE4ZZ4heBt1QI+s7IhG39qecsFW269nvKfbHAkEArXmEgUBalQFjslGyB+1Zyyk8keuJrx9ifbRKQdwgK1R6eICkfxlZiXGx/NFeP041jGnBkLTXpzYUZOexc+YJoQJBANR3r87vnxAU+l+zr62O7oCk+XtT0y/HZJYEYpBHEQyn+MfnSXqG89R8iovLjd0GJ4E+173KlrU2SqHEQ57w8pMCQAEoDjFUXpYzBl8e0M9XsSpGY531mbOl8ASnVp4OSyavaCsn56eEkL9TI5q69KrYUEA4PN3BazMWfll2s/wu6G0=";

/**
* ice-run 的 test 环境的 API 网关 的域名地址
* 正式生产环境的域名地址请查阅 ice-run 的技术文档
*/
const DOMAIN: string = "test-fire-api.ice.run";

/**
* 示例地址,仅用于测试加密解密加签验签流程,无任何实际业务场景
*/
const DEMO: string = "/demo/api/demo";

/**
* 示例接口 demo
*/
it("demo", (): void => {

/**
* 对方公钥
*/
const publicKey: RsaPublicKey = RsaUtil.publicKey(PUBLIC_KEY);

/**
* 己方私钥
*/
const privateKey: RsaPrivateKey = RsaUtil.privateKey(PRIVATE_KEY);

/**
* 此处使用 `${URL}` 模拟请求地址
*/
const URL: string = `https://${DOMAIN}${DEMO}`;
console.log("request url", URL)

/**
* 请求参数:通常为复杂对象。
* 此处使用 json 对象 模拟,实际场景可以定义数据模型类
* 注意:不可传 null 或 空字符串 或 单值 ( string, number, boolean )
* 如果当前业务接口不需要传递具体参数,可以传空对象参数 `{}`,json 序列化为 `{"param":{}}`
*/
const param: { name: string } = {
name: "!"
}

/**
* 参数明文:参数对象的 json 序列化后的字符串的明文
*/
const paramPlains: string = JSON.stringify(param);
console.log("param plains", paramPlains);

/**
* 参数密文:agency 使用 ice-run 公钥 对参数明文加密后得到的密文
*/
const paramCipher: string = RsaUtil.encrypt(publicKey, paramPlains);
console.log("param cipher", paramCipher);

/**
* 请求签名:agency 使用 私钥 对参数密文进行签名
* 当 ice-run 收到请求时,将会使用 agency 公钥 进行验签
*/
const requestSign: string = RsaUtil.sign(privateKey, paramCipher);
console.log("request sign", requestSign);

/**
* 请求数据 request body
* 此处使用 json 对象 模拟,实际场景建议定义数据模型类
* body 数据的 键 固定为 param ,值为 json 序列化后的字符串 RSA 加密后的密文
*/
const requestBodyObject: { param: string } = {
param: paramCipher,
}
console.log("request body object", requestBodyObject);

/**
* 请求时间,当前世界标准时间的字符串,格式为 RFC-1123 标准,服务器会校验时间差,不允许超过设置的时间间隔(默认为 2 分钟)
*/
const time: string = new Date().toUTCString();

/**
* 链路追踪号(防止重放攻击的请求编号)
* 单位时间内(默认为 1 分钟)不重复的字符串,建议使用 uuid 或者时间戳+随机数。限制长度 = 32。正则表达式 = `^[0-9a-f]{32}$`
*/
const trace: string = crypto.randomUUID().replaceAll("-", "");

const requestOptions: http.RequestOptions = {
method: "POST",
hostname: DOMAIN,
path: DEMO,
headers: {
"Content-Type": "application/json",
"X-Client": AGENCY_NO,
"X-Security": SECURITY,
"X-Sign": requestSign,
"X-Time": time,
"X-Trace": trace,
},
}
console.log("request headers", requestOptions.headers);

/**
* demo 客户端,此处使用 node 标准库模拟,实际场景建议自行选择 HTTP 客户端组件包
* 发起 HTTP 请求之前,请确认 `${DOMAIN}` 可以正常访问,部分客户端的服务器环境可能需要设置请求代理
*/
const request: http.ClientRequest = https.request(requestOptions, (response: http.IncomingMessage): any => {

/**
* 响应状态码
*/
const statusCode: number | undefined = response.statusCode;
console.log("response status", statusCode);
assert.equal(statusCode, 200);

/**
* 响应 header
*/
const headers: http.IncomingHttpHeaders = response.headers;
console.log("response headers", headers);

/**
* 响应 header X-Sign
*/
const responseSign: string = (headers["X-Sign"] ?? headers["x-sign"])?.toString() ?? "";
assert.isNotEmpty(responseSign);
console.log("response sign", responseSign);

const trunks: Buffer[] = [];

response.on("data", (chunk: Buffer): void => {
trunks.push(chunk);
});

response.on("end", (): void => {

/**
* 响应 body ,json 格式的字符串
*/
const responseBody: string = Buffer.concat(trunks).toString();
console.log("response body", responseBody);

/**
* 响应数据 response body
* 此处使用 json 对象 模拟,实际场景建议定义数据模型类
* 数据的 键 有 code 和 message 和 data
* 其中 data 为 json 序列化后的字符串 RSA 加密后的密文
*/
const responseBodyObject: { code: string, message: string, data: string } = JSON.parse(responseBody);
console.log("response body object", responseBodyObject);

/**
* 响应码,默认 000000 表示请求成功
*/
const code: string = responseBodyObject.code;
console.log("response code", code);
assert.equal(code, "000000");

/**
* 响应 数据密文
*/
const dataCipher: string = responseBodyObject.data;
console.log("data cipher", dataCipher);

/**
* 响应验签:ice-run 处理响应时,会使用 ice-run 私钥对响应密文进行签名
* agency 收到响应后,需要使用 ice-run 公钥进行验签
*/
const verify: boolean = RsaUtil.verify(publicKey, dataCipher, responseSign);
console.log("response verify", verify);
assert.ok(verify);

/**
* 响应 数据明文
*/
const dataPlains: string = RsaUtil.decrypt(privateKey, dataCipher);
console.log("data plains", dataPlains);

const data: { name: string } = JSON.parse(dataPlains);
console.log("data", data);
assert.equal(data.name, param.name.split("").reverse().join(""));
});

return response;

}).on("error", (error: Error): void => {
console.error(error);
assert.fail(error.message);
});

const requestBody: string = JSON.stringify(requestBodyObject);
console.log("request body", requestBody);
request.write(requestBody);

request.end();

});

});