在Node.js中保护随机令牌


273

这个问题中, Erik需要在Node.js中生成一个安全的随机令牌。有crypto.randomBytes生成随机缓冲区的方法。但是,node中的base64编码不是网址安全的,它包含/+而不是-_。因此,我发现的生成此类令牌的最简单方法是

require('crypto').randomBytes(48, function(ex, buf) {
    token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
});

有没有更优雅的方式?


其余的代码是什么?
Lion789

3
没有更多需要了。您想休息一下吗?
休伯特·奥格

没关系,我可以使用它,只是不确定如何将其放入,但是可以更好地理解该概念
Lion789 2013年

1
无耻的自我插入,我创建了另一个npm包:tokgen。您可以使用类似于正则表达式('a-zA-Z0-9_-')中字符类的范围语法来指定允许的字符。
Max Truxa

1
这对于任何想要特定字符串长度的人来说都很方便。3/4位用于处理基本转换。/ *返回长度为base64的编码字符串,* /函数randomString(length){return crypto.randomBytes(length * 3/4).toString('base64'); }对于那些具有字符数限制的数据库来说效果很好。
TheUnknownGeek

Answers:


353

尝试crypto.randomBytes()

require('crypto').randomBytes(48, function(err, buffer) {
  var token = buffer.toString('hex');
});

“十六进制”编码在节点v0.6.x或更高版本中有效。


3
似乎更好,谢谢!不过,“ base64-url”编码会很好。
休伯特·奥格

2
感谢您的提示,但我认为OP仅需要已标准的RFC 3548第4节“使用URL和文件名安全字母进行Base 64编码”。IMO,替换字符“足够优雅”。
natevw

8
如果您希望上述内容能满足您的要求,可以做node -e "require('crypto').randomBytes(48, function(ex, buf) { console.log(buf.toString('hex')) });"
Dmitry Minkovsky 2014年

24
而且,您始终buf.toString('base64')可以获取Base64编码的数字。
德米特里·明科夫斯基2014年

1
看到此answser下面底座64编码与URL和文件名安全字母
伊夫M.

232

如果您不是像我这样的JS专家,请使用同步选项。不得不花一些时间在如何访问内联函数变量上

var token = crypto.randomBytes(64).toString('hex');

7
同样,如果您不想嵌套所有内容。谢谢!
Michael Ozeryansky '16

2
尽管这绝对可行,但请注意,在大多数情况下,您会希望在jh的答案中展示异步选项。
Triforcey

1
const generateToken = (): Promise<string> => new Promise(resolve => randomBytes(48, (err, buffer) => resolve(buffer.toString('hex'))));
yantrab

1
@Triforcey您能解释一下为什么通常需要async选项吗?
托马斯,

2
@thomas根据硬件,可能需要一段时间才能计算出随机数据。在某些情况下,如果计算机用尽了随机数据,它只会在该位置返回一些内容。但是,在其他情况下,计算机可能会延迟返回随机数据(这实际上是您想要的),从而导致通话速度变慢。
Triforcey

80

0.使用nanoid第三方库[NEW!]

一个很小的,安全的,URL友好的,唯一的JavaScript字符串ID生成器

https://github.com/ai/nanoid

import { nanoid } from "nanoid";
const id = nanoid(48);


1.使用URL和文件名安全字母进行Base 64编码

RCF 4648的第7页介绍了如何使用URL安全性在base 64中进行编码。您可以使用现有的库(例如base64url)来完成这项工作。

该函数将是:

var crypto = require('crypto');
var base64url = require('base64url');

/** Sync */
function randomStringAsBase64Url(size) {
  return base64url(crypto.randomBytes(size));
}

用法示例:

randomStringAsBase64Url(20);
// Returns 'AXSGpLVjne_f7w5Xg-fWdoBwbfs' which is 27 characters length.

请注意,返回的字符串长度将与size参数不匹配(size!=最终长度)。


2.有限字符集的加密随机值

请注意,使用此解决方案时,生成的随机字符串不是均匀分布的。

您还可以从有限的一组字符中构建一个强大的随机字符串,如下所示:

var crypto = require('crypto');

/** Sync */
function randomString(length, chars) {
  if (!chars) {
    throw new Error('Argument \'chars\' is undefined');
  }

  var charsLength = chars.length;
  if (charsLength > 256) {
    throw new Error('Argument \'chars\' should not have more than 256 characters'
      + ', otherwise unpredictability will be broken');
  }

  var randomBytes = crypto.randomBytes(length);
  var result = new Array(length);

  var cursor = 0;
  for (var i = 0; i < length; i++) {
    cursor += randomBytes[i];
    result[i] = chars[cursor % charsLength];
  }

  return result.join('');
}

