id-generator
生成 19 位的 Long ID、22 位的短 UUID、卡号、短卡号、带校验码卡号、激活码、付款码、数据加密、手机号加密、带失效时间的数字加密。生成器是分布式,支持多负载,无需数据库、redis 或者 zk 作为 ID 分配的 key。ID 分配无需 RPC 调用,基于本地内存计算,结构简单,可靠性和性能比较高,每秒可以分配几十万的 ID。
Features
1、19 位 Long 类型的 ID
1.1 说明
ID 固定为 19 位,64bit。 可用于各种业务系统的 ID 生成。格式为:”1053669091396554764“,
+=============================================
| 42bit 毫秒时间戳 | 10bit 机器编号 | 12bit 序号 |
+=============================================
- 42 bit 的毫秒时间戳支持 68 年
- 12 bit 序号支持 4096 个序号
- 10 bit 机器编号支持 1024 台负载
即 ID 生成最大支持 1024 台负载,每台负载每毫秒可以生成 4096 个 ID,这样每台负载每秒可以产生 40 万 ID。
生成器代码LongIdGenerator
详细示例代码:LongIdGeneratorTest
1.2 生成 ID
private final LongIdGenerator generator = new LongIdGenerator(1L);
@Test
public void generateId() {
Long id = generator.generate();
Assert.assertEquals(19, String.valueOf(id).length());
}
1.3 ID 逆向
支持从 ID 解析出时间、机器和序号等信息
@Test
public void parse() {
Long id = generator.generate();
Long[] results = generator.parse(id);
long timestamp = results[0];
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd").format(dateTime));
System.out.println("Machine id: " + results[1]);
System.out.println("Sequence: " + results[2]);
}
执行结果:
2、22 位短 UUID
2.1 说明
UUID 最长 22 位。 排除掉 1、l 和 I,0 和 o 易混字符。本质是将 UUID(32 位 16 进制整数)转换为 22 位 57 进制数。格式为:”MCyYSL4uvizAhvem4jYXW6“。
生成器代码ShortUuidGenerator
详细示例代码:ShortUuidGeneratorTest
2.2 生成 UUID
private final ShortUuidGenerator shortUuidGenerator = new ShortUuidGenerator();
@Test
public void generate() {
shortUuidGenerator.generate();
}
3、带系统编号的卡号
3.1 说明
卡号固定为 16 位,全数字,53bit。 设计 3bit 的卡类型,支持 8 种不同卡的类型。格式为:”1174893642711839“。
+=================================================================
| 3bit 卡类型 | 31bit 时间戳 | 3bit 机器编号 | 9bit 序号 | 7bit 卡号校验位 |
+=================================================================
- 31 bit 的秒时间戳支持 68 年
- 9 bit 序号支持 512 个序号
- 3 bit 机器编号支持 8 台负载
即卡号生成最大支持 8 台负载,每台负载每秒钟可以生成 512 个卡号。
时间戳、机器编号、序号和校验位的 bit 位数支持业务自定义,方便业务定制自己的生成器。
生成器代码CardIdGenerator
详细示例代码:CardIdGeneratorTest
3.2 生成卡号
private final CardIdGenerator cardIdGenerator = new CardIdGenerator();
@Test
public void generate() {
Long id = cardIdGenerator.generate();
Assert.assertEquals(16, String.valueOf(id).length());
}
3.3 校验
因为卡号中包含校验码和时间戳,因此后台可以对卡号进行合法性校验,作为系统的首道安全屏障。如果对卡号进行暴力破解,卡号校验通过的概率大概为 0.03%。
@Test
public void validate() {
long id = cardIdGenerator.generate();
Assert.assertTrue(cardIdGenerator.validate(id));
Assert.assertFalse(cardIdGenerator.validate(++id));
}
3.4 卡号逆向
支持从卡号中解析出卡类型、时间、机器和序号等信息。
@Test
public void parse() {
long id = cardIdGenerator.generate();
Long[] results = cardIdGenerator.parse(id);
System.out.println("System: " + results[0]);
long timestamp = results[1];
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd").format(dateTime));
System.out.println("Machine id: " + results[2]);
System.out.println("Sequence: " + results[3]);
}
输出结果:
4、带店铺编号的卡号
4.1 说明
卡号固定为 16 位,全数字,53bit。格式为:”2300795729019213“。
+=======================================================================
| 4bit 店铺编号校验位 | 30bit 时间戳 | 3bit 机器编号 | 9bit 序号 | 7bit 卡号校验位 |
+=======================================================================
- 30 bit 的秒时间戳支持 34 年
- 9 bit 序号支持 512 个序号
- 3 bit 机器编号支持 8 台负载
即卡号生成最大支持 8 台负载,每台负载每秒钟可以生成 512 个卡号。
时间戳、机器编号、序号和校验位的 bit 位数支持业务自定义,方便业务定制自己的生成器。
生成器代码ShopCardIdGenerator
详细示例代码:ShopCardIdGeneratorTest
4.2 生成卡号
private final ShopCardIdGenerator cardIdGenerator = new ShopCardIdGenerator();
@Test
public void generate() {
Long id = cardIdGenerator.generate("A00001");
Assert.assertEquals(16, String.valueOf(id).length());
}
4.3 校验
因为卡号中包含店铺编号校验位、校验码和时间戳,因此后台可以对卡号进行合法性校验,作为系统的首道安全屏障。如果对卡号进行暴力破解,卡号校验通过的概率大概为 0.004%。
@Test
public void validate() {
String shopId = "A00001";
long id = cardIdGenerator.generate(shopId);
Assert.assertTrue(cardIdGenerator.validate(shopId, id));
Assert.assertFalse(cardIdGenerator.validate(shopId, ++id));
Assert.assertFalse(cardIdGenerator.validate("A000111", id));
}
4.4 卡号逆向
支持从卡号中解析出时间、机器和序号等信息。
@Test
public void parse() {
String shopId = "A1234567";
long id = cardIdGenerator.generate(shopId);
Long[] results = cardIdGenerator.parse(id);
long timestamp = results[0];
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd").format(dateTime));
System.out.println("Machine id: " + results[1]);
System.out.println("Sequence: " + results[2]);
}
执行结果:
5、短卡号
5.1 说明
卡号固定为 13 位,全数字,43bit。格式为:”1290903816253“。
+=====================================================
| 3bit 机器编号 | 29bit 时间戳 | 8bit 序号 | 3bit 卡号校验位 |
+=====================================================
- 29 bit 的秒时间戳支持 17 年
- 8 bit 序号支持 256 个序号(起始序号是 20 以内的随机数)
- 3 bit 机器编号支持 7 台负载(负载编号从 1-7)
即卡号生成最大支持 7 台负载;每台负载每秒钟可以生成最少 236,最多 256 个卡号。
生成器代码ShortCardIdGenerator
详细示例代码:ShortCardIdGeneratorTest
5.2 生成卡号
private final ShortCardIdGenerator cardIdGenerator = new ShortCardIdGenerator();
@Test
public void generate() {
Long id = cardIdGenerator.generate();
Assert.assertEquals(13, String.valueOf(id).length());
}
5.3 卡号逆向
支持从卡号中解析出时间、机器和序号等信息。
@Test
public void parse() {
long id = cardIdGenerator.generate();
Long[] results = cardIdGenerator.parse(id);
long timestamp = results[0];
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd").format(dateTime));
System.out.println("Machine id: " + results[1]);
System.out.println("Sequence: " + results[2]);
}
执行结果:
6、店铺卡号激活码
6.1 说明
激活码和卡号绑定,格式为:”IQKHWVAJYZBV“。
激活码有如下特点:
- 激活码固定 12 位,全大写字母。
- 激活码生成时植入关联的卡号的 Hash,但是不可逆;即无法从激活码解析出卡号,也无法从卡号解析出激活码。
- 激活码本质上是一个正整数,通过一定的编码规则转换成全大写字符。为了安全,生成器使用 26 套编码规则,以字符 A 来说, 可能在“KMLVAPPGRABH”激活码中代表数字 4,在"MONXCRRIUNVA"激活码中代表数字 23。即每个大写字符都可以代表 0-25 的任一数字。
- 具体使用何种编码规则,是通过时间戳+店铺编号 Hash 决定的。
- 校验激活码分为两个步骤。(1)、 首先校验激活码的合法性 (2)校验通过后,从数据库查询出关联的卡号,对卡号和激活码的关系做二次校验
激活码的正整数由 51bit 组成
+=========================================================================================
| 4bit 店铺编号校验位 | 29bit 时间戳 | 3bit 机器编号 | 7bit 序号 | 4bit 激活码校验位 | 4bit 卡号校验位 |
+=========================================================================================
- 29 bit 的秒时间戳支持 17 年,激活码生成器计时从 2017 年开始,可以使用到 2034 年
- 7 bit 序号支持 128 个序号
- 3 bit 机器编号支持 8 台负载
即激活码生成最大支持 8 台负载,每台负载每秒钟可以生成 128 个激活码,整个系统 1 秒钟可以生成 1024 个激活码
时间戳、机器编号、序号和校验位的 bit 位数支持业务自定义,方便业务定制自己的生成器。
详细示例代码:ActivationCodeGeneratorTest
6.2 激活码生成流程
输入参数为店铺编号、卡号
获取当前时间戳和序列号
获取当前店铺编码(4bit,最大为 15)。店铺编号从高位开始,每间隔一位的数字乘 2 然后获取除以 10 的商和余数之和(放大单位错误带来的影响),然后取各位的和再进行取模。
将步骤 3 计算得到的店铺编码、时间戳、机器编号和序号拼接在一起
通过一定的算法对步骤 4 计算得到的结果进行数字计算,获取验证码
通过相同的算法对卡号进行数字计算,获取卡号验证码
将步骤 5 和步骤 6 得到的验证码拼接在一起得到新的验证码
将步骤 7 得到的验证码和 4 得到的原始编码拼接在一起形成新的字符串
将步骤 3 计算得到的店铺验证码和当前时间混合在一起,得到当前编码规则
利用步骤 9 计算得到的 base26 编码对步骤 8 得到的结果进行编码
将步骤 9 和步骤 10 的结果拼在一起,得到 12 位的店铺激活码
6.3 生成激活码:
private final ActivationCodeGenerator codeGenerator = new ActivationCodeGenerator(alphabets);
@Test
public void generate() {
String shopId = "A1111";
for (int i = 0; i < 100; i++) {
Long cardId = cardIdGenerator.generate(shopId);
String code = codeGenerator.generate(shopId, cardId);
Assert.assertEquals(12, code.length());
}
}
6.4 校验
因为激活码有着较多的校验信息,因此很难通过暴力方式破解激活码。
@Test
public void validate() {
String shopId = "A1111";
Long cardId = cardIdGenerator.generate(shopId);
String code = codeGenerator.generate(shopId, cardId);
Assert.assertTrue(codeGenerator.validate(shopId, code));
Assert.assertFalse(codeGenerator.validate("A111", code));
Assert.assertFalse(codeGenerator.validate(shopId, code + "A"));
char lastChar = code.charAt(code.length() - 1);
char newChar;
if (lastChar < 'Z') {
newChar = (char) (lastChar + 1);
} else {
newChar = (char) (lastChar - 1);
}
String newCode = code.substring(0, code.length() - 2) + newChar;
Assert.assertFalse(codeGenerator.validate(shopId, newCode));
}
@Test
public void validateCardId() {
String shopId = "A1111";
long cardId = cardIdGenerator.generate(shopId);
String code = codeGenerator.generate(shopId, cardId);
Assert.assertFalse(codeGenerator.validateCardId(code, cardId + 1));
Assert.assertFalse(codeGenerator.validateCardId(code, cardId - 1));
}
6.5 逆向激活码
支持从激活码中解析出时间、机器和序号等信息。
@Test
public void parse() {
String shopId = "A1008";
String code = codeGenerator.generate(shopId, 100000000L);
Long[] results = codeGenerator.parse(code);
long timestamp = results[0];
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
System.out.println("Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd").format(dateTime));
System.out.println("Machine id: " + results[1]);
System.out.println("Sequence: " + results[2]);
}
执行结果:
7、安全激活码
该激活码无需密码,凭码就可以直接激活消费。
激活码代码SecureActivationCodeGenerator
详细示例代码:SecureActivationCodeGeneratorTest
7.1 说明
激活码固定 16 位,全大写字母和数字,排除掉易混字符 0O、1I,一共 32 个字符。
激活码本质上是一个 16*5=80bit 的正整数,通过一定的编码规则转换成全大写字符和数字。
为了安全,使用者在创建生成器的时候,需要提供 32 套随机编码规则,以字符 A 来说,可能在“KMLVAPPGRABH”激活码中代表数字 4,在"MONXCRRIUNVA"激活码中代表数字 23。即每个字符都可以代表 0-31 的任一数字。
具体使用何种编码规则,是通过卡号进行 ChaCha20 加密后的随机数 hash 决定的。
激活码的正整数由 80bit 组成
+========================================================
| 5bit 编码号 | 30bit 序号明文 | 45bit 序号、店铺编号生成的密文 |
+========================================================
7.2 激活码生成流程
输入参数为店铺编号、卡号、序号
用 ChaCha20 算法对序号加密,得到一个 512 字节的随机数
将步骤 2 生成的随机数取前 256 字节作为 HMAC 算法的密钥
将序号、店铺编号、步骤 2 生成的随机数的后 256 字节拼成字节数组
用步骤 3 生成的 HMAC 对步骤 4 生成的字节数组进行加密
将店铺编号编码为 27bit,步骤 5 生成的字节数组取前 18bit,拼成 45bit 报文
步骤 4 生成的字节数组取前 45bit 报文 M1,步骤 6 生成的 45bit 报文 M2,将 M1 和 M2 进行异或运算
根据序号得到 30bit 的明文,步骤 7 得到 45bit 密文,将明文和密文拼接成 75bit 的激活码主体
用 ChaCha20 算法对卡号进行加密,得到的随机数按字节求和,然后对 32 取模
根据步骤 9 的结果,得到一套 base32 的编码方式,对步骤 8 产生的 75bit 激活码主体进行编码,得到 15 位的 32 进制数(大写字母和数字,排除掉 0O1I)
步骤 9 得到的结果进行 base32 编码得到一位 32 进制数
将步骤 11 和步骤 10 得到的结果拼在一起,得到 16 位的激活码
7.3 激活码验证流程
和生成流程相反。
7.4 激活码生成
private final SecureActivationCodeGenerator codeGenerator = createCodeGenerator();
private SecureActivationCodeGenerator createCodeGenerator() {
String alphabets = SecureActivationCodeGenerator.generateAlphabets();
return new SecureActivationCodeGenerator("abc1234567845#$&*(fYYTYTeefg~!@)", "^^jinpeicomp", 99999, alphabets);
}
@Test
public void generate() {
String shopId = "A1111";
for (int i = 0; i < 100; i++) {
Long cardId = cardIdGenerator.generate(shopId);
String code = codeGenerator.generate(shopId, cardId, i);
Assert.assertEquals(16, code.length());
System.out.println(code);
}
}
7.5 校验
如果对激活码进行暴力破解,校验通过的概率很小。
即使激活码生成算法暴露了,要破解一个激活码需要进行 2 的 45 次方次尝试,如果是一半概率的话也要 2 的 44 次方次。
@Test
public void validate() {
String shopId = "A1111";
Long cardId = cardIdGenerator.generate(shopId);
String code = codeGenerator.generate(shopId, cardId, 999);
Assert.assertTrue(codeGenerator.validate(shopId, code));
Assert.assertFalse(codeGenerator.validate("A111", code));
Assert.assertFalse(codeGenerator.validate(shopId, code + "A"));
char lastChar = code.charAt(code.length() - 1);
char newChar;
if (lastChar < 'Z') {
newChar = (char) (lastChar + 1);
} else {
newChar = (char) (lastChar - 1);
}
String newCode = code.substring(0, code.length() - 2) + newChar;
Assert.assertFalse(codeGenerator.validate(shopId, newCode));
}
8、数字加密
很多场景(快递、二维码等)下为了信息隐蔽需要对数字进行加密,比如用户的手机号码;并且需要支持解密。格式为:”210781520001014801“
本算法支持对不大于 12 位的正整数(即 1000,000,000,000)进行加密,输出固定长度为 18 位的数字字符串;支持解密。
详细示例代码:NumberHidingGeneratorTest
8.1 说明
加密字符串固定 18 位数字,原始待加密正整数不大于 12 位
加密字符串本质上是一个 56bit 的正整数,通过一定的编码规则转换而来。
为了安全,使用者在创建生成器的时候,需要提供 10 套随机编码规则,以数字 1 来说,可能在“5032478619”编码规则中代表数字 8,在"2704168539"编码规则中代表数字 4。即每个字符都可以代表 0-9 的任一数字。
具体使用何种编码规则,是通过原始正整数进行 ChaCha20 加密后的随机数 hash 决定的。
为了方便开发者使用,提供了随机生成编码的静态方法。
加密后的数字字符串由编码规则+密文报文体组成,密文由 56bit 组成,可转化为 17 位数,编码规则为一位数字:
+====================================================
| 1 位编码规则 | 37bit 原始数字 | 19bit 原始数字生成的密文 |
+====================================================
8.2 加密流程
输入参数为原始正整数
用 ChaCha20 算法对原始正整数加密,得到一个 512 字节的随机数
将步骤 2 生成的随机数取前 256 字节作为 HMAC 算法的密钥
将步骤 2 生成的随机数的后 256 字节、原始正整数拼成字节数组
用步骤 3 生成的 HMAC 对步骤 4 生成的字节数组进行加密
将原始正整数编码为 37bit,步骤 5 生成的字节数组取前 19bit,拼成 56bit 报文
步骤 2 得到的随机数按字节求和,然后对 9 取模加 1
根据步骤 7 的结果,得到一套 base10 的编码方式,对步骤 6 产生的 56bit 激活码主体进行编码,得到 17 位的 10 进制数
步骤 7 得到的结果进行 base10 编码得到一位 10 进制数
将步骤 9 和步骤 8 得到的结果拼在一起,得到 18 位的加密字符串
8.3 解密流程
和加密流程相反。
如果为非法字符串,解密方法则返回 null。
8.4 安全
如果对加密数字进行暴力破解,校验通过的概率很小。
内置 10 套编码方式,
8.5 使用方式
加密
private final NumberHidingGenerator generator = new NumberHidingGenerator("abcdefj11p23710837e]q222rqrqweqe",
"!@#$&123frwq", 10, alphabetsStr);
@Test
public void generate() {
long originNumber = 99999999999L;
String hidingStr = generator.generate(originNumber);
Assert.assertEquals(18, hidingStr.length());
Assert.assertTrue(isCharValid(hidingStr));
originNumber = 6L;
hidingStr = generator.generate(originNumber);
Assert.assertEquals(18, hidingStr.length());
Assert.assertTrue(isCharValid(hidingStr));
}
解密
@Test
public void parse() {
Long originNumber = 14825847997L;
String hidingStr = generator.generate(originNumber);
Assert.assertEquals(originNumber, generator.parse(hidingStr));
originNumber = 6L;
hidingStr = generator.generate(originNumber);
Assert.assertEquals(originNumber, generator.parse(hidingStr));
}
9、带有效期的数字加密
很多场景下为了信息隐蔽需要对数字进行加密,比如用户的付款码;并且需要支持解密。格式为:”77550501392592614656“
加密结果混入了时间信息,有效时间为 1 分钟,超过有效期加密结果会失效。
本算法支持对不大于 12 位的正整数(即 1000,000,000,000)混合时间信息进行加密,输出固定长度为 20 位的数字字符串;支持解密。
加密器代码TimeNumberHidingGenerator
详细示例代码:TimeNumberHidingGeneratorTest
9.1 说明
加密字符串固定 20 位数字,原始待加密正整数不大于 12 位
加密字符串本质上是一个 63bit 的正整数,通过一定的编码规则转换而来。
为了安全,使用者在创建生成器的时候,需要提供 10 套随机编码规则,以数字 1 来说,可能在“5032478619”编码规则中代表数字 8,在"2704168539"编码规则中代表数字 4。即每个字符都可以代表 0-9 的任一数字。
具体使用何种编码规则,是通过原始正整数进行 ChaCha20 加密后的随机数 hash 决定的。
为了方便开发者使用,提供了随机生成编码的静态方法。
加密后的数字字符串由编码规则+密文报文体组成,密文由 63bit 组成,可转化为 19 位数,编码规则为一位数字:
+===========================================================================================
| 1 位编码规则 | 37bit 原始数字 | 15bit 原始数字加当前时间加密生成的密文 | 11bit 当天时间分钟信息 |
+===========================================================================================
9.2 加密流程
输入参数为原始正整数
用 ChaCha20 算法对原始正整数加密,得到一个 512 字节的随机数
将步骤 2 生成的随机数取前 256 字节作为 HMAC 算法的密钥
将步骤 2 生成的随机数的后 256 字节、原始正整数、当前时间序列拼成字节数组
用步骤 3 生成的 HMAC 对步骤 4 生成的字节数组进行加密
将原始正整数编码为 37bit,步骤 5 生成的字节数组取前 15bit,当天时间分钟信息编码为 11bit,拼成 63bit 报文
步骤 2 得到的随机数按字节在求和,然后对 9 取模加 1
根据步骤 7 的结果,得到一套 base10 的编码方式,对步骤 6 产生的 56bit 激活码主体进行编码,得到 19 位的 10 进制数
步骤 7 得到的结果进行 base10 编码得到一位 10 进制数
将步骤 9 和步骤 8 得到的结果拼在一起,得到 20 位的加密字符串
9.3 解密流程
和加密流程相反。
如果为非法字符串或者已经过期,解密方法则返回 null。
9.4 安全
如果对加密数字进行暴力破解,校验通过的概率很小。
内置 10 套编码方式,
9.5 使用方式
加密
private final TimeNumberHidingGenerator generator = createGenerator();
private TimeNumberHidingGenerator createGenerator() {
String alphabetsStr = "0381592647,1270856349,4685109372,3904682157,7316492805,3645927810,1803756249,6153940728,2905437861,7968012435";
return new TimeNumberHidingGenerator("abcdefj11p23710837e]q222rqrqweqe",
"!@#$7¥yt", 10, alphabetsStr);
}
@Test
public void generate() {
long originNumber = 99999999999L;
String hidingStr = generator.generate(originNumber);
Assert.assertEquals(20, hidingStr.length());
Assert.assertTrue(isCharValid(hidingStr));
originNumber = 15052331988L;
hidingStr = generator.generate(originNumber);
Assert.assertEquals(20, hidingStr.length());
Assert.assertTrue(isCharValid(hidingStr));
}
解密
@Test
public void parse() {
Long originNumber = 14825847997L;
String hidingStr = generator.generate(originNumber);
Assert.assertEquals(originNumber, generator.parse(hidingStr));
originNumber = 6L;
hidingStr = generator.generate(originNumber);
Assert.assertEquals(originNumber, generator.parse(hidingStr));
}