如何生成带有校验和的资源标识符

简介

唯一标识符(UIDs)或标识符可以是一个字符串值或一个整数,API开发者经常使用它们来寻址API中的唯一资源。然后,API消费者使用这些标识符从资源集合中获取单个资源。如果没有唯一标识符,几乎不可能将资源分开并根据需要调用它们。标识符可以指代数据库结构元素,例如表的名称、表中的字段(列)或约束,并且可以进一步指定为数据库中的唯一项目。例如,在与酒店预订门户网站相关的数据库中,Hotel(id)可能指向一个引用唯一酒店的标识符。通过Hotel(id = 1234,name = “Hyatt”),您将能够通过ID 1234或名称“Hyatt”识别这家特定酒店。

在API设计模式中,John J. Geewax确定了一个良好标识符所具备的七个基本特征。当生成唯一ID时,考虑到这些特征非常重要。

  • Easy to use: an identifier should avoid reserved characters like the forward slash (/) as these characters perform specific meaning in URLs.
  • Unique: an identifier should be able to refer to a single resource in an API.
  • Fast to generate: the ID generation process should perform in a predictable manner for consistency when scaling.
  • Unpredictable: when an identifier is unpredictable, it provides security benefits for vulnerability management.
  • Readable: an identifier should be human-readable, which is achieved by avoiding the digit 1, lowercase L, uppercase I, or the pipe character (|) as these characters may create confusion if someone needs to check the ID manually.
  • Verifiable: a checksum character can be used to verify the ID during an integrity check.
  • Permanent: once assigned, the identifiers should not change.

Note

提示:更改标识符可能会引起意想不到的混淆。如果你有一个指定了酒店标识符的条目(id=1234, name=”Hyatt”),然后这个标识符后来变成了(id=5678, name=”Hyatt”),原来的ID可能会被重复使用。如果原先的标识符可用,并且创建一个名为“Grand Villa”的新酒店(Hotel(id=1234, name=”Grand Villa”)),这个新酒店会重用原始的标识符(1234)。然后,当你查询酒店1234时,可能会收到与预期不符的结果。

在本教程中,您将使用Node.JS生成一个独特的自定义资源标识符,满足这些特征,并生成一个相关的校验和。校验和是通过对数字对象应用哈希函数获得的文件或数字数据的数字指纹的哈希值。本教程的校验和将是通过对与您的资源对应的字节大小进行编码(或哈希)的算法过程得出的单个字母数字字符。

先决条件

开始这个教程之前,你需要准备以下物品:

  • Node.js installed on your machine, which you can set up by following How To Install Node.js. This tutorial has been tested with Node.JS version 16.16.0.
  • Familiarity with Node.js. Learn more in the How To Code in Node.js series.
  • Familiarity with APIs. For a comprehensive tutorial on working with APIs, you can review How to Use Web APIs in Python3. Though written for Python, the article will help you understand the core concepts for working with APIs.
  • A text editor that supports JavaScript syntax highlighting, such as Atom, Visual Studio Code, or Sublime Text. This tutorial uses the command line editor nano.

步骤1 — 生成一个编码ID

在这个步骤中,您将编写一个函数,将随机字节生成的标识符转换为唯一的字母数字字符串。您的标识符将使用base32编码进行编码,但在本教程的后期将不会附带校验和。编码过程将根据您选择的字节数创建一个指定长度的唯一标识符,构建一个融合了一些良好标识符特征的ID。

开始时,为这个项目创建一个新文件夹,然后进入该文件夹。

  1. mkdir checksum
  2. cd checksum

这个教程中,项目文件夹将被称为”校验和”。

使用您喜爱的编辑器在项目文件夹中创建和打开一个package.json文件。

  1. nano package.json

然后添加以下代码行:

package.json -> 包描述文件
{
  "name": "checksum",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module"
}

在这个文件中,你将项目名称定义为checksum,并且将代码版本定义为”1.0.0″。你把主要的JavaScript文件定义为index.js。当你在package.json文件中有”type”: “module”时,你的源代码应该使用import语法。在这个文件中,你使用JSON数据格式,你可以在《如何在JavaScript中使用JSON》中了解更多相关内容。

