perf: 查看证书增加证书详情显示,包括域名,过期时间,颁发机构,指纹等

This commit is contained in:
xiaojunnuo
2026-03-16 00:52:33 +08:00
parent 76d12d6062
commit 0b9933df1e
5 changed files with 110 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import fs from "fs";
import os from "os";
import path from "path";
import { CertificateInfo, crypto } from "@certd/acme-client";
import cryptoLib from "crypto";
import { ILogger } from "@certd/basic";
import dayjs from "dayjs";
import { uniq } from "lodash-es";
@@ -119,9 +120,28 @@ export class CertReader {
const detail = crypto.readCertificateInfo(crt.toString());
const effective = detail.notBefore;
const expires = detail.notAfter;
const fingerprints = CertReader.getFingerprintX509(crt);
// @ts-ignore
detail.fingerprints = fingerprints;
return { detail, effective, expires };
}
static getFingerprintX509(crt: string) {
try {
// 创建X509Certificate实例
const cert = new cryptoLib.X509Certificate(crt);
// 获取指纹
return {
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,
fingerprint512: cert.fingerprint512,
};
} catch (error) {
console.error("处理证书失败:", error.message);
return null;
}
}
getAllDomains() {
const { detail } = this.getCrtDetail();
const domains = [];

View File

@@ -154,6 +154,7 @@ export type CertInfo = {
ic: string;
der: string;
pfx: string;
detail: any;
};
export async function GetCert(pipelineId: number): Promise<CertInfo> {

View File

@@ -1,12 +1,47 @@
<template>
<div class="cert-view">
<a-list item-layout="vertical" :data-source="certFiles">
<div class="cert-detail mt-4">
<a-descriptions class="w-full" bordered :column="2" size="small">
<a-descriptions-item label="主域名">
<a-tag color="blue">{{ props.cert.detail?.domains?.commonName || "-" }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="颁发机构">
<a-tag color="green">{{ props.cert.detail?.issuer?.commonName || "-" }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="备用域名" :span="2">
<a-tag v-for="(domain, index) in props.cert.detail?.domains?.altNames || []" :key="index" color="blue">
{{ domain }}
</a-tag>
<span v-if="!props.cert.detail?.domains?.altNames?.length">-</span>
</a-descriptions-item>
<a-descriptions-item label="生效时间">
{{ formatDate(props.cert.detail?.notBefore) }}
</a-descriptions-item>
<a-descriptions-item label="过期时间">
{{ formatDate(props.cert.detail?.notAfter) }}
</a-descriptions-item>
<a-descriptions-item label="指纹">
<div class="w-full flex flex-col fingerprint">
<div class="flex flex-nowrap">
<span class="font-bold label">SHA-1:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint || '-'"></fs-copyable>
</div>
<div class="flex flex-nowrap mt-1">
<span class="font-bold label">SHA-256:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint256 || '-'"></fs-copyable>
</div>
<div class="flex flex-nowrap mt-1">
<span class="font-bold label">SHA-512:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint512 || '-'"></fs-copyable>
</div>
</div>
</a-descriptions-item>
</a-descriptions>
</div>
<a-list item-layout="vertical" :data-source="certFiles" class="cert-content">
<template #renderItem="{ item }">
<a-list-item key="item.key">
<a-list-item-meta>
<template #title>
<div class="title">
<div>{{ item.name }}({{ item.fileName }})</div>
<div class="font-bold">{{ item.name }}({{ item.fileName }})</div>
<fs-copyable :model-value="item.content" :button="{ show: false }">
<a-tag color="success">复制</a-tag>
</fs-copyable>
@@ -34,10 +69,25 @@ const certFiles = ref([
{ name: "私钥", fileName: "private.pem", key: "key", content: props.cert.key },
{ name: "中间证书", fileName: "chain.pem", key: "ic", content: props.cert.ic },
]);
function formatDate(dateStr: string): string {
if (!dateStr) return "-";
const date = new Date(dateStr);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
}
</script>
<style lang="less">
.cert-view {
margin-right: 25px;
.title {
display: flex;
justify-content: space-between;
@@ -46,5 +96,29 @@ const certFiles = ref([
margin-block-end: 0px !important;
margin-top: 10px;
}
.cert-detail {
table {
width: 100%;
table-layout: fixed;
}
.fingerprint {
.label {
width: 70px;
flex-shrink: 0;
}
.text {
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
}
}
}
.cert-content {
.ant-list-item {
padding-left: 0;
padding-right: 0;
}
}
}
</style>

View File

@@ -8,6 +8,7 @@ import { logger } from "@certd/basic";
import fs from "fs";
import dayjs from "dayjs";
import { ApiTags } from "@midwayjs/swagger";
import { CertReader } from "@certd/plugin-lib";
/**
*/
@@ -159,6 +160,10 @@ export class CertInfoController extends CrudController<CertInfoService> {
await this.checkOwner(this.getService(),id,"read");
const certInfoEntity = await this.service.info(id);
const certInfo = JSON.parse(certInfoEntity.certInfo);
if (certInfo?.crt) {
const certReader = new CertReader(certInfo);
certInfo.detail = certReader.detail
}
return this.ok(certInfo);
}

View File

@@ -42,7 +42,14 @@ export class CertController extends BaseController {
}
}
const privateVars = await this.storeService.getPipelinePrivateVars(id);
return this.ok(privateVars.cert);
const certInfo = privateVars.cert;
if (certInfo?.crt) {
const certReader = new CertReader(certInfo);
certInfo.detail = certReader.detail
}
return this.ok(certInfo);
}