前端基于 node-forge
依赖进行 PKI
流程,先安装依赖 npm install --save node-forge
。
1 密钥对生成与下载/复制
1.1 密钥对与证书请求生成
利用 node-forge
的 pki.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 证书文件上传
elementUI
和 arco-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
}
笔记主要为自用,欢迎友好交流!