node-ssh vs ssh2 vs ssh2-promise vs ssh2-sftp-client
Node.js 中的 SSH 与 SFTP 客户端库选型
node-sshssh2ssh2-promisessh2-sftp-client

Node.js 中的 SSH 与 SFTP 客户端库选型

node-sshssh2ssh2-promisessh2-sftp-client 都是用于在 Node.js 环境中建立 SSH 或 SFTP 连接的 npm 包。它们允许开发者远程执行命令、传输文件或管理服务器资源。其中 ssh2 是底层核心实现,其他包多基于它进行封装以提供更易用的 API。这些库广泛应用于自动化部署、远程运维脚本、CI/CD 流水线及文件同步等场景。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
node-ssh099678.3 kB601 年前MIT
ssh205,7631.12 MB928 个月前-
ssh2-promise0151169 kB20-MIT
ssh2-sftp-client0918247 kB120 天前Apache-2.0

Node.js SSH 客户端库深度对比:node-ssh、ssh2、ssh2-promise 与 ssh2-sftp-client

在 Node.js 中实现 SSH 或 SFTP 功能时,开发者常面临多个库的选择。虽然它们底层大多基于 ssh2,但抽象层级、API 设计和适用场景差异显著。本文从真实工程角度出发,深入比较 node-sshssh2ssh2-promisessh2-sftp-client 的技术细节,帮助你做出合理选型。

🔧 底层依赖与维护状态

所有四个包都直接或间接依赖于 ssh2 —— 这是由 mscdex 维护的、功能完整的纯 JavaScript SSH2 客户端与服务器实现。它是事实上的底层标准。

  • ssh2:活跃维护,提供最底层、最完整的控制能力。
  • node-ssh:基于 ssh2 封装,提供更简洁的 Promise API,持续更新。
  • ssh2-promise:对 ssh2 的 Promise 包装,但 自 2019 年起已不再维护,GitHub 仓库归档,npm 页面无明确弃用标记但实际应视为废弃。
  • ssh2-sftp-client:专精 SFTP 场景,基于 ssh2 构建,仅暴露 SFTP 相关操作,维护活跃。

⚠️ 注意:ssh2-promise 不应再用于新项目。其 API 已过时,且无法获得安全更新或 bug 修复。

📡 基础连接与命令执行

ssh2(底层原生)

ssh2 使用事件驱动模型,需手动处理连接、认证、会话创建等流程。适合需要精细控制连接生命周期的场景。

const { Client } = require('ssh2');

const conn = new Client();
conn
  .on('ready', () => {
    conn.exec('uptime', (err, stream) => {
      if (err) throw err;
      stream
        .on('close', () => conn.end())
        .on('data', (data) => console.log(data.toString()));
    });
  })
  .connect({
    host: 'example.com',
    username: 'user',
    password: 'pass'
  });

node-ssh(高层封装)

提供链式 Promise API,自动管理连接和会话,代码简洁。

const { NodeSSH } = require('node-ssh');

const ssh = new NodeSSH();
await ssh.connect({
  host: 'example.com',
  username: 'user',
  password: 'pass'
});

const result = await ssh.execCommand('uptime');
console.log(result.stdout);
await ssh.dispose();

ssh2-promise(已废弃)

曾提供类似 node-ssh 的 Promise 接口,但因长期未维护,不推荐使用。示例仅作历史参考:

// ❌ 不要在新项目中使用
const SSH = require('ssh2-promise');
const ssh = new SSH({
  host: 'example.com',
  username: 'user',
  password: 'pass'
});

const res = await ssh.exec('uptime');
console.log(res); // 返回 stdout/stderr 对象

ssh2-sftp-client(SFTP 专用)

不支持普通 shell 命令执行,仅用于文件传输。若需执行命令,必须搭配其他库。

// ❌ 无法执行 exec('uptime')
const Client = require('ssh2-sftp-client');
const sftp = new Client();

await sftp.connect({
  host: 'example.com',
  username: 'user',
  password: 'pass'
});

// 只能做文件操作
await sftp.list('/remote/path');
await sftp.end();

📁 文件传输能力对比

普通文件上传/下载

ssh2 需通过 sftp() 方法获取 SFTP 会话,再调用 fastPut/fastGet 等方法:

conn.on('ready', () => {
  conn.sftp((err, sftp) => {
    if (err) throw err;
    sftp.fastPut('local.txt', '/remote.txt', (err) => {
      if (err) throw err;
      conn.end();
    });
  });
});

node-ssh 提供 putFile/getFile 等高层方法,自动处理 SFTP 会话:

