前端node-forge PKI 密钥对生成与验签流程

前端基于 node-forge 依赖进行 PKI 流程,先安装依赖 npm install --save node-forge

1 密钥对生成与下载/复制

1.1 密钥对与证书请求生成

利用 node-forgepki.rsa.generateKeyPair 方法生成密钥对,用 pki.publicKeyToPem、pki.privateKeyToPem 将公钥私钥转成 PEM 格式,便于导出和复制。

pki.createCertificationRequest 生成空的证书请求,依次写入公钥 publicKey 、写入主体信息、私钥签名,最后转成 PEM 格式。

js 复制代码
import { pki, md, util } from 'node-forge'

// 生成RSA密钥对
export function handleToGetGenerateRSAKeyPair() {
  try {
    // 配置RSA密钥对参数
    const keypair = pki.rsa.generateKeyPair({
      bits: 2048, // 密钥长度
    })

    // 输出公钥和私钥的PEM格式
    const publicKey = pki.publicKeyToPem(keypair.publicKey)
    const privateKey = pki.privateKeyToPem(keypair.privateKey)

    // 创建证书请求对象
    const certReq = pki.createCertificationRequest()
    certReq.publicKey = keypair.publicKey // 1. 写入公钥
    certReq.setSubject([ // 2. 写入主体信息
      { name: 'commonName', value: import.meta.env.VITE_APP_BASE_API }, // 可以写入域名
      { // 法人名称 即 O
        name: 'organizationName',
        value: encodeURIComponent(enterpriseName) // 所有 value 字符不可以是中文,所有转码保存
      },
      { // 统一社会信用代码 即 OU
        name: 'organizationalUnitName',
        value: enterpriseCode
      }
    ])
    certReq.sign(keypair.privateKey) // 3. 私钥签名

    return {
      certReq, // csr 证书请求文件
      publicKey,
      privateKey
    }
  } catch (error) {
    Message.error('生成失败,' + error)
  }
}

注意,写入主体信息时,value 不允许是中文格式,否则导出的 csr 文件会报错。

1.2 私钥导出与复制

通过依赖生成的 PEM 格式私钥,可通过以下方式下载或复制。

js 复制代码
// binary 文件下载
// 下载私钥调用示例: downloadBinary(privateKey, '私钥.key')
export function downloadBinary(binaryData, fileName = 'file.pdf', type) {
  const length = binaryData.length
  const uint8Array = new Uint8Array(length)

  for (let i = 0; i < length; i++) {
    uint8Array[i] = binaryData.charCodeAt(i)
  }

  // 创建 Blob 对象
  const blob = new Blob([uint8Array], { type })

  // 创建下载链接
  const url = window.URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = fileName

  // 触发下载
  document.body.appendChild(link)
  link.click()

  // 清理资源
  document.body.removeChild(link)
  window.URL.revokeObjectURL(url)
}

// 复制内容
// 复制私钥内容示例:copyToClipboard(privateKey)
export function copyToClipboard(string) {
  if (navigator.clipboard) {
    navigator.clipboard.writeText(string)
    Message.success('已复制到粘贴板!');
  } else if (document.execCommand) {
    const el = document.createElement('textarea')
    el.setAttribute('readonly', 'readonly')
    el.value = string
    document.body.appendChild(el)
    el.select()
    document.execCommand('copy')
    document.body.removeChild(el)
    Message.success('已复制到粘贴板!');
  }
}

2 PKI证书验签

2.1 证书文件上传

elementUIarco-design 的证书或私钥上传。

html 复制代码
<a-input
  v-model="keyPairForm.certificate"
  type="file"
  placeholder="请选择证书"
  :input-attrs="{
    accept: '.crt'
  }"
  allow-clear
  @input="(file, evt) => file2base64(evt, 'certificateBinary')"
/>

<el-input
  v-model="loginForm.privateKey"
  type="file"
  placeholder="请选择私钥"
  accept=".key"
  @input.native="file2base64($event, 'privateKeyBinary')"
/>

2.2 获取证书文件的内容

将证书 .crt 文件转为可操作的数据类型。

