如何在Node.js控制台中隐藏密码?

55

我想要隐藏密码输入框。我在stackoverflow上看到了很多答案,但如果我按退格键无法验证值,则条件返回false。

我尝试过几种覆盖函数的解决方案,但是如果我按退格键,我会遇到缓冲区问题,我会得到不可见字符\b

我按下: "A"、退格键、"B",我在我的缓冲区中得到了这个:"\\u0041\\u0008\\u0042"(toString() ='A\bB'),而不是"B"。

我有:

var readline = require('readline');

var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question("password : ", function(password) {
    console.log("Your password : " + password);
});
13个回答

61

可以通过在readline中拦截输出流来处理此问题,就像npm上的read项目(https://github.com/isaacs/read/blob/master/lib/read.js)中所做的那样:

var readline = require('readline');
var Writable = require('stream').Writable;

var mutableStdout = new Writable({
  write: function(chunk, encoding, callback) {
    if (!this.muted)
      process.stdout.write(chunk, encoding);
    callback();
  }
});

mutableStdout.muted = false;

var rl = readline.createInterface({
  input: process.stdin,
  output: mutableStdout,
  terminal: true
});

rl.question('Password: ', function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

mutableStdout.muted = true;

1
虽然这段代码可以正常工作,但我想知道它是否“合适”。您没有将回调传递给stdout.write,而是无条件地调用它并抑制任何错误。不幸的是,传递回调会使其停止工作,并需要使用超时为0来进行静音(这样看起来像是竞争条件)。 - Marnes
不要错过 Enter 键:*if (!this.muted || ['\n', '\r\n'].includes(chunk.toString())) ...* - Mir-Ismaili

53

覆盖应用程序读取行界面的_writeToOutput方法:https://github.com/nodejs/node/blob/v9.5.0/lib/readline.js#L291

要隐藏密码输入,您可以使用:

第一解决方案:"password : [=-]"

此解决方案在按下键时具有动画效果:

password : [-=]
password : [=-]

该代码:

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.stdoutMuted = true;

rl.query = "Password : ";
rl.question(rl.query, function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

rl._writeToOutput = function _writeToOutput(stringToWrite) {
  if (rl.stdoutMuted)
    rl.output.write("\x1B[2K\x1B[200D"+rl.query+"["+((rl.line.length%2==1)?"=-":"-=")+"]");
  else
    rl.output.write(stringToWrite);
};

这个序列 "\x1B[2K\x1BD" 使用了两个转义序列:

  • Esc [2K : 清除整行。
  • Esc D : 将窗口向上移动/滚动一行。

了解更多,请阅读:http://ascii-table.com/ansi-escape-sequences-vt-100.php

第二种解决方案: "密码:****"

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.stdoutMuted = true;

rl.question('Password: ', function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

rl._writeToOutput = function _writeToOutput(stringToWrite) {
  if (rl.stdoutMuted)
    rl.output.write("*");
  else
    rl.output.write(stringToWrite);
};

您可以使用以下方法清除历史记录:

rl.history = rl.history.slice(1);

1
\033[2K\033D,使用两个序列“Esc[2K”和“EscD”。首先清除整行,第二个将窗口向上移动/滚动一行。 - user2226755
3
如果您正在使用严格模式,请将'\033[2K\033[200D'更改为'\x1B[2K\x1B[200D'。这只是将已弃用的八进制转义替换为等效的十六进制转义。 - Jeff Thompson
2
第二个解决方案的回车键行为很奇怪。它会打印“Password:”,然后如果您输入'a'并按回车键,则第一行输出的总内容为“Password: **”而没有换行符。任何随后的控制台日志记录都将在同一行右侧记录。我通过在rl._writeToOutput中测试换行符来修复它,如下所示:if (rl.stdoutMuted && stringToWrite != '\r\n' && stringToWrite != '\n' && stringToWrite != '\r') - chrwoizi
1
@chrwoizi 是的,当擦除密码时,界面会重新呈现整行文本,这也是正确的。请尝试使用以下代码: if (password) { rl._writeToOutput = (s) => { const v = s.split(question) if (v.length == '2') { rl.output.write(question) rl.output.write('*'.repeat(v[1].length)) } else { rl.output.write('*') } } }(https://github.com/artdecocode/reloquent/blob/master/src/lib/ask.js#L23)或 NPM 包 reloquent。 - zavr
4
在 readline 中使用一个类/接口的私有(内部)部分(这里是 __writeToOutput)并不是一个好主意。类型只能承诺维护它们的公共接口。 - Alex Sed
显示剩余9条评论

16

readline-sync 结合 ttys 可以让你在密码提示符中输入字符时不显示出来。感谢提供的链接! :) - greduan

10

另一种使用 readline 的方法:

var readline = require("readline"),
    rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });

rl.input.on("keypress", function (c, k) {
  // get the number of characters entered so far:
  var len = rl.line.length;
  // move cursor back to the beginning of the input:
  readline.moveCursor(rl.output, -len, 0);
  // clear everything to the right of the cursor:
  readline.clearLine(rl.output, 1);
  // replace the original input with asterisks:
  for (var i = 0; i < len; i++) {
    rl.output.write("*");
  }
});

rl.question("Enter your password: ", function (pw) {
  // pw == the user's input:
  console.log(pw);
  rl.close();
});

1
展示了一个好的解决方案,简单而有效,并且不需要安装模块或覆盖私有API。很容易添加一个标志来切换keypress处理,具体取决于是否应该隐藏问题响应。 - Shaun
1
这个解决方案对我不起作用,因为每个字符在被替换之前都会短暂地显示出来。 - Kian
建议使用 rl.output.write('*'.repeat(len)); 代替 for 循环。 - Ben
1
这仍然将输入写入标准输出 - 在清除并写入 * 之前,根据操作系统/设置可能会被拦截/出错,因此可能会泄漏输出,而 _writeToOutput 虽然难看但在写入之前拦截它。 - Michael B.

8

我的解决方案,从网上搜集而来:

import readline from 'readline';

export const hiddenQuestion = query => new Promise((resolve, reject) => {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  const stdin = process.openStdin();
  process.stdin.on('data', char => {
    char = char + '';
    switch (char) {
      case '\n':
      case '\r':
      case '\u0004':
        stdin.pause();
        break;
      default:
        process.stdout.clearLine();
        readline.cursorTo(process.stdout, 0);
        process.stdout.write(query + Array(rl.line.length + 1).join('*'));
        break;
    }
  });
  rl.question(query, value => {
    rl.history = rl.history.slice(1);
    resolve(value);
  });
});

使用方法如下:
// import { hiddenQuestion } from './hidden-question.js';

const main = async () => {
  console.log('Enter your password and I will tell you your password! ');
  const password = await hiddenQuestion('> ');
  console.log('Your password is "' + password + '". ');
};

main().catch(error => console.error(error));

1
对于多个提示的脚本,为了避免每次调用hiddenQuestion时打印额外的新行,您需要在解决承诺之前放置一个rl.close() - n8jadams
此外,在承诺解决之前,您可能希望使用 process.stdin.removeListener("data", onChar);(这假定处理程序已提取到一个名为 onChar 的函数中)。 - Izhaki

5

我想补充标记解决方案#2。

当我们检测到行末时,我认为我们应该删除事件处理程序,而不仅仅是使用stdin.pause()。如果您在其他地方等待rl.question/rl.prompt的情况下,这可能会出现问题。 在这些情况下,如果使用了stdin.pause(),它将只是退出程序而不给任何错误,并且可能非常烦人进行调试。

function hidden(query, callback) {
    var stdin = process.openStdin();
    var onDataHandler = function(char) {
        char = char + "";
        switch (char) {
          case "\n": case "\r": case "\u0004":
            // Remove this handler
            stdin.removeListener("data",onDataHandler); 
            break;//stdin.pause(); break;
          default:
            process.stdout.write("\033[2K\033[200D" + query + Array(rl.line.length+1).join("*"));
          break;
        }
    }
    process.stdin.on("data", onDataHandler);

    rl.question(query, function(value) {
      rl.history = rl.history.slice(1);
      callback(value);
    });
}

3
此外,您可以使用tty.ReadStream,将process.stdin的模式更改为禁用回显输入字符。
let read_Line_Str = "";
let credentials_Obj = {};
process.stdin.setEncoding('utf8');
process.stdin.setRawMode( true );
process.stdout.write( "Enter password:" ); 
process.stdin.on( 'readable', () => {
  const chunk = process.stdin.read();
  if ( chunk !== null ) {
    read_Line_Str += chunk;
    if( 
      chunk == "\n" ||
      chunk == "\r" ||
      chunk == "\u0004"
    ){
      process.stdout.write( "\n" );
      process.stdin.setRawMode( false );
      process.stdin.emit('end'); /// <- this invokes on.end
    }else{
      // providing visual feedback
      process.stdout.write( "*" );  
    }  
  }else{
    //console.log( "readable data chunk is null|empty" );
  }
} );
process.stdin.on( 'end', () => {
  credentials_Obj.user = process.env.USER;
  credentials_Obj.host = 'localhost';
  credentials_Obj.database = process.env.USER;
  credentials_Obj.password = read_Line_Str.trim();
  credentials_Obj.port = 5432;
  //
  connect_To_DB( credentials_Obj );
} );

2

一个经过Promise封装的TypeScript原生版本:

这也将处理多个question调用(正如@jeffrey-woo指出的)。我选择不使用*替换输入,因为它不太符合Unix风格,而且如果输入过快,有时会出现故障。

import readline from 'readline';

export const question = (question: string, options: { hidden?: boolean } = {}) =>
  new Promise<string>((resolve, reject) => {
    const input = process.stdin;
    const output = process.stdout;

    type Rl = readline.Interface & { history: string[] };
    const rl = readline.createInterface({ input, output }) as Rl;

    if (options.hidden) {
      const onDataHandler = (charBuff: Buffer) => {
        const char = charBuff + '';
        switch (char) {
          case '\n':
          case '\r':
          case '\u0004':
            input.removeListener('data', onDataHandler);
            break;
          default:
            output.clearLine(0);
            readline.cursorTo(output, 0);
            output.write(question);
            break;
        }
      };
      input.on('data', onDataHandler);
    }

    rl.question(question, (answer) => {
      if (options.hidden) rl.history = rl.history.slice(1);
      rl.close();
      resolve(answer);
    });
  });

使用方法:

(async () => {
  const hiddenValue = await question('This will be hidden', { hidden: true });
  const visibleValue = await question('This will be visible');
  console.log('hidden value', hiddenValue);
  console.log('visible value', visibleValue);
});

1
你可以使用提示模块,如此处所建议。
const prompt = require('prompt');

const properties = [
    {
        name: 'username', 
        validator: /^[a-zA-Z\s\-]+$/,
        warning: 'Username must be only letters, spaces, or dashes'
    },
    {
        name: 'password',
        hidden: true
    }
];

prompt.start();

prompt.get(properties, function (err, result) {
    if (err) { return onErr(err); }
    console.log('Command-line input received:');
    console.log('  Username: ' + result.username);
    console.log('  Password: ' + result.password);
});

function onErr(err) {
    console.log(err);
    return 1;
}

我无法在“async”主函数中正确运行此代码,这个实现方式只适用于顺序执行吗? - Huge

0

这是我的解决方案,它不需要任何外部库(除了readline)或大量的代码。

// turns off echo, but also doesn't process backspaces
// also captures ctrl+c, ctrl+d
process.stdin.setRawMode(true); 

const rl = require('readline').createInterface({input: process.stdin});
rl.on('close', function() { process.exit(0); }); // on ctrl+c, doesn't work? :(
rl.on('line', function(line) {
    if (/\u0003\.test(line)/) process.exit(0); // on ctrl+c, but after return :(
    // process backspaces
    while (/\u007f/.test(line)) {
        line = line.replace(/[^\u007f]\u007f/, '').replace(/^\u007f+/, '');
    }

    // do whatever with line
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接