await ssh.putFile('local.txt', '/remote.txt');
await ssh.getFile('downloaded.txt', '/remote.txt');

ssh2-sftp-client 是专为 SFTP 设计的,API 更丰富(如 uploadDir, downloadDir, append 等):

await sftp.upload('local.txt', '/remote.txt');
await sftp.download('/remote.txt', 'downloaded.txt');

💡 提示:node-sshput 方法支持 Buffer、Stream 或本地路径;ssh2-sftp-client 同样支持多种输入源,但对目录递归操作支持更完善。

🔄 流式传输与大文件处理

处理大文件时,流式传输可避免内存溢出。

ssh2 原生支持 Stream:

const readStream = fs.createReadStream('large-file.zip');
conn.sftp((err, sftp) => {
  const writeStream = sftp.createWriteStream('/remote/large-file.zip');
  readStream.pipe(writeStream);
});

node-ssh 支持传入 ReadableStream:

const stream = fs.createReadStream('large-file.zip');
await ssh.put(stream, '/remote/large-file.zip');

ssh2-sftp-client 也支持 Stream,并额外提供进度回调:

await sftp.upload(
  fs.createReadStream('large-file.zip'),
  '/remote/large-file.zip',
  { progress: (progress) => console.log(`Uploaded: ${progress}`) }
);

🛡️ 错误处理与连接管理

ssh2 要求开发者手动监听 errorendclose 等事件,容易遗漏导致内存泄漏。

node-sshssh2-sftp-client 在内部封装了错误传播,Promise 会 reject 异常,且提供 dispose()end() 方法确保资源释放。

// node-ssh
try {
  await ssh.execCommand('invalid-command');
} catch (err) {
  console.error('Command failed:', err.message);
} finally {
  await ssh.dispose();
}

// ssh2-sftp-client
try {
  await sftp.list('/nonexistent');
} catch (err) {
  console.error('SFTP error:', err.message);
} finally {
  await sftp.end();
}

🧩 适用场景总结

场景推荐库理由
通用 SSH + SFTP 操作,追求开发效率node-sshAPI 简洁,Promise 友好,覆盖命令执行与文件传输
仅需 SFTP 文件操作,需高级功能(如进度、目录同步)ssh2-sftp-client专注 SFTP,功能完整,错误处理健壮
需要底层控制(如自定义加密算法、代理跳转、多通道复用)ssh2提供最细粒度的 SSH 协议控制
新项目避免 ssh2-promise已停止维护,存在安全与兼容风险

✅ 最佳实践建议

  1. 优先考虑 node-ssh:对于大多数前端团队涉及的自动化部署、远程脚本执行、简单文件同步等任务,它提供了最佳平衡 —— 简单、安全、功能完整。
  2. 纯 SFTP 场景选 ssh2-sftp-client:如果你只做文件上传下载、目录同步,它的 API 更贴合需求,且支持进度监控等实用功能。
  3. 仅在必要时用 ssh2:除非你需要实现非标准 SSH 行为(如 SSH 代理链、自定义认证方式),否则高层封装已足够。
  4. 永远不要用 ssh2-promise:即使代码能跑,长期维护成本和安全风险极高。

💡 结语

这四个库并非竞争关系,而是不同抽象层级的工具。ssh2 是引擎,node-sshssh2-sftp-client 是为不同用途定制的车身。根据你的具体需求 —— 是要一辆全能轿车(node-ssh),还是一辆专业货车(ssh2-sftp-client),或是自己造车(ssh2)—— 做出选择即可。记住:简单场景用高层封装,复杂协议需求才下沉到底层

如何选择: node-ssh vs ssh2 vs ssh2-promise vs ssh2-sftp-client

  • node-ssh:

    选择 node-ssh 如果你需要一个兼顾 SSH 命令执行和 SFTP 文件传输的高层封装库,且希望使用简洁的 Promise API。它适合大多数通用场景,如自动化部署、远程脚本运行和简单文件同步,能显著减少样板代码并自动管理连接资源。

  • ssh2:

    选择 ssh2 如果你需要对 SSH 连接进行精细控制,例如实现非标准认证方式、自定义加密算法、SSH 代理跳转或多通道复用。它提供最完整的底层能力,但要求开发者手动处理事件、错误和资源释放,适合高级用户或特殊协议需求。

  • ssh2-promise:

    不要在新项目中使用 ssh2-promise。该包自 2019 年起已停止维护,GitHub 仓库已归档,存在安全漏洞和兼容性风险。即使其 Promise API 曾经方便,也应优先评估 node-sshssh2-sftp-client 等活跃替代方案。

  • ssh2-sftp-client:

    选择 ssh2-sftp-client 如果你的需求仅限于 SFTP 文件操作(如上传、下载、目录同步),且需要高级功能如进度回调、流式传输或递归目录处理。它不支持普通 shell 命令执行,但在纯文件传输场景下比通用库更专注、功能更完整。

