从登录到支付:手把手用Node.js crypto打造你的应用安全防线(附JWT签名实战)

在数字化业务高速发展的今天,安全防线已成为每个开发者必须重视的战场。想象这样一个场景:用户输入密码登录系统,后端验证身份后发放访问令牌,随后用户在支付环节提交银行卡信息——这条看似简单的业务链路,实际上每一步都暗藏安全风险。作为全栈开发者,我们该如何用Node.js构建可靠的安全体系?

本文将带你深入Node.js crypto模块,从密码存储到数据传输,打造一套完整的业务安全解决方案。不同于单纯讲解API用法的教程,我们将聚焦三个核心问题: 为什么需要这些安全措施 如何选择合适的技术方案 ,以及 怎样组合多种加密技术形成防御体系 。通过一个模拟电商支付的完整案例,你会掌握:

  • 密码存储的最佳实践(bcrypt/scrypt)
  • JWT签名与验证的防篡改机制
  • 敏感数据传输的混合加密策略
  • 常见安全漏洞的防御方案

1. 密码安全:从存储到验证

用户密码是系统安全的第一道闸门。我们先看一个典型的反面案例:

// 危险!明文存储密码
const users = {
  'admin': '123456'
}

这种存储方式一旦数据库泄露,攻击者就能直接获取所有用户凭证。正确的做法是使用 单向哈希函数 处理密码,确保即使数据泄露也无法还原原始密码。

1.1 为什么需要加盐哈希

纯哈希(如MD5/SHA256)仍存在风险:

  1. 彩虹表攻击 :预先计算常见密码的哈希值进行反向查询
  2. 相同密码问题 :用户A和用户B使用相同密码时,哈希值也相同

解决方案是 加盐哈希 ——为每个密码生成随机盐值(salt),将盐与密码组合后再哈希:

const crypto = require('crypto');

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto
    .pbkdf2Sync(password, salt, 100000, 64, 'sha512')
    .toString('hex');
  return { salt, hash };
}

参数说明:

  • 100000 :迭代次数,增加计算成本
  • 64 :输出长度(字节)
  • sha512 :哈希算法

验证密码时需使用相同的盐值:

function verifyPassword(savedHash, savedSalt, inputPassword) {
  const inputHash = crypto
    .pbkdf2Sync(inputPassword, savedSalt, 100000, 64, 'sha512')
    .toString('hex');
  return inputHash === savedHash;
}

1.2 更优方案:bcrypt/scrypt

虽然PBKDF2已经比纯哈希安全,但现代方案更推荐:

算法 特点 Node.js实现
bcrypt 内置盐值,自适应成本因子 npm install bcrypt
scrypt 内存密集型,抗ASIC/GPU攻击 crypto.scrypt() 原生支持

bcrypt示例:

const bcrypt = require('bcrypt');
const saltRounds = 12;

// 哈希密码
const hash = await bcrypt.hash('user_password', saltRounds);

// 验证密码
const isMatch = await bcrypt.compare('input_password', hash);

关键决策点

  • 常规业务:bcrypt(平衡安全性与性能)
  • 高安全需求:scrypt(更好的抗暴力破解能力)

2. JWT安全:令牌的生成与验证

用户登录成功后,系统需要颁发访问令牌。JSON Web Token(JWT)已成为主流方案,但错误实现会导致严重漏洞。

2.1 JWT结构解析

一个标准的JWT包含三部分:

header.payload.signature
  • Header :指定算法和类型
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  • Payload :携带业务数据
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    
  • Signature :防篡改关键
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )
    

2.2 使用crypto实现签名

不使用第三方库,直接通过crypto模块实现:

const crypto = require('crypto');

function signJWT(payload, secret) {
  const header = {
    alg: 'HS256',
    typ: 'JWT'
  };
  
  const encodedHeader = Buffer.from(JSON.stringify(header))
    .toString('base64url');
  const encodedPayload = Buffer.from(JSON.stringify(payload))
    .toString('base64url');
  
  const signature = crypto
    .createHmac('sha256', secret)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64url');
    
  return `${encodedHeader}.${encodedPayload}.${signature}`;
}

function verifyJWT(token, secret) {
  const [header, payload, signature] = token.split('.');
  
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(`${header}.${payload}`)
    .digest('base64url');
    
  if (signature !== expectedSig) {
    throw new Error('Invalid signature');
  }
  
  return JSON.parse(Buffer.from(payload, 'base64url').toString());
}

2.3 安全增强策略

