信用卡支付能否用 JS 加密替代 SSL?
结论
不能。用 JavaScript 在浏览器端加密信用卡号,既不能代替 HTTPS/TLS,也不适合充当支付页面的传输安全方案。
问题不在于加密算法够不够强,而在于页面、加密代码和公钥都通过未加密的 HTTP 连接传输。只要攻击者能够篡改连接,就能在加密前窃取信用卡信息,甚至替换整个支付页面。
支付页面及相关请求应全程使用 HTTPS。更推荐使用支付服务商提供的托管支付页、Hosted Fields 或令牌化方案,避免信用卡原始数据进入自己的服务器。
为什么 JS 加密无法替代 HTTPS
加密代码本身可能被替换
用户通过 HTTP 打开支付页面时,中间人可以修改服务器返回的 HTML 或 JavaScript。例如,原来的代码可能是:
const encrypted = encrypt(cardNumber, publicKey);
sendToServer(encrypted);
攻击者可以把它改成:
sendToAttacker({
cardNumber,
expiry,
cvv
});
const encrypted = encrypt(cardNumber, publicKey);
sendToServer(encrypted);
用户仍然可以正常付款,服务器也能收到正确的密文,网站因此很难立即察觉信用卡数据已经泄露。
公钥无法得到可信验证
如果浏览器通过 HTTP 获取公钥,攻击者可以将它替换成自己的公钥。此后,浏览器会把信用卡信息加密后发送给攻击者,而非网站服务器。
将公钥硬编码在 JavaScript 中也无济于事。攻击者仍然可以替换 JavaScript 文件、HTML 页面或表单提交地址。
TLS 不只负责加密,还提供服务器身份验证和数据完整性保护。一段客户端加密代码无法单独提供后两项保障。
需要保护的不只是信用卡号
支付过程还可能涉及:
- 姓名、地址、电话和邮箱
- 登录状态与会话 Cookie
- 订单编号、商品、数量和金额
- 支付结果与跳转地址
- CSRF Token 及其他安全字段
如果只加密信用卡字段,攻击者仍可能篡改订单金额、更换收款对象、劫持账户或伪造页面内容。因此,安全保护必须覆盖整个支付页面和完整的请求流程。
无法阻止页面伪造和降级攻击
攻击者可以在 HTTP 页面中插入假提示、假验证码输入框或虚假的支付失败信息,也可以将用户重定向到仿冒页面。页面上即使显示”信用卡号已加密”,也无法证明浏览器实际执行的代码可信。
推荐的解决方案
1. 全站启用 HTTPS
支付页、登录页、静态脚本、API 请求和跳转页面都应使用 HTTPS,不能只保护最终的提交接口。页面也不应混用通过 HTTP 加载的图片、脚本或 iframe。
HTTP 端口应仅用于跳转到 HTTPS:
import express from "express";
const app = express();
app.use((req, res, next) => {
if (req.secure) {
return next();
}
return res.redirect(301, `https://${req.headers.host}${req.originalUrl}`);
});
如果应用部署在可信反向代理之后,判断 req.secure 时还要考虑代理配置,不能无条件信任客户端提供的 X-Forwarded-Proto。
2. 配置 HSTS
确认网站及其子域都能稳定通过 HTTPS 访问后,可以发送 HSTS 响应头:
Strict-Transport-Security: max-age=31536000; includeSubDomains
HSTS 会让支持它的浏览器在后续访问时强制使用 HTTPS。在启用 includeSubDomains 或申请预加载前,需要先确认所有相关子域都支持 HTTPS,否则部分子域可能无法访问。
3. 使用安全的会话 Cookie
Set-Cookie: session=opaque-session-id; Secure; HttpOnly; SameSite=Lax; Path=/
各属性的作用如下:
Secure:仅通过 HTTPS 发送 Cookie。HttpOnly:降低客户端脚本直接读取会话 Cookie 的风险。SameSite:帮助防范部分跨站请求伪造攻击。
这些设置不能取代 CSRF 防护、权限检查和会话生命周期管理。
4. 优先采用令牌化支付
更稳妥的方式是由支付服务商托管信用卡输入界面,或者使用服务商提供的 Hosted Fields、受控 iframe 或官方客户端组件采集卡片信息。浏览器直接将卡片数据提交给支付服务商,网站只接收一次性 token。
典型流程如下:
Browser
├─ HTTPS ─> Payment provider: card details
└─ HTTPS ─> Merchant server: payment token + order ID
Merchant server
└─ HTTPS ─> Payment provider: create/confirm payment
服务端仍要根据自身保存的订单记录重新计算金额,不能信任浏览器提交的价格:
app.post("/payments", async (req, res) => {
const { orderId, paymentToken } = req.body;
const order = await loadOrderForCurrentUser(orderId);
if (!order || order.status !== "pending") {
return res.status(400).json({ error: "Invalid order" });
}
// 金额和币种来自服务端保存的订单,不使用客户端提交值。
const result = await paymentProvider.charge({
token: paymentToken,
amount: order.totalAmount,
currency: order.currency,
idempotencyKey: `order:${order.id}`
});
return res.json({ status: result.status });
});
这里的 paymentProvider.charge 只是架构示例,并非某个支付平台的真实 API。实际字段、幂等机制和支付确认流程应以所选服务商的官方文档为准。
还需要注意什么
HTTPS 是支付安全的必要条件,但还需要采取以下措施:
- 不记录卡号、CVV 或包含敏感字段的请求正文。
- 不要将卡片数据写入 URL、查询参数、分析事件或错误报告。
- 验证支付回调的签名,并防止回调重放。
- 使用幂等键,避免网络重试造成重复扣款。
- 在服务端校验用户、订单、金额、币种和订单状态。
- 配置合理的 Content Security Policy,降低脚本注入风险。
- 及时修补依赖和服务器,并限制后台及密钥的访问权限。
- 根据业务角色、地区和支付架构确认适用的 PCI DSS 合规范围。
浏览器端加密可以叠加在 HTTPS 之上,例如支付服务商可借此实现令牌化,或减少敏感数据的暴露范围。但它无法把不可信的 HTTP 页面变成安全的支付页面。只要执行加密的代码来自未受保护的连接,攻击者就有机会在加密前拿到明文。
备注:内容仅供参考。
