TL; DR
Meteor 有两种 ID。一种是 'STRING', 17位的随机字符串;一种是 'MONGO',和 MongoDB 的 ObjectId 对象几乎一样,核心是 24 位的 16 进制字符串,但是 Meteor 生成的 24 位数字是随机的,没有 MongoDB 生成的 ObjectId 那样的时间、机器、进程等信息
先讲讲 Meteor 的随机数生成。
Meteor 随机数生成 random.js
Meteor 生成随机字符串的代码 random.js 在这里。该源码头部注释文档里有说明
// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server,
// window.crypto.getRandomValues() in the browser) when available. If these
// PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically
// strong, and we seed it with various sources such as the date, Math.random,
// and window size on the client. When using crypto.getRandomValues(), our
// primitive is hexString(), from which we construct fraction(). When using
// window.crypto.getRandomValues() or alea, the primitive is fraction and we use
// that to construct hex string.
首先名词解释
PRNG: PseudoRandom Number Generator ,伪随机数生成器的缩写
Alea: 特殊名词,应该是 Aleatory (意为随机) 的简写 (源自拉丁语)
这段英文注释大致就是说 Meteor 在服务器端生成随机数使用的是 <code>crypto.getRandomBytes()</code>, 而在浏览器里使用 <code>window.crypto.getRandomValues()</code>,都是强加密型的伪随机数。如果它们生成失败,那么就是使用 Alea PRNG,不过不如前面的加密级强大,仅仅是使用日期,Math.random 随机数,甚至客户端窗口的尺寸大小等等作为种子来生成随机字符串。注意不是直接使用时间戳,而是把时间戳作为种子,所以基本还原不了时间戳。
下面是其中一段 id 生成的代码。
/**
* @name Random.id
* @summary Return a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is
* likely to be unique in the whole world.
* @locus Anywhere
* @param {Number} [n] Optional length of the identifier in characters
* (defaults to 17)
*/
RandomGenerator.prototype.id = function (charsCount) {
var self = this;
// 17 characters is around 96 bits of entropy, which is the amount of
// state in the Alea PRNG.
if (charsCount === undefined)
charsCount = 17;
return self._randomString(charsCount, UNMISTAKABLE_CHARS);
};
可以看到,Meteor 默认生成的是一个 17 位的随机字符串,通常这个字符串是在 "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz" 范围内,没有 0 和 1 是因为容易和小写字母 o 和 l 混淆;而这个字符串有 96 比特的熵,就是 2 的 96 次方,差不多是 8 万亿亿亿种可能性,所以注释里说基本上生成的随机字符串就是世界上的唯一字符串,那么具体到你的数据库里重复的概率更是非常之小的。
Meteor MongoDB 文档 ID
Meteor 有两种 ID。
- 上面提到的 17 位大小写字母加数字 2-9 构成的字符串(占 17 个 byte)
- MongoDB 的 ObjectId,这是一个 JS 对象,核心是 24 位 16 进制数(占 12 个 byte,当然整个对象不止 12 个字节)
下面来详述在 Meteor 中 MongoDB 文档两种 ID 的生成方式。
1. 'STRING' ID
这种方式就是 <code> new Mongo.Collection(name, [options])</code> 里 options 的 idGeneration 设置为 'STRING',它使用前面提到的方法随机生成 17 位字符串(占 17 个 byte)。如果使用 Robomongo 之类的软件查看自动生成的 ID,会看到如下的 _id
"_id" : "ARSqxg23yHCkb3Xqt" // 17 位大小写字母加数字的字符串
这个<code> new Mongo.Collection(name, [options])</code> 里 options 的 idGeneration 有 两种方式, 'STRING' 字符串为默认方式,另外一种是 'MONGO',我们下面会讲到。使用 'STRING' 默认方式的话,插入的文档 ID 就会是这种 17 位的字符串。
'STRING' 方式的好处就是因为是普通字符串,所以可以在 URL 里直接使用这个 _id 来定位资源。
2. ObjectId
前面提到<code> new Mongo.Collection(name, [options])</code> 里 options 的设置为 'MONGO' 的话,插入的文档 ID 就会是这种 ObjectId 对象。还有一种方式,就是直接使用 <code>new Mongo.ObjectID()</code>, 这种 ID 生成源码在这里,(老的 Meteor 版本里使用 Meteor.Collection.ObjectID)。它们都会得到如下类似的 _id,
"_id" : ObjectID("7c5a6d3249584873a3262806") // 24 位 16 进制数
这个 ObjectId 是一个对象。可以通过这个对象的 valueOf 函数获得它的字符串。这个字符串是随机的 24 位 16 进制字符串,占用 12 个字节 的存储空间,每个字节两位 16 进制的字符。
Meteor 生成的 ObjectId 不符合 MongoDB 的 ID 生成规则,见下面文档
ObjectID values created by Meteor will not have meaningful answers to their getTimestamp method, since Meteor currently constructs them fully randomly.
Meteor 生成的 ObjectId 的字符串完全是随机的。这个 24 位 16 进制数生成的代码为(也是在 random.js)
/**
* @name Random.hexString
* @summary Return a random string of `n` hexadecimal digits.
* @locus Anywhere
* @param {Number} n Length of the string
*/
RandomGenerator.prototype.hexString = function (digits) {
var self = this;
if (self.type === RandomGenerator.Type.NODE_CRYPTO) {
var numBytes = Math.ceil(digits / 2);
var bytes;
// Try to get cryptographically strong randomness. Fall back to
// non-cryptographically strong if not available.
try {
bytes = nodeCrypto.randomBytes(numBytes);
} catch (e) {
// XXX should re-throw any error except insufficient entropy
bytes = nodeCrypto.pseudoRandomBytes(numBytes);
}
var result = bytes.toString("hex");
// If the number of digits is odd, we'll have generated an extra 4 bits
// of randomness, so we need to trim the last digit.
return result.substring(0, digits);
} else {
return this._randomString(digits, "0123456789abcdef");
}
};
在 Meteor 里 MongoDB 的 ObjectId 下列规则不成立
- 前4个字节表示时间戳
- 接下来的3个字节是机器标识码, 这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值, 这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突
- 紧接的两个字节由进程 id 组成(PID), 为了在同一台机器不同的mongodb进程产生了objectId不冲突
- 最后三个字节是随机数
因为在 Meteor 里 ObjectId 的字符串是完全随机生成的,没有这些意义,所以 getTimestamp 方法得到的时间也是随机的。估计是因为 Meteor 需要在客户端生成 ID,这样时间的准确性就很难保证,也很难在客户端和服务器端实现统一,所以才采用这种随机模式。
安全性
关于防止恶意猜测 ID 来攻击服务器。
Meteor 生成的 17 位 Random ID 和 24 位的 ObjectId 都是随机的,没有时间戳这些信息,所以基本不能被猜测,比较安全。MongoDB 的 ObjectId 是不安全的,就是能够被暴力猜测到。
但是 ID 的目的不是为了安全,而只是为了生成 唯一的 ID。要让数据安全必须使用其他方法,例如在服务器端判断用户是否有权限获取该数据,设置 rate limiting 防止暴力破解等。
History
为什么 Meteor 会有不同于 MongoDB 的字符串 ID?
一名 MDG 成员在 这里 回答了。主要意思就是在 Meteor 早期(甚至早于 Meteor 这个名字的命名),为了实现 Optimistic UI,需要在客户端和服务器端生成一样的 ID,所以这样 Meteor 就必须自己生成 ID 而不是依靠 MongoDB 在插入时生成(Meteor 是依靠同样的随机种子来生成同样的 _id)。但是就算这样,Meteor 也可以生成 ObjectId 的啊?这一次问题出在 JSON,Meteor 早期使用 JSON 而不是 ESJON(Meteor v0.5.7 才开始使用),所以不能传递 JavaScript 对象而只能使用字符串了。虽然现在可以一直使用 ObjectId,但是 Meteor 貌似没有计划完全转到 ObjectId。
补充: Account 的 User Collection Doc 不能用 ObjectId 对象作为 ID。