基础JWT实现仍有改进空间:

  1. 算法选择

    • 避免使用 none 算法
    • 高安全场景使用RS256(非对称签名)
  2. 有效期控制

    {
      "exp": 1516239022, // 过期时间
      "nbf": 1516239022  // 生效时间
    }
    
  3. 敏感信息 :不要在payload中存储密码等敏感数据

  4. 密钥轮换 :定期更换签名密钥

3. 支付环节的混合加密方案

当用户进入支付流程,需要传输信用卡号等敏感信息时,单一加密方式往往存在局限。我们采用 非对称加密+对称加密 的混合方案。

3.1 密钥交换流程

  1. 客户端生成临时对称密钥(sessionKey)
  2. 使用服务器公钥加密sessionKey
  3. 服务器用私钥解密获取sessionKey
  4. 后续通信使用sessionKey对称加密
// 客户端代码
const { publicKey } = getServerPublicKey(); // 预置或动态获取
const sessionKey = crypto.randomBytes(32);

const encryptedKey = crypto.publicEncrypt(
  {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
  },
  sessionKey
);

// 发送encryptedKey到服务器

3.2 数据传输加密

获取sessionKey后,双方使用AES-GCM进行加密通信:

// 加密函数
function encryptData(data, key) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([
    cipher.update(data, 'utf8'),
    cipher.final()
  ]);
  const authTag = cipher.getAuthTag();
  return {
    iv: iv.toString('hex'),
    encryptedData: encrypted.toString('hex'),
    authTag: authTag.toString('hex')
  };
}

// 解密函数
function decryptData(encrypted, key) {
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(encrypted.iv, 'hex')
  );
  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
  return Buffer.concat([
    decipher.update(Buffer.from(encrypted.encryptedData, 'hex')),
    decipher.final()
  ]).toString();
}

GCM模式相比CBC的优势:

  • 内置认证(防止篡改)
  • 不需要单独实现填充方案
  • 性能更好

4. 综合防御:常见攻击与应对

将上述技术组合后,我们还需要防范其他攻击向量:

4.1 安全头设置

Express中间件示例:

app.use((req, res, next) => {
  res.set({
    'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'Content-Security-Policy': "default-src 'self'"
  });
  next();
});

4.2 速率限制

防止暴力破解:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100 // 每IP限制100次请求
});

app.use('/login', limiter);

4.3 敏感操作审计

记录关键操作:

function auditLog(userId, action) {
  const logEntry = {
    timestamp: new Date(),
    userId,
    action,
    metadata: {
      ip: req.ip,
      userAgent: req.get('User-Agent')
    }
  };
  
  // 使用Winston等日志库存储
  logger.info(logEntry);
}

// 在支付回调中使用
app.post('/payment', (req, res) => {
  auditLog(req.user.id, 'payment_initiated');
  // ...处理逻辑
});

5. 实战:完整支付流程安全实现

让我们将这些技术整合到一个电商支付场景:

// 用户登录
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 1. 验证密码
  const user = await db.getUser(username);
  const isValid = await bcrypt.compare(password, user.passwordHash);
  
  if (!isValid) {
    auditLog(username, 'login_failed');
    return res.status(401).send('Invalid credentials');
  }
  
  // 2. 签发JWT
  const token = signJWT(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET
  );
  
  // 3. 设置HttpOnly Cookie
  res.cookie('token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  });
  
  auditLog(username, 'login_success');
  res.send({ success: true });
});

// 支付请求
app.post('/checkout', async (req, res) => {
  // 1. 验证JWT
  const token = req.cookies.token;
  try {
    const payload = verifyJWT(token, process.env.JWT_SECRET);
    
    // 2. 解密支付数据
    const { encryptedData, iv, authTag } = req.body.payment;
    const decrypted = decryptData(
      { encryptedData, iv, authTag },
      req.session.key
    );
    
    // 3. 处理支付逻辑
    const result = await processPayment({
      userId: payload.sub,
      ...JSON.parse(decrypted)
    });
    
    auditLog(payload.sub, 'payment_processed');
    res.send(result);
  } catch (err) {
    auditLog('unknown', 'payment_failed', { error: err.message });
    res.status(400).send('Invalid request');
  }
});

关键安全措施总结:

  1. 传输层

    • HTTPS强制加密
    • 敏感数据二次加密
  2. 存储层

    • 密码加盐哈希
    • 日志脱敏处理
  3. 会话层

    • HttpOnly + Secure Cookie
    • JWT短期有效
  4. 业务层

    • 关键操作审计
    • 输入输出验证

在项目初期就植入这些安全实践,远比后期修补漏洞成本更低。建议开发者建立安全检查清单,在代码审查时重点关注安全相关实现。

更多推荐