保存并关闭文件。

你将使用一些Node.js模块来生成ID:crypto和base32-encode,以及它对应的解码器base32-decode。crypto模块已经打包在Node.JS中,但是你需要在本教程后面使用时安装base32-encode和base32-decode。编码是将一系列字符(字母、数字、标点符号和特定符号)转换为一种专门的格式,以实现高效的传输或存储。解码是相反的过程:将编码格式转换回原始字符序列。Base32编码使用32个字符集,使它成为用于表示数字的文本32符号符号化方法。

在终端会话中,使用以下命令将这些模块包安装到项目文件夹中。

  1. npm i base32-encode base32-decode

你将收到一个输出,该输出表明这些模块已经被添加。

Output
added 3 packages, and audited 5 packages in 2s found 0 vulnerabilities

如果安装中遇到问题,您可以参考《如何使用npm和package.json来支持Node.js模块》获取支持。

在你的项目文件夹中,还需要创建一个名为index.js的新文件。

  1. nano index.js

将下列行的JavaScript代码添加到index.js文件中:

index.js 文件
import crypto from 'crypto';  
import base32Encode from 'base32-encode';
import base32Decode from 'base32-decode';
 
function generate_Id(byte_size) {
    const bytes = crypto.randomBytes(byte_size);
    return base32Encode(bytes, 'Crockford');
}

console.log('ID for byte size = 1:',generate_Id(1), '\n');
console.log('ID for byte size = 12:',generate_Id(12), '\n');
console.log('ID for byte size = 123:',generate_Id(123), '\n');

导入命令会加载所需的模块。为了从数字生成字节,您可以定义一个generate_Id函数来接收字节数大小,并使用加密模块的randomBytes函数生成该大小的随机字节。然后,generate_Id函数将使用Crockford的base32编码方式对这些字节进行编码。

为了教学目的,生成了一些ID并将其记录到控制台中。下一步将使用base32-decode模块来解码资源ID。

保存你的index.js文件,然后在终端会话中使用这个命令运行代码。

node index.js

你会收到类似于这样的输出回应。

Output
ID for byte size = 1: Y8 ID for byte size = 12: JTGSEMQH2YZFD3H35HJ0 ID for byte size = 123: QW2E2KJKM8QZ7174DDB1Q3JMEKV7328EE8T79V1KG0TEAE67DEGG1XS4AR57FPCYTS24J0ZRR3E6TKM28AM8FYZ2AZTZ55C9VVQTABE0R7QRH7QBY7V3GBYBNN5D9JK0QMD9NXSWZN95S0772DHN43Q003G0QNTPA2J3AFA3P7Q167C1VNR92Z85PCDXCMEY0M7WA

由于生成的字节的随机性,您的ID值可能会有所不同。生成的ID可能会因为所选择的字节大小而变短或变长长度不定。

