
本文详解如何通过编程方式准确、鲁棒地检测域名的 spf、dkim 和 dmarc dns 记录,重点解决 dkim selector 动态性、记录层级嵌套、多域场景等常见误判问题,并提供可落地的 python 实战代码与最佳实践。
本文详解如何通过编程方式准确、鲁棒地检测域名的 spf、dkim 和 dmarc dns 记录,重点解决 dkim selector 动态性、记录层级嵌套、多域场景等常见误判问题,并提供可落地的 python 实战代码与最佳实践。
在构建反钓鱼邮件系统、安全审计工具或企业邮件合规平台时,仅依赖固定格式(如 dkim._domainkey.example.com)查询 DNS 记录极易失败——正如 Ryanair 和 MarvelApp 的案例所示:其 DKIM 签名头中明确声明 s=15below 和 s=default,而非通用 dkim;DMARC 记录需查询 _dmarc.example.com 而非根域;SPF 则可能嵌套多层 include: 且需语法校验。真正的“健壮验证”,必须模拟接收方邮件服务器的实际行为:从邮件头逆向推导 + 多路径 DNS 探测 + 语义化解析。
一、核心原理:邮件认证不是静态配置,而是动态链式验证
Gmail 或 Outlook 在 Authentication-Results 中显示 dkim=pass,并非凭空判断,而是严格遵循 RFC 6376 流程:
-
提取签名头:从原始邮件中解析
DKIM-Signature:字段,获取关键参数:-
d=(签署域名,如care.ryanair.com) -
s=(selector,如15below) -
v=(协议版本,通常为1)
-
-
构造 DNS 查询目标:
{selector}._domainkey.{domain}→15below._domainkey.care.ryanair.com -
递归验证 SPF/DMARC:
- SPF:查询
example.com的 TXT 记录(主机名@),并递归解析所有include:指向的子记录; - DMARC:强制查询
_dmarc.example.com的 TXT 记录(非example.com根域);
- SPF:查询
-
语法与逻辑校验:SPF 记录需满足
v=spf1 ...结构,且机制数 ≤10(避免 DNS 查找超限);DKIM 公钥需为合法 Base64 编码的 RSA 公钥;DMARC 策略必须含p=参数。
⚠️ 关键误区纠正:
- ❌ “查不到
dkim._domainkey就代表无 DKIM” → 正确做法是先获取真实邮件样本,解析DKIM-Signature头;- ❌ “SPF 记录存在即有效” → 必须检查是否含冗余
include、是否使用-all(强拒绝)而非~all(软失败);- ❌ “DMARC 不存在 = 未配置” → 部分企业仅配置 SPF+DKIM,DMARC 为可选策略层。
二、Python 实战:基于 dnspython 的健壮验证器
以下代码实现完整链路验证,已适配 2026 年主流 DNS 解析规范(支持 EDNS、TCP 回退、超时重试):
import dns.resolver
import dns.exception
from typing import Dict, Optional, List
def query_txt_record(domain: str, host: str = "@") -> List[str]:
"""安全查询 TXT 记录,支持自动追加域名后缀"""
try:
# 处理根域 @ 和子域
fqdn = domain if host == "@" else f"{host}.{domain}"
answers = dns.resolver.resolve(fqdn, "TXT", raise_on_no_answer=False)
return [txt.to_text().strip('"') for rdata in answers for txt in rdata.strings]
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
return []
except dns.exception.Timeout:
return ["ERROR: DNS timeout"]
except Exception as e:
return [f"ERROR: {str(e)}"]
def check_spf(domain: str) -> Dict:
records = query_txt_record(domain)
spf_records = [r for r in records if r.startswith("v=spf1")]
return {
"exists": len(spf_records) > 0,
"records": spf_records,
"valid_syntax": all("v=spf1" in r and ("~all" in r or "-all" in r) for r in spf_records)
}
def check_dmarc(domain: str) -> Dict:
# DMARC 必须查询 _dmarc 子域
records = query_txt_record(domain, "_dmarc")
dmarc_records = [r for r in records if r.startswith("v=DMARC1")]
return {
"exists": len(dmarc_records) > 0,
"records": dmarc_records,
"policy": next((r.split("p=")[1].split(";")[0] for r in dmarc_records if "p=" in r), "none")
}
def check_dkim_from_signature(dkim_header: str) -> Dict:
"""从 DKIM-Signature 头解析 selector 和 domain,再查 DNS"""
import re
# 提取 d= 和 s= 值(兼容空格和换行)
d_match = re.search(r"d\s*=\s*([^;\s]+)", dkim_header)
s_match = re.search(r"s\s*=\s*([^;\s]+)", dkim_header)
if not d_match or not s_match:
return {"exists": False, "error": "Failed to parse d= or s= from DKIM header"}
domain, selector = d_match.group(1).strip(), s_match.group(1).strip()
fqdn = f"{selector}._domainkey.{domain}"
try:
answers = dns.resolver.resolve(fqdn, "TXT", raise_on_no_answer=False)
pubkey = "".join(txt.to_text().strip('"') for rdata in answers for txt in rdata.strings)
return {
"exists": True,
"domain": domain,
"selector": selector,
"public_key": pubkey[:100] + "..." if len(pubkey) > 100 else pubkey,
"is_valid_rsa": "k=rsa" in pubkey and "p=" in pubkey
}
except Exception as e:
return {"exists": False, "error": str(e), "query": fqdn}
# 使用示例:验证 Ryanair 邮件
if __name__ == "__main__":
domain = "ryanair.com"
print("=== SPF ===")
print(check_spf(domain))
print("\n=== DMARC ===")
print(check_dmarc(domain))
# 模拟从真实邮件头提取的 DKIM-Signature
dkim_header = 'v=1; a=rsa-sha1; c=relaxed/relaxed; s=15below; d=care.ryanair.com; h=MIME-Version:From:To:Date:Subject:Message-ID:Content-Type; bh=MCorT6FfWGOmISJQSzdv4YLmKfg=; b=eXcQvy0odmzIAYy11bfM8OsoiXziin5E1hbWHvxlY6Q+KSpZr6/5OiUZ4EiNoCpNwFrciKB9Yj8GwmZOZwxQd3PW05+2bnu+8oKMPij/AyAEAi2tJ0TBEZxM7BOsno84L3eZ0BQFZvog6bW9UQE1fJCQaoQYXPgsHV6dzWjmHYo='
print("\n=== DKIM (from header) ===")
print(check_dkim_from_signature(dkim_header))三、进阶建议:生产环境必备实践
-
缓存与节流:DNS 查询成本高,对高频调用应引入 TTL 缓存(如
redis存储domain:spf键值对,TTL=3600); -
多解析器兜底:当本地 DNS 失败时,自动切换至
8.8.8.8或1.1.1.1公共解析器; -
SPF 递归展开:使用
dnspython的resolver.resolve()逐层解析include:,统计总 IP 数量,避免超过 10 次 DNS 查询限制; -
DMARC 报告监控:配置
rua=mailto:reports@example.com后,需部署 ARC 兼容接收器解析 XML 聚合报告; -
自动化测试集:维护包含
google.com(多 selector)、github.com(strict DMARC)、paypal.com(复杂 SPF)的基准域列表,每日巡检。
✅ 总结:邮件认证验证的本质,是将 DNS 配置与实际邮件流对齐。抛弃“静态猜测”,拥抱“头信息驱动”的动态探测,辅以 RFC 合规性校验,才能真正实现 99.9% 准确率的自动化检测——这不仅是技术方案,更是对抗钓鱼攻击的第一道程序化防线。

