js 复制代码
// 文件转为binary
async function file2base64(evt, key) {
  const file = await pdf2base64(evt.target.files[0]) // 文件先转成 base64
  const binaryString = window.atob(file.split(',')[1]) // 然后读取内容
  keyPairForm.value[key] = binaryString // 内容写入
  // 1. 配合 html 的 input,文件上传后以 "地址形式" 保存在 certificate 字段
  // 2. 通过此方法将文件转为 binary data 保存在 certificateBinary 字段
  // 3. certificate 字段可用于 el-form-item / a-form-item 校验
  // 4. 实际接口及后续签名操作使用 certificateBinary 字段
}

// pdf 转 base64 (实际可以支持其他格式的文件转码)
function pdf2base64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    let fileResult = ""
    reader.readAsDataURL(file)
    // 开始转码
    reader.onload = () => {
      fileResult = reader.result
    }
    // 转码失败
    reader.onerror = (error) => {
      reject(error)
    }
    // 转码结束
    reader.onloadend = () => {
      resolve(fileResult)
    }
  })
}

3 PKI验签流程

获取证书文件,传给后端,将返回的 nonce私钥 privateKey 进行签名后,再传回后端,获得校验通过的 token

代码示例:

js 复制代码
// PKI 验签
function keyPairVerify() {
  keyPairFormRef.value.validate(async valid => { // 证书与私钥文件校验
    if (valid) return // 存在验证未通过项
    keyPairLoading.value = true // 开启交互 loading 锁按钮
    const { code, data, message } = await getNonce({ // 1. 将 binary data 格式的 crt 证书发给后端,并拿到 nonce (getNonce 为后端定义的证书上传接口)
      crt: keyPairForm.value.certificateBinary
    }) // 发证书收nonce
    const signature = dataSignature(keyPairForm.value.privateKeyBinary, data?.nonce) // 2. 用私钥对 nonce 签名 (dataSignature 为签名函数)
    const res = await signatureVerifyPort({ signature }) // 3. 使用签完名的nonce 进行后续业务 (signatureVerifyPort 为后端定义的后续业务流接口,主要是接收签名,验签通过后返回数据或token)
    // PKI主流程完成,以下为后续提示 可自行优化
    if (res.code === 200) {
      Message.success('注册成功!')
      keyPairModalCancel() // 关闭弹窗
      router.go(0) // 刷新页面,重新获取信息
    } else {
      Message.warning('注册失败,' + res.message)
      keyPairLoading.value = false // 结束交互
    }
  }).catch(err => {
    Message.error('注册失败,' + err.message)
    keyPairLoading.value = false // 结束交互
  })
}

// 数据签名
function dataSignature(key, data) {
  const privateKey = pki.privateKeyFromPem(key) // binary key 转为 pem key
  const md1 = md.sha256.create() // 创建哈希算法
  md1.update(data, 'utf8') // 数据更新
  const signature = privateKey.sign(md1) // 签名
  const signatureBase64 = util.encode64(signature) // 转码base64
  return signatureBase64
}

笔记主要为自用,欢迎友好交流!

相关推荐
前端菜鸟杂货铺几秒前
前端首屏优化及可实现方法
前端
遂心_几秒前
React Fragment与DocumentFragment:提升性能的双剑合璧
前端·javascript·react.js
ze_juejin几秒前
ionic、flutter、uniapp对比
前端
咚咚咚ddd1 分钟前
WebView Bridge 跨平台方案:统一 API 实现多端小程序通信
前端·前端工程化
程序视点2 分钟前
Microsoft .Net 运行库离线合集包专业解析
前端·后端·.net
混水的鱼3 分钟前
PasswordValidation 密码校验组件实现与详解
前端·react.js
ze_juejin4 分钟前
async、defer 和 module 属性的比较
前端
归于尽5 分钟前
关于数组的这些底层你真的知道吗?
前端·javascript
puppy0_07 分钟前
前端性能优化基石:HTML解析与资源加载机制详解
前端·性能优化
三小河7 分钟前
e.target 和 e.currentTarget 的区别
前端