回到index.js文件,使用JavaScript的注释功能注释掉控制台输出(在代码行前面添加双斜杠 //)。

index.js 文件
...
//console.log('ID for byte size = 1:',generate_Id(1), '\n'); 
//console.log('ID for byte size = 12:',generate_Id(12), '\n');
//console.log('ID for byte size = 123:',generate_Id(123), '\n');

这些行展示了编码将根据相关的字节输出不同的标识符。由于这些行在后面的部分不会被使用,你可以根据这个代码块示例来将其注释掉或者完全删除掉。

在这一步中,您通过对随机字节进行编码来创建了一个编码的ID。在下一步中,您将合并编码字节和校验和,创建一个唯一的标识符。

第二步 — 生成资源标识符

现在您将创建一个带有校验字符的ID。生成校验字符是一个两步骤的过程。为了教学目的,创建组合函数的每个函数将分别在以下小节中构建。首先,您将编写一个运行模数运算的函数。然后,您将编写另一个将结果映射到校验字符的函数,这是生成资源ID的校验和的方式。最后,您将验证标识符和校验和,以确保资源标识符准确无误。

进行模运算

在本节中,您将把与编号ID相对应的字节转换为介于0到36之间的数字(含上下限,即0到36之间的任何数字,包括0和36)。将与编号ID相对应的字节通过求模运算转换为整数。求模运算将返回将字节转换为BigInteger(BigInt)值后获得的被除数的余数。

要实施此过程,将以下代码行添加到index.js文件的底部。

index.js – 索引.js
...

function calculate_checksum(bytes) {
    const intValue = BigInt(`0x${bytes.toString('hex')}`);
    return Number(intValue % BigInt(37));
}

函数calculate_checksum与之前在文件中定义的字节一起工作。该函数将字节转换为十六进制值,进一步将其转换为BigInteger BigInt值。BigInt数据类型表示比Javascript中的原始数据类型number表示的数值更大的数字。例如,虽然整数37相对较小,但它被转换为BigInt进行模操作。

为了实现这个转换,首先你要使用BigInt的转换方法将intValue变量设置为一个数值,然后使用toString方法将字节转换为十六进制。接着,你可以使用Number构造函数返回一个数值,其中你可以运用%符号来执行模运算,以找出intValue和BigInt之间使用37这个样例值的余数。这个整数值(在这个例子中为37)被用作下标,从自定义的字符字符串中选择一个字母数字字符。

如果intValue的值是123(根据字节而定),模运算将为123 % 37。这个运算的结果,以37为整数值,将会得到余数12和商3。如果资源ID的值为154,运算154 % 37将会得到余数6。

这个函数将输入的字节映射到模数结果。接下来,你将编写一个函数将模数结果映射到校验和字符。

获取校验字符

在前一节中获得模数结果后,你可以将其映射为校验字符。

将以下代码行添加到index.js文件中的先前代码之下:

index.js -> 主程序文件.index.js
...

function get_checksum_character(checksumValue) {
    const alphabet = '0123456789ABCDEFG' +
        'HJKMNPQRSTVWXYZ*~$=U';  
    return alphabet[Math.abs(checksumValue)]; // 
}

在get_checksum_character函数中,您以checksumValue作为参数调用checksumValue函数。在这个函数中,您将字符串常量alphabet定义为一个自定义的字母数字字符串。根据checksumValue的值,该函数将返回一个将字母表常量中定义的字符串与checksumValue的绝对值进行配对的值。

接下来,你将编写一个函数,该函数利用这两个部分编写的函数来生成一个由字节编码和校验字符组合而成的ID。

将以下代码添加到index.js文件中。

index.js的汉语翻译有:”主文件.js”
... 

function generate_Id_with_checksum(bytes_size) {
    const bytes = crypto.randomBytes(bytes_size);
    const checksum = calculate_checksum(bytes);
    const checksumChar = get_checksum_character(checksum);
    console.log("checksum character: ", checksumChar); 
    const encoded = base32Encode(bytes, 'Crockford');
    return encoded + checksumChar;
}

const Hotel_resource_id =generate_Id_with_checksum(132)
console.log("Hotel resource id: ",Hotel_resource_id)

这段代码将你之前的两个函数calculate_checksum和get_checksum_character(用于生成校验字符)与编码函数结合在一起,并命名为generate_Id_with_checksum,创建了一个带有校验字符的ID的新函数。

保存文件,然后在单独的终端会话中运行代码。

  1. node index.js

你将会收到一个类似于这样的输出。

Output
checksum character: B Hotel resource id: 9V99B9P55K7M4DN5XYP4VTJYJGENZKJ0F9Q6EEEZ07X49G0V14AXJS3RYXBT3J1WJZXWGM76C6H7G895TJT27AW77BHBX2D16QNQ2ZNBY9MQHWG9NJ1WWVTNRCKRBX6HC3M7BB3JG0V413VJ767JN6FT0GFS5VQJ9X7KSP1KM29B02NAGXN3FP30WA8Y63N1XJAMGDPEE1RNHRTWH6P0B

相同的校验字符出现在ID的末尾,表示校验和匹配。

这个示意图提供了一个结构性的表示,展示了这个复合函数是如何工作的。

A chart with Product ID at the top. It points to Crypto method, which points to Bytes. There are two branches from Bytes: base32-decode and Modulo process. The base32-decode branch points to the Encoded ID, whereas the Modulo process branch points to the Checksum. When the Encoded ID and Checksum are paired, they become the Resource ID.

这个流程图展示了一个产品ID是如何通过编码和取模的过程转换成唯一的资源ID的。产品ID是由资源计数器手动创建的标识符。图中的加密方法指的是crypto.randomBytes()函数。

你根据字节大小创建了一个包含校验字符的ID。在下一个部分,你将实现一个标识函数,通过base32解码来验证ID的完整性。

检查标识符的完整性

为了保证完整性,您将使用一个名为verify_Id的新函数,将校验和字符(标识符的最后一个字符)与生成的校验和进行比较。比较校验和字符是检查原始ID的完整性并确定其是否被篡改的重要步骤。

将这些行添加到你的index.js文件中。

index.js 内容如下
...
function verify_Id(identifier) {
    const value = identifier.substring( 0, identifier.length-1);
    const checksum_char = identifier[identifier.length-1];     
    const buffer = Buffer.from( base32Decode(value, 'Crockford'));
    const calculated_checksum_char = get_checksum_character(calculate_checksum(buffer));
    console.log(calculated_checksum_char);
    const flag =calculated_checksum_char== checksum_char;
    return (flag);    
     }
console.log('\n');
console.log("computing checksum")
const flag = verify_Id(Hotel_resource_id);
if (flag) console.log("Checksums matched.");
else console.log("Checksums did not match.");

verify_Id 函数通过检查校验和来验证 ID 的完整性。将标识符的剩余字符解码到缓冲区中,然后依次运行 calculate_checksum 和 get_checksum_character 函数来提取校验和字符,以与计算得出的校验和字符进行比较(即 calculated_checksum_char == checksum_char)。

这个示意图展示了复合函数如何工作。

A chart with Resource ID at the top. It points to Slicing Method, which has two branches: Value and Checkum. The Value branch points to the base32-decode, which then becomes a Decoded checksum. The Checksum branch points to a Checksum. If the Decoded checksum and the Checksum match, it results in Verification.

在此图表中,切片指的是将ID值(value)与校验和字符(checksum)分隔开。在您之前的代码块中,函数identifier.substring( 0, identifier.length-1)选择了ID值,而identifier[identifier.length-1]则获取了资源ID中的最后一个字符。

你的 index.js 文件现在应该与以下代码相匹配:

你的 index.js 文件现在需要与以下代码一致:

index.js 的中文本地化翻译如下:主文件.js。
import crypto from 'crypto';  // for generating bytes from the number
import base32Encode from 'base32-encode'; // for encoding the bytes into Unique ID as string type
import base32Decode from 'base32-decode';// for decoding the ID into bytes

function generate_Id(byte_size) {
    const bytes = crypto.randomBytes(byte_size);
    return base32Encode(bytes, 'Crockford');
}

//console.log('ID for byte size = 1:',generate_Id(1), '\n');
//console.log('ID for byte size = 12:',generate_Id(12), '\n');
//console.log('ID for byte size = 123:',generate_Id(123), '\n');

function calculate_checksum(bytes) {
    const intValue = BigInt(`0x${bytes.toString('hex')}`);
    return Number(intValue % BigInt(37));
}

function get_checksum_character(checksumValue) {
    const alphabet = '0123456789ABCDEFG' +
        'HJKMNPQRSTVWXYZ*~$=U'; // custom-built string  consisting of alphanumeric character
    return alphabet[Math.abs(checksumValue)]; // picking out an alphanumeric character
}

function generate_Id_with_checksum(bytes_size) {
    const bytes = crypto.randomBytes(bytes_size);
    const checksum = calculate_checksum(bytes);
    const checksumChar = get_checksum_character(checksum);
    console.log("checksum character: ", checksumChar); 
    const encoded = base32Encode(bytes, 'Crockford');
    return encoded + checksumChar;
}

const Hotel_resource_id =generate_Id_with_checksum(132)
console.log("Hotel resource id: ",Hotel_resource_id)

function verify_Id(identifier) {
    const value = identifier.substring( 0, identifier.length-1);
    const checksum_char = identifier[identifier.length-1]; 
    //console.log(value,checksum_char);
    const buffer = Buffer.from( base32Decode(value, 'Crockford'));
    const calculated_checksum_char = get_checksum_character(calculate_checksum(buffer));
    console.log(calculated_checksum_char);
    const flag =calculated_checksum_char== checksum_char;

    return (flag);
    
     }

console.log('\n');
console.log("computing checksum")
const flag = verify_Id(Hotel_resource_id);
if (flag) console.log("Checksums matched.");
else console.log("Checksums did not match.");

现在你可以运行这段代码了。

node index.js

您将收到以下输出结果:

Output
... computing checksum AW75SY7FVC7TKT7VP5ZF0M8C67CN36YZK27BXHVFHSDXJFKH54HK2AXQFMPN89Q5YQRPGNHGAYQ5JFKVD40EKTXCET97Q0FEPX6MX1ZTNWGCA08SBRSHP8B0037ACJG6F6472FEVARCAWM6P5MRJ2F6WTRPXHYS9N1JEDZVH41D33RA5365VNFC5G5VYEFPFJJD8151B28XXDBRHAF80 H H Checksums matched.

你现在有一个叫做verify_Id的函数,它使用一个校验字符来检查你的标识符的完整性。接下来,为了教学目的,你可以修改资源ID,这样函数将会返回一个不匹配的结果,以评估当校验失败时会发生什么。

(可选)第三步——更改不匹配结果的标识符

您现在将更改标识符的值,以检查校验和是否匹配。在这一步中进行的更改总是会导致校验和不匹配,因为如果ID中的任何一个字符被篡改,完整性就无法得到保持。这种更改可能是由于传输错误或恶意行为引起的。这种更改仅用于教学目的,不建议在生产环境中使用,但它将使您能够评估不匹配的校验和结果。

在你的index.js文件中,通过添加高亮行来修改Hotel_resource_id。

index.js 重点.js
...
const altered_Hotel_resource_id= Hotel_resource_id.replace('P','H');   
console.log("computing checksum")
const flag = verify_Id(altered_Hotel_resource_id);
if (flag) console.log("Checksum matched.");
else console.log("Checksums did not match.");

在上述的代码中,你需要将所有的P替换为H,并将变量名从Hotel_resource_ID改为altered_Hotel_resource_id。同样,这些变动仅为了信息目的,在这一步结束后可以还原以确保匹配的完整性。

将文件保存,然后使用更改后的资源ID重新运行代码。

  1. node index.js

你将会收到一个输出,校验和不匹配。

Output
Checksums did not match.

在这个步骤中,您创建了一个函数,用于验证校验和是否通过完整性测试,您遇到了两种情况。不匹配的校验和表示资源ID已被篡改。通知使开发人员可以根据应用需求采取措施对抗恶意行为,比如阻止用户请求或者报告与资源ID相关的请求。

为了使函数恢复与匹配校验和结果相符,删除在此步骤开头添加的额外代码,以便代码与第2步结束时的文件相匹配。

当你需要一个带有校验和的自定义唯一标识符时,你可以使用这个教程来帮助你生成数据模型、版本化你的API等等。

结论

在本教程中,你开发了与良好标识符特征相一致的资源ID。你还在Node.js环境中使用base32编码创建了一个带有校验和的唯一资源ID。最后,你通过base32解码来验证该ID的完整性。

为了交叉验证,您可以将最终文件与Silicon Cloud社区仓库中的文件进行对比。如果您熟悉git版本控制系统,或者按照《GitHub和开源项目入门系列》的介绍进行操作,也可以进行git克隆仓库。

现在你了解了校验和的基础知识,你可以尝试其他的编码算法,比如MD5。

发表回复 0

Your email address will not be published. Required fields are marked *