从登录到支付:手把手用Node.js crypto打造你的应用安全防线(附JWT签名实战)
从登录到支付:手把手用Node.js crypto打造你的应用安全防线(附JWT签名实战)
在数字化业务高速发展的今天,安全防线已成为每个开发者必须重视的战场。想象这样一个场景:用户输入密码登录系统,后端验证身份后发放访问令牌,随后用户在支付环节提交银行卡信息——这条看似简单的业务链路,实际上每一步都暗藏安全风险。作为全栈开发者,我们该如何用Node.js构建可靠的安全体系?
本文将带你深入Node.js crypto模块,从密码存储到数据传输,打造一套完整的业务安全解决方案。不同于单纯讲解API用法的教程,我们将聚焦三个核心问题: 为什么需要这些安全措施 、 如何选择合适的技术方案 ,以及 怎样组合多种加密技术形成防御体系 。通过一个模拟电商支付的完整案例,你会掌握:
- 密码存储的最佳实践(bcrypt/scrypt)
- JWT签名与验证的防篡改机制
- 敏感数据传输的混合加密策略
- 常见安全漏洞的防御方案
1. 密码安全:从存储到验证
用户密码是系统安全的第一道闸门。我们先看一个典型的反面案例:
// 危险!明文存储密码
const users = {
'admin': '123456'
}
这种存储方式一旦数据库泄露,攻击者就能直接获取所有用户凭证。正确的做法是使用 单向哈希函数 处理密码,确保即使数据泄露也无法还原原始密码。
1.1 为什么需要加盐哈希
纯哈希(如MD5/SHA256)仍存在风险:
- 彩虹表攻击 :预先计算常见密码的哈希值进行反向查询
- 相同密码问题 :用户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实现仍有改进空间:
-
算法选择 :
- 避免使用
none算法 - 高安全场景使用RS256(非对称签名)
- 避免使用
-
有效期控制 :
{ "exp": 1516239022, // 过期时间 "nbf": 1516239022 // 生效时间 } -
敏感信息 :不要在payload中存储密码等敏感数据
-
密钥轮换 :定期更换签名密钥
3. 支付环节的混合加密方案
当用户进入支付流程,需要传输信用卡号等敏感信息时,单一加密方式往往存在局限。我们采用 非对称加密+对称加密 的混合方案。
3.1 密钥交换流程
- 客户端生成临时对称密钥(sessionKey)
- 使用服务器公钥加密sessionKey
- 服务器用私钥解密获取sessionKey
- 后续通信使用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');
}
});
关键安全措施总结:
-
传输层 :
- HTTPS强制加密
- 敏感数据二次加密
-
存储层 :
- 密码加盐哈希
- 日志脱敏处理
-
会话层 :
- HttpOnly + Secure Cookie
- JWT短期有效
-
业务层 :
- 关键操作审计
- 输入输出验证
在项目初期就植入这些安全实践,远比后期修补漏洞成本更低。建议开发者建立安全检查清单,在代码审查时重点关注安全相关实现。
更多推荐
所有评论(0)