You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用ReactJS/NodeJS为程序化生成的XLSM文件添加PFX证书签名?

实现XLSM文件程序化生成与PFX证书签名方案

核心原则

PFX证书包含敏感私钥,绝对不能在前端处理,签名逻辑必须放在NodeJS后端执行。React仅负责触发请求并处理已签名文件的下载。


一、NodeJS后端实现(签名+文件生成)

1. 安装依赖

npm install officegen node-forge jszip express cors

2. 完整后端代码示例

const express = require('express');
const cors = require('cors');
const officegen = require('officegen');
const JSZip = require('jszip');
const forge = require('node-forge');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(cors());

// 加载PFX证书(建议用环境变量存密码,避免硬编码)
const loadPFX = () => {
  const pfxPath = path.join(__dirname, 'cert.pfx');
  const pfxBuffer = fs.readFileSync(pfxPath);
  const pfxAsn1 = forge.asn1.fromDer(pfxBuffer.toString('binary'));
  const pfxPassword = process.env.PFX_PASSWORD || 'your-pfx-password';
  
  const pfx = forge.pkcs12.pkcs12FromAsn1(pfxAsn1, false, pfxPassword);
  
  // 提取私钥与证书链
  const privateKey = pfx.getBags({bagType: forge.pki.oids.pkcs8ShroudedKeyBag})[forge.pki.oids.pkcs8ShroudedKeyBag][0].key;
  const certificate = pfx.getBags({bagType: forge.pki.oids.certBag})[forge.pki.oids.certBag][0].cert;
  return { privateKey, certificate };
};

// 生成未签名的XLSM文件Buffer
const generateXLSM = async () => {
  return new Promise((resolve, reject) => {
    const xlsx = officegen('xlsx', { type: 'macro' });
    const sheet = xlsx.makeNewSheet();
    sheet.name = 'Data Sheet';
    sheet.data[0] = ['ID', 'Name', 'Value'];
    sheet.data[1] = ['1', 'Sample A', '100'];
    sheet.data[2] = ['2', 'Sample B', '200'];

    const chunks = [];
    xlsx.on('data', chunk => chunks.push(chunk));
    xlsx.on('end', () => resolve(Buffer.concat(chunks)));
    xlsx.on('error', reject);
  });
};

// 为XLSM添加数字签名
const signXLSM = async (xlsmBuffer, { privateKey, certificate }) => {
  const zip = await JSZip.loadAsync(xlsmBuffer);

  // 1. 创建签名所需的目录结构
  zip.folder('_xmlsignatures');
  zip.folder('_rels');

  // 2. 计算所有文件的哈希(用于签名验证)
  const fileHashes = [];
  const fileKeys = Object.keys(zip.files).filter(key => !zip.files[key].dir);
  
  for (const key of fileKeys) {
    const content = await zip.files[key].async('binary');
    const sha256 = forge.md.sha256.create();
    sha256.update(content);
    fileHashes.push({ path: key, hash: sha256.digest().toHex() });
  }

  // 3. 生成PKCS#7签名
  const p7 = forge.pkcs7.createSignedData();
  p7.content = forge.util.createBuffer(JSON.stringify(fileHashes), 'utf8');
  p7.addCertificate(certificate);
  p7.addSigner({
    key: privateKey,
    certificate: certificate,
    digestAlgorithm: forge.pki.oids.sha256,
    authenticatedAttributes: [
      { type: forge.pki.oids.contentType, value: forge.pki.oids.data },
      { type: forge.pki.oids.messageDigest },
      { type: forge.pki.oids.signingTime, value: new Date() }
    ]
  });
  p7.sign({ detached: false });

  // 4. 将签名写入ZIP包
  zip.file('_xmlsignatures/sig1.xml', forge.asn1.toDer(p7.toAsn1()).toString('binary'), { binary: true });

  // 5. 生成已签名的XLSM Buffer
  return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
};

// 导出接口
app.get('/download-signed-xlsm', async (req, res) => {
  try {
    const pfxData = loadPFX();
    const rawXLSM = await generateXLSM();
    const signedXLSM = await signXLSM(rawXLSM, pfxData);

    res.setHeader('Content-Type', 'application/vnd.ms-excel.sheet.macroEnabled.12');
    res.setHeader('Content-Disposition', 'attachment; filename="signed_report.xlsm"');
    res.send(signedXLSM);
  } catch (err) {
    console.error('Signature error:', err);
    res.status(500).send('Failed to generate signed XLSM file');
  }
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

二、React前端实现(触发下载)

1. 组件代码示例

import React from 'react';

const XLSMDownloader = () => {
  const handleDownload = async () => {
    try {
      const response = await fetch('http://localhost:3001/download-signed-xlsm');
      if (!response.ok) throw new Error('Request failed');

      const blob = await response.blob();
      const downloadUrl = window.URL.createObjectURL(blob);
      const link = document.createElement('a');
      
      link.href = downloadUrl;
      link.download = 'signed_report.xlsm';
      document.body.appendChild(link);
      link.click();

      // 清理资源
      window.URL.revokeObjectURL(downloadUrl);
      document.body.removeChild(link);
    } catch (err) {
      console.error('Download error:', err);
      alert('Failed to download signed file. Please try again.');
    }
  };

  return (
    <button onClick={handleDownload} style={{ padding: '8px 16px', cursor: 'pointer' }}>
      Download Signed XLSM
    </button>
  );
};

export default XLSMDownloader;

注意事项

  1. 证书安全:PFX密码不要硬编码到代码中,建议用环境变量(如dotenv库)管理。
  2. 签名规范:上述示例为简化实现,生产环境需严格遵循ECMA-376(Open XML)数字签名标准,完善签名结构以确保Office软件能正常识别签名。
  3. 跨域配置:后端CORS需根据实际前端域名调整,避免生产环境跨域问题。
  4. 库版本兼容:确保依赖库版本与NodeJS LTS版本兼容,避免出现API不一致问题。

内容的提问来源于stack exchange,提问作者Vidya G Nadh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 13:05:57