node-ssh的README

Node-SSH - SSH2 with Promises

Node-SSH is an extremely lightweight Promise wrapper for ssh2.

Installation

$ npm install node-ssh # If you're using npm
$ yarn add node-ssh # If you're using Yarn

Example

const fs = require('fs')
const path = require('path')
const {NodeSSH} = require('node-ssh')

const ssh = new NodeSSH()

ssh.connect({
  host: 'localhost',
  username: 'steel',
  privateKeyPath: '/home/steel/.ssh/id_rsa'
})

// or with inline privateKey

ssh.connect({
  host: 'localhost',
  username: 'steel',
  privateKey: Buffer.from('...')
})
.then(function() {
  // Local, Remote
  ssh.putFile('/home/steel/Lab/localPath/fileName', '/home/steel/Lab/remotePath/fileName').then(function() {
    console.log("The File thing is done")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Array<Shape('local' => string, 'remote' => string)>
  ssh.putFiles([{ local: '/home/steel/Lab/localPath/fileName', remote: '/home/steel/Lab/remotePath/fileName' }]).then(function() {
    console.log("The File thing is done")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Local, Remote
  ssh.getFile('/home/steel/Lab/localPath', '/home/steel/Lab/remotePath').then(function(Contents) {
    console.log("The File's contents were successfully downloaded")
  }, function(error) {
    console.log("Something's wrong")
    console.log(error)
  })
  // Putting entire directories
  const failed = []
  const successful = []
  ssh.putDirectory('/home/steel/Lab', '/home/steel/Lab', {
    recursive: true,
    concurrency: 10,
    // ^ WARNING: Not all servers support high concurrency
    // try a bunch of values and see what works on your server
    validate: function(itemPath) {
      const baseName = path.basename(itemPath)
      return baseName.substr(0, 1) !== '.' && // do not allow dot files
             baseName !== 'node_modules' // do not allow node_modules
    },
    tick: function(localPath, remotePath, error) {
      if (error) {
        failed.push(localPath)
      } else {
        successful.push(localPath)
      }
    }
  }).then(function(status) {
    console.log('the directory transfer was', status ? 'successful' : 'unsuccessful')
    console.log('failed transfers', failed.join(', '))
    console.log('successful transfers', successful.join(', '))
  })
  // Command
  ssh.execCommand('hh_client --json', { cwd:'/var/www' }).then(function(result) {
    console.log('STDOUT: ' + result.stdout)
    console.log('STDERR: ' + result.stderr)
  })
  // Command with escaped params
  ssh.exec('hh_client', ['--json'], { cwd: '/var/www', stream: 'stdout', options: { pty: true } }).then(function(result) {
    console.log('STDOUT: ' + result)
  })
  // With streaming stdout/stderr callbacks
  ssh.exec('hh_client', ['--json'], {
    cwd: '/var/www',
    onStdout(chunk) {
      console.log('stdoutChunk', chunk.toString('utf8'))
    },
    onStderr(chunk) {
      console.log('stderrChunk', chunk.toString('utf8'))
    },
  })
})

API

// API reference in Typescript typing format:
import SSH2, {
  AcceptConnection,
  Channel,
  ClientChannel,
  ConnectConfig,
  ExecOptions,
  Prompt,
  PseudoTtyOptions,
  RejectConnection,
  SFTPWrapper,
  ShellOptions,
  TcpConnectionDetails,
  TransferOptions,
  UNIXConnectionDetails,
} from 'ssh2'
import stream from 'stream'

// ^ You do NOT need to import these package, these are here for reference of where the
// types are coming from.

export type Config = ConnectConfig & {
  password?: string
  privateKey?: string
  privateKeyPath?: string
  tryKeyboard?: boolean
  onKeyboardInteractive?: (
    name: string,
    instructions: string,
    lang: string,
    prompts: Prompt[],
    finish: (responses: string[]) => void,
  ) => void
}
export interface SSHExecCommandOptions {
  cwd?: string
  stdin?: string | stream.Readable
  execOptions?: ExecOptions
  encoding?: BufferEncoding
  noTrim?: boolean
  onChannel?: (clientChannel: ClientChannel) => void
  onStdout?: (chunk: Buffer) => void
  onStderr?: (chunk: Buffer) => void
}
export interface SSHExecCommandResponse {
  stdout: string
  stderr: string
  code: number | null
  signal: string | null
}
export interface SSHExecOptions extends SSHExecCommandOptions {
  stream?: 'stdout' | 'stderr' | 'both'
}
export interface SSHPutFilesOptions {
  sftp?: SFTPWrapper | null
  concurrency?: number
  transferOptions?: TransferOptions
}
export interface SSHGetPutDirectoryOptions extends SSHPutFilesOptions {
  tick?: (localFile: string, remoteFile: string, error: Error | null) => void
  validate?: (path: string) => boolean
  recursive?: boolean
}
export type SSHMkdirMethod = 'sftp' | 'exec'
export type SSHForwardInListener = (
  details: TcpConnectionDetails,
  accept: AcceptConnection<ClientChannel>,
  reject: RejectConnection,
) => void
export interface SSHForwardInDetails {
  dispose(): Promise<void>
  port: number
}
export type SSHForwardInStreamLocalListener = (
  info: UNIXConnectionDetails,
  accept: AcceptConnection,
  reject: RejectConnection,
) => void
export interface SSHForwardInStreamLocalDetails {
  dispose(): Promise<void>
}
export class SSHError extends Error {
  code: string | null
  constructor(message: string, code?: string | null)
}

export class NodeSSH {
  connection: SSH2.Client | null

  connect(config: Config): Promise<this>
  isConnected(): boolean
  requestShell(options?: PseudoTtyOptions | ShellOptions | false): Promise<ClientChannel>
  withShell(
    callback: (channel: ClientChannel) => Promise<void>,
    options?: PseudoTtyOptions | ShellOptions | false,
  ): Promise<void>
  requestSFTP(): Promise<SFTPWrapper>
  withSFTP(callback: (sftp: SFTPWrapper) => Promise<void>): Promise<void>
  execCommand(givenCommand: string, options?: SSHExecCommandOptions): Promise<SSHExecCommandResponse>
  exec(
    command: string,
    parameters: string[],
    options?: SSHExecOptions & {
      stream?: 'stdout' | 'stderr'
    },
  ): Promise<string>
  exec(
    command: string,
    parameters: string[],
    options?: SSHExecOptions & {
      stream: 'both'
    },
  ): Promise<SSHExecCommandResponse>
  mkdir(path: string, method?: SSHMkdirMethod, givenSftp?: SFTPWrapper | null): Promise<void>
  getFile(
    localFile: string,
    remoteFile: string,
    givenSftp?: SFTPWrapper | null,
    transferOptions?: TransferOptions | null,
  ): Promise<void>
  putFile(
    localFile: string,
    remoteFile: string,
    givenSftp?: SFTPWrapper | null,
    transferOptions?: TransferOptions | null,
  ): Promise<void>
  putFiles(
    files: {
      local: string
      remote: string
    }[],
    { concurrency, sftp: givenSftp, transferOptions }?: SSHPutFilesOptions,
  ): Promise<void>
  putDirectory(
    localDirectory: string,
    remoteDirectory: string,
    { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
  ): Promise<boolean>
  getDirectory(
    localDirectory: string,
    remoteDirectory: string,
    { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
  ): Promise<boolean>
  forwardIn(remoteAddr: string, remotePort: number, onConnection?: SSHForwardInListener): Promise<SSHForwardInDetails>
  forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number): Promise<Channel>
  forwardInStreamLocal(
    socketPath: string,
    onConnection?: SSHForwardInStreamLocalListener,
  ): Promise<SSHForwardInStreamLocalDetails>
  forwardOutStreamLocal(socketPath: string): Promise<Channel>
  dispose(): void
}

Typescript support

node-ssh requires extra dependencies while working under Typescript. Please install them as shown below

yarn add --dev @types/ssh2
# OR
npm install --save-dev @types/ssh2

If you're still running into issues, try adding these to your tsconfig.json

{
  "compilerOptions": {
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  }
}

Keyboard-interactive user authentication

In some cases you have to enable keyboard-interactive user authentication. Otherwise you will get an All configured authentication methods failed error.

Example:

const password = 'test'

ssh.connect({
  host: 'localhost',
  username: 'steel',
  port: 22,
  password,
  tryKeyboard: true,
})

// Or if you want to add some custom keyboard-interactive logic:

ssh.connect({
  host: 'localhost',
  username: 'steel',
  port: 22,
  tryKeyboard: true,
  onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish) {
    if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) {
      finish([password])
    }
  }
})

For further information see: https://github.com/mscdex/ssh2/issues/604

License

This project is licensed under the terms of MIT license. See the LICENSE file for more info.