背景

在重构以前的 Java 版本的系统时发现,当时的 Java 开发者在对登陆用户的密码进行存储时采用了 BigInteger 方式存储 hash 加密后的 16 进制数字,并在最后存储时转换为了 32进制的数字。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
private String sha(String s) {
BigInteger sha = null;
byte[] bys = s.getBytes();
try {
String keySha = "SHA-1";
MessageDigest messageDigest = MessageDigest.getInstance(keySha);
messageDigest.update(bys);
sha = new BigInteger(messageDigest.digest());
} catch (Exception e) {
e.printStackTrace();
}
return sha.toString(32);
}

而前端在转换精度时发现,一旦数字大于 2 ** 53(2 的 53 次方)则会出现精度丢失问题,无法完全重构以前的 Java 代码。

原理

Java 的 BigInteger 方法可以存储超大型数字, BigInteger 可以进行无限位的存储与运算,但是实际上受你的计算机内存和计算能力影响。
而 JavaScript 中的所能表示的 最大整数Math.pow(2, 53) === 9007199254740992
在前端中进制转换的原理为,将当前进制通过 parseInt 转为 10 进制, 再通过 toString 转换为需要的进制,而 parseInt 将字符串转换成十进制数字也不能超出Math.pow(2, 53) 俗称安全数字。

比如说,当前的 16 进制数字为: 06d1f54860ed59aa95c9984b07e6a547fa690a26,常规转换为 32进制的方法为:

1
2
3
4
/// 先转换为 10 进制
const base10Num = parseInt('06d1f54860ed59aa95c9984b07e6a547fa690a26', 16)
const base32Num = Number(base10Num).toString(32)
console.log(base32Num)//r8vai30tlcs00000000000000000000

可以看到转换后的结果为:r8vai30tlcs00000000000000000000, 而以前的 Java 代码转换后的正确值为: r8vai30tlcql5e9j15gfpl58vt6i2h6,使用 js 出现了精度丢失问题。

实现方法

在 JavaScript 原生方法中,有一个 bigInt 的方法,表示可以存储超出安全数字的方法:

BigInt is a built-in object that provides a way to represent whole numbers larger than 253, which is the largest number JavaScript can reliably represent with the Number primitive.

它在某些方面类似于 Number ,但是也有几个关键的不同点:不能和 Math 对象中的方法一起使用;不能和任何 Number 实例混合运算。
虽然不能混合运算,但它支持 toString 方法,可以实现大型数字的精度转换
所以最后的解决方案为:

  • 通过 npm 包 big-number 对 16 进制的数字采取原生方法转换为 10 进制(不用 parseInt)
  • 将返回值传入 bigInt
  • 调用 toString 方法实现精度转换

详细代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const BigNumber = require('big-number')

function covertHexToDec (bigInt) {
const num = bigInt.split('')
let sum = 0
while (num.length) {
const currentNum = num.shift()
const currentLen = num.length
sum = BigNumber(sum).plus(BigNumber(16).pow(currentLen).multiply(parseInt(currentNum, 16)))
}
return sum
}

const decNum = covertHexToDec('06d1f54860ed59aa95c9984b07e6a547fa690a26').toString()
const base32Num = bigInt(decNum).toString(32)
console.log(base32Num) // "r8vai30tlcql5e9j15gfpl58vt6i2h6"

达到了我们预期的效果。
done!