/** Sync */
function randomAsciiString(length) {
  return randomString(length,
    'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}

用法示例:

randomAsciiString(20);
// Returns 'rmRptK5niTSey7NlDk5y' which is 20 characters length.

randomString(20, 'ABCDEFG');
// Returns 'CCBAAGDGBBEGBDBECDCE' which is 20 characters length.

2
@Lexynux 解决方案1(使用URL和文件名安全字母进行Base 64编码),因为它是安全性最强的解决方案。该解决方案仅对密钥进行编码,并且不会干扰密钥生产过程。
Yves M.

谢谢你的支持。您有任何可行的例子可以与社区分享吗?会受到欢迎吗?
alexventuraio

6
注意,生成的随机字符串不是均匀分布的。一个简单的例子表明,对于长度为255,字符串长度为1的字符集,出现第一个字符的可能性是原来的两倍。
Florian Wendelborn

@Dodekeract是的,您正在谈论解决方案2。这就是为什么解决方案1更强大的原因
Yves M.


13

使用异步和等待的ES 2016标准(从节点7开始)异步执行此操作的最新正确方法如下:

const crypto = require('crypto');

function generateToken({ stringBase = 'base64', byteLength = 48 } = {}) {
  return new Promise((resolve, reject) => {
    crypto.randomBytes(byteLength, (err, buffer) => {
      if (err) {
        reject(err);
      } else {
        resolve(buffer.toString(stringBase));
      }
    });
  });
}

async function handler(req, res) {
   // default token length
   const newToken = await generateToken();
   console.log('newToken', newToken);

   // pass in parameters - adjust byte length
   const shortToken = await generateToken({byteLength: 20});
   console.log('newToken', shortToken);
}

无需任何Babel转换即可在Node 7中直接使用


我已经更新了此示例,以合并更新的传递命名参数的方法,如下所述:2ality.com/2011/11/keyword-parameters.html
real_ate

7

随机URL和文件名字符串安全(1个衬里)

Crypto.randomBytes(48).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');

一个简单的好答案!请注意,它可能以不确定的方式停止事件循环(仅在经常加载,时间敏感的系统中经常使用才有意义)。否则,执行相同的操作,但是使用randomBytes的异步版本。见nodejs.org/api/...
亚历克Thilenius

6

退房:

var crypto = require('crypto');
crypto.randomBytes(Math.ceil(length/2)).toString('hex').slice(0,length);

真好!绝对低估了解决方案。如果将“ length”重命名为“ desiredLength”并在使用前用一个值初始化它,那将是很好的:)
Florian Blum

对于任何想知道的人,对于期望的奇数长度,ceilslice调用都是必需的。即使长度不变,它们也不会改变任何东西。
赛斯

6

随着异步/等待和承诺

const crypto = require('crypto')
const randomBytes = Util.promisify(crypto.randomBytes)
const plain = (await randomBytes(24)).toString('base64').replace(/\W/g, '')

产生类似于 VjocVHdFiz5vGHnlnwqJKN0NdeHcz8eM


4

看一下real_atesES2016的方式,它更正确。

ECMAScript 2016(ES7)方式

import crypto from 'crypto';

function spawnTokenBuf() {
    return function(callback) {
        crypto.randomBytes(48, callback);
    };
}

async function() {
    console.log((await spawnTokenBuf()).toString('base64'));
};

发电机/屈服方式

var crypto = require('crypto');
var co = require('co');

function spawnTokenBuf() {
    return function(callback) {
        crypto.randomBytes(48, callback);
    };
}

co(function* () {
    console.log((yield spawnTokenBuf()).toString('base64'));
});

@Jeffpowrs确实,Java语言正在升级:)查找承诺和生成器!
K-SO的毒性在增加。

尝试等待,另一个ECMA7承诺处理程序
贾恩(Jain)

我认为您应该使ES 2016成为第一个示例,因为在大多数情况下,ES 2016正朝着“正确的方法”迈进
real_ate

我在下面添加了一个自己的答案,该答案专门针对Node(使用require而不是import)。使用导入有特定原因吗?你有通天塔吗?
real_ate

@real_ate实际上,在正式支持导入之前,我已经恢复使用CommonJS。
K-SO的毒性在增加。


2

npm模块anyid提供了灵活的API,可以生成各种字符串ID /代码。

要使用48个随机字节在A-Za-z0-9中生成随机字符串:

const id = anyid().encode('Aa0').bits(48 * 8).random().id();
// G4NtiI9OYbSgVl3EAkkoxHKyxBAWzcTI7aH13yIUNggIaNqPQoSS7SpcalIqX0qGZ

要生成固定长度的仅由随机字节填充的字母字符串:

const id = anyid().encode('Aa').length(20).random().id();
// qgQBBtDwGMuFHXeoVLpt

在内部,它用于crypto.randomBytes()生成随机数。


1

这是@Yves M.的回答逐字记录的异步版本

var crypto = require('crypto');

function createCryptoString(length, chars) { // returns a promise which renders a crypto string

    if (!chars) { // provide default dictionary of chars if not supplied

        chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    }

    return new Promise(function(resolve, reject) {

        var charsLength = chars.length;
        if (charsLength > 256) {
            reject('parm chars length greater than 256 characters' +
                        ' masks desired key unpredictability');
        }

        var randomBytes = crypto.randomBytes(length);

        var result = new Array(length);

        var cursor = 0;
        for (var i = 0; i < length; i++) {
            cursor += randomBytes[i];
            result[i] = chars[cursor % charsLength];
        }

        resolve(result.join(''));
    });
}

// --- now generate crypto string async using promise --- /

var wantStringThisLength = 64; // will generate 64 chars of crypto secure string

createCryptoString(wantStringThisLength)
.then(function(newCryptoString) {

    console.log(newCryptoString); // answer here

}).catch(function(err) {

    console.error(err);
});

1

一个简单的函数,可为您提供URL安全且具有base64编码的令牌!它是上面两个答案的组合。

const randomToken = () => {
    crypto.randomBytes(64).toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.