登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 怎么读取 PEM 证书的域名和到期时间

来源:17golang原创

时间:2026-09-06 03:29:54 254浏览 收藏

在 Go 里读取 .pem 证书,关键不是把文本按换行切开,而是先用 encoding/pem 找到 CERTIFICATE 块,再把其中的 DER 字节交给 crypto/x509。解析成功后,域名通常看 DNSNames,IP 地址看 IPAddresses,到期时间看 NotAfterSubject.CommonName 更适合做展示或兼容旧数据,不能代替现代主机名校验。

要点速览
  • pem.Decode 得到的是 PEM 块,x509.ParseCertificate 才会得到证书对象。
  • 域名优先读取 DNSNames,IP 地址单独读取 IPAddresses,不要只盯着 Common Name。
  • NotBeforeNotAfter 是时间边界;展示倒计时前还要判断证书是否已经生效或过期。

PEM 到 x509.Certificate 的关系

PEM 是带有头尾标记的文本封装,证书主体通常是 Base64 表示的 DER 数据。Go 官方的处理路径可以压缩成两步:pem.Decode 找到块并取出 block.Bytesx509.ParseCertificate 再解析这些 DER 字节。任何一步失败,都不应该继续读取证书字段。

Go PEM 证书从 PEM 文本到 x509.Certificate 的编码层和证书对象层关系图
图1:PEM 文本、DER 字节与 x509.Certificate 对象的静态关系。

先解析 CERTIFICATE 块,再读取证书字段

下面的函数接收 PEM 文件路径,返回第一张证书的关键信息。类型检查很重要:同一个文件里可能同时放着私钥、证书链或其他 PEM 块,不能看到 block != nil 就直接解析。

package main

import (
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "os"
    "time"
)

func readCertificate(path string) (*x509.Certificate, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("读取证书文件失败: %w", err) // 保留文件路径阶段的原始错误
    }

    block, _ := pem.Decode(data)
    if block == nil {
        return nil, fmt.Errorf("没有找到 PEM 数据块") // 空块通常意味着格式或文件内容不对
    }
    if block.Type != "CERTIFICATE" {
        return nil, fmt.Errorf("第一个 PEM 块不是证书: %s", block.Type) // 防止把私钥交给证书解析器
    }

    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        return nil, fmt.Errorf("解析 DER 证书失败: %w", err) // 解析失败时不要读取半成品字段
    }
    return cert, nil
}

func main() {
    cert, err := readCertificate("server-cert.pem")
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println("域名:", cert.DNSNames)          // SAN 中的 DNS 名称
    fmt.Println("IP 地址:", cert.IPAddresses)   // SAN 中的 IP 地址
    fmt.Println("生效时间:", cert.NotBefore)
    fmt.Println("到期时间:", cert.NotAfter)
    fmt.Println("距离到期:", time.Until(cert.NotAfter)) // 负数表示已经过期
}

这里没有调用 Verify,所以代码回答的是“证书里写了什么”,不是“证书是否被当前系统信任”。如果要判断某个主机名能否使用这张证书,应使用 cert.VerifyHostname("api.example.com"),并把信任链验证和字段展示分成两个动作。

域名和到期时间应该从哪些字段读取

一个证书可能同时包含多个 DNS 名称和多个 IP 地址,返回值都是切片。常见字段可以这样区分:

字段表示什么读取建议
DNSNamesSubject Alternative Name 中的 DNS 名称域名列表优先读取它
IPAddressesSubject Alternative Name 中的 IP 地址连接目标是 IP 时单独展示或比较
Subject.CommonName证书主题中的旧式通用名称可用于兼容显示,不作为主机名验证依据
NotBefore / NotAfter证书有效期的起止边界time.Now()time.Until 一起判断状态
Go x509 Certificate 的 DNSNames、IPAddresses、CommonName 与 NotAfter 字段关系图
图2:域名、IP 地址与证书有效期分别落在 Certificate 的不同字段中。

证书没有域名或已经过期时怎么处理

DNSNames 为空不一定代表解析失败,可能是这张证书只包含 IP 地址,也可能是输入本身不适合作为服务器证书。展示层可以把空切片显示为“未声明 DNS 名称”,但业务校验应该明确要求 DNS 名称还是 IP 地址。

now := time.Now()
switch {
case now.Before(cert.NotBefore):
    fmt.Println("证书尚未生效") // 当前时间早于 NotBefore
case now.After(cert.NotAfter):
    fmt.Println("证书已经过期") // 当前时间晚于 NotAfter
default:
    fmt.Printf("证书有效,剩余 %s\n", time.Until(cert.NotAfter).Round(time.Minute)) // 便于日志阅读
}

if err := cert.VerifyHostname("api.example.com"); err != nil {
    fmt.Println("主机名不匹配:", err) // VerifyHostname 会按 DNSNames 检查域名
}

如果证书来自自动续期系统,可以把“剩余时间”作为提醒条件,但不要把提醒阈值写死成证书有效期。真正上线前还要确认系统加载的是新文件、服务进程是否重新读取,以及证书链是否完整。

PEM 文件包含证书链时要保留 rest

pem.Decode 会同时返回当前块和剩余字节。只取第一块适合读取叶子证书;如果文件里还有中间证书,就应沿着 rest 继续解码,并按 block.Type == "CERTIFICATE" 过滤。解析链时不要把多个证书的域名简单合并成一张证书的域名清单,每张证书的字段归属仍然独立。

另外,PEM 文件可能带有空白、注释或私钥。遇到 block == nil 时先检查文件是否真的是 PEM;遇到类型不是 CERTIFICATE 时记录块类型并跳过或终止,避免将私钥内容写进日志。

常见问题

为什么 CommonName 有域名,但 DNSNames 是空的?

这是旧式证书字段与现代 SAN 字段的区别。CommonName 可以用于展示旧数据,但 VerifyHostname 不把它作为现代主机名匹配依据;需要做主机校验时,应让证书包含正确的 DNSNames。

读取 NotAfter 就能证明证书可用吗?

不能。NotAfter 只说明时间上的结束边界,还需要结合 NotBefore、主机名匹配、用途和信任链判断是否真的可用。

一个 PEM 文件有多个证书,应该选哪一张?

通常第一张是叶子证书,后续块是中间证书,但不要只靠位置猜测业务语义。遍历每个 CERTIFICATE 块并根据 Subject、Issuer 和验证需求建立链,展示域名时明确标出证书来源。

结论:域名看 SAN,到期看 NotAfter

Go 读取 PEM 证书的稳定路径是 pem.Decode → x509.ParseCertificate。域名优先使用 DNSNames,IP 地址看 IPAddresses,到期时间看 NotAfter,再用 NotBeforeVerifyHostname 补足状态判断。这样既能准确展示证书信息,也不会把“字段存在”误判成“证书已被信任”。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>