你有没有过这样的经历:某个平台突然流传出“百万用户数据泄露”的截图,底下一群人晒出自己的手机号和邮箱,点开一看,自己的账号居然也在里面。这时候你最想干的事,就是确认自己到底暴露了多少信息、哪些平台的数据被拖了、手机号、邮箱、身份证有没有被卷进去。leak-check API 就是用来解决这个问题的工具,它把散落在公开渠道的泄漏样本数据整理成可查询的接口,用简单的 HTTP 请求就能检测三类敏感信息是否出现在已知泄漏记录中。
这篇博文不绕弯子,直接用三步带你跑通 leak-check API:第一步准备密钥和请求协议,第二步写出邮箱、手机号、身份证三类检测的完整代码,第三步处理结果、批量任务和文档里不会写的坑。适合谁看?想做账号安全自查的个人用户、需要批量排查企业客户数据是否泄露的 IT 人员、安全工程师,以及所有想系统了解泄漏检测 API 工作方式的人。
1. 这三个数据维度到底在查什么:leak-check API的产品逻辑
很多人以为“泄漏检测”就是一个黑盒,丢一个手机号进去,出来一句“泄露了/没泄露”。真上手之后会发现,邮箱、手机号、身份证这三个维度的检测逻辑差异很大,搞清楚它们分别是怎么工作的,后面写代码、判读结果才不会跑偏。
1.1 邮箱检测:数据库匹配和指纹比对
邮箱是目前泄漏数据里最常见的主键。不管是电商网站、社区论坛还是各种业务系统被拖库,导出的用户表里基本都有邮箱字段。leak-check 类服务做的事,是把历年来公开流传的泄漏样本收集、清洗、建立索引,再存成一套可检索的数据库。你查询一个邮箱时,服务端做的是字符串匹配——先做归一化处理,再去样本库比对,命中就返回该邮箱出现在哪些泄漏事件中、首次发现时间和最近出现时间。
归一化这个细节特别值得注意。同一个邮箱可能有多种写法,比如User.Name+tag@Example.com和username@example.com可能指向同一个人。服务商一般有自己的归一化规则,但你在传参时也最好自己做一遍简单清洗:去掉首尾空格、全部转小写,这样能减少不少无效查询。
有一点要提醒:这类接口通常不会直接返回泄漏库中的密码明文。正规服务商只告诉你“命中/未命中/风险等级”以及命中的事件名称,不会把库里对应密码原文返回给你。原因很简单,把别人的密码原文二次分发是违法的,服务商自己也担不起这个责任。所以对接时如果看到某个“泄漏检测 API”承诺返回密码明文,基本可以判定不合规,趁早远离。
1.2 手机号检测:与邮箱检测的差异
手机号检测的原理和邮箱类似,也是拿手机号去样本库匹配,但有几个坑跟邮箱不一样。
首先,手机号比邮箱的隐私敏感度高得多。邮箱可能是工作号、注册专用号,泄露后无非是收垃圾邮件,但手机号泄露之后意味着短信轰炸、精准诈骗、社工攻击都找上门。所以正经服务商对手机号查询的管控更严格,有的要求企业资质,有的会在响应里强制脱敏,比如返回138****5678而不是完整号码。
其次,手机号匹配的准确性受号码段变化影响。携号转网、虚拟运营商号段、170/171 这类号码,在一些老样本库里可能没有覆盖,或者被错误标记。这意味着命中结果相对保守,没查到不代表绝对安全,只能代表“在当前样本库中未发现”。反过来,如果查到了,基本就是实锤,因为手机号是强标识,不太可能因为格式差异导致误报。
还有一个实操经验:批量查手机号时,建议在脚本里前置一步格式清洗,统一去掉+86前缀、空格和横线,存成 11 位纯数字再提交。别小看这一步,很多服务商的样本库做得糙,你传一个带空格或者带国家码的号码过去,它匹配不上,返回“未命中”,容易误导判断。
1.3 身份证检测:实名信息核验的特殊性
身份证这个维度最特殊,因为它不是“能不能换”的问题——邮箱可以换、手机号可以换,身份证号是伴随终身的,一旦泄露没有任何挽回余地。所以身份证检测的价值比前两者更大,但能做的事情也要分清楚。
leak-check 类接口能回答的问题是:这个身份证号是否出现在已知的泄漏样本中。它不能回答的,是“这个身份证号对应的姓名是谁”“这个身份证号和某个人名是否匹配”。后一类属于实名核验,需要的是身份证二要素(姓名+身份证号)或三要素(姓名+身份证号+手机号)核验接口,通常由运营商或持牌数据服务商提供,个人开发者申请门槛高,很多还需要企业资质。
所以一个完整的身份证风险排查闭环通常是两步:第一步,用 leak-check API 查身份证号是否在历史泄漏样本中;第二步,如果命中,再配合实名核验服务确认当前这个身份证对应的实名信息是否已经跟某个手机号、某个姓名绑定。两步是不同接口,别指望一个 API 全搞定。这一点我在对接时经常遇到有人理解偏差,以为身份证检测就是查“这个号是谁的”,那是实名核验的事,不是泄漏检测的事。
2. 第一步:密钥、鉴权与第一个请求
不管后面写多少代码,第一步都是先拿到 API 密钥,把请求协议跑通。这一步听起来简单,但我在实际项目里见过太多人栽在环境准备上。
2.1 注册、密钥获取与额度确认
大多数 leak-check 服务商都提供自助注册。注册之后进入控制台,能找到 API Key 或者 Access Token。有些服务商还区分测试密钥和正式密钥,测试密钥有严格的调用次数限制,正式密钥按套餐计费。拿密钥之后,第一件事不是写代码,而是看文档里的两页:接口鉴权方式和限流规则。
鉴权方式常见的就几种:请求头带Authorization: Bearer <KEY>、请求头带X-Api-Key: <KEY>、或者要求在 URL 里拼?api_key=<KEY>。我建议优先选择支持 Header 鉴权的服务商,因为 API Key 拼在 URL 里有被网关日志、代理服务记录下来的风险,而 Header 里的鉴权信息被默认脱敏的概率更大。这属于网络安全的基本功,谁做谁知道。
限流规则重点看三件事:每秒请求上限(QPS)、每日总调用量、超过限制之后的返回码。大多数服务商限流返回 429,也有的返回 403,这个字段直接决定了你后面的重试策略。拿到密钥后,我习惯先写一个最小请求脚本,打一次真实接口,确认返回结构和文档描述一致,再往下走。别嫌多这一步,文档和真实行为不一致的情况,我碰到不止一次。
2.2 请求协议与最小可用的查询示例
下面这个示例基于常见 leak-check API 的设计约定编写。域名、端点路径和字段名是示例,真实服务商可能用/api/v1/lookup、X-Api-Key头等不同设计,一切以官方文档为准。
先看一个最小查询,用 curl 查一个邮箱:
curl -X POST "https://api.leak-check.example/v1/check" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type":"email","value":"user@example.com"}'这里用 POST 而不是 GET,是有讲究的。GET 请求的查询参数会完整出现在访问日志、代理缓存和浏览器历史里,也就是说,你查的手机号、邮箱很可能被基础设施记录。POST 把请求体放在 Body 里,虽然不等于绝对安全,但至少不会随便进 URL 日志。对于泄漏检测这种本身就极度敏感的接口,选择 POST 是更稳的做法。
提示:示例中的 API 域名、端点路径和字段名是通用写法,实际服务商的接口可能叫
/api/v1/search、鉴权头可能用X-Api-Key,请以官方文档为准。
对应的 Python 请求长这样:
import requests API_URL = "https://api.leak-check.example/v1/check" API_KEY = "YOUR_API_KEY" def check_value(data_type: str, value: str) -> dict: resp = requests.post( API_URL, headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json={"type": data_type, "value": value}, timeout=10, ) resp.raise_for_status() return resp.json() result = check_value("email", "user@example.com") print(result)跑通这个脚本,确认能拿到正常响应,第一步就算完成了。这里有一个小建议:timeout=10一定要加。泄漏检测接口背后是大型样本库检索,有些慢查询可能拖到十几秒,但不设置超时时间的话,你的脚本可能因为一个异常请求永远挂在那里。
2.3 响应结构、状态码与错误码速查
正常的响应一般长这样:
{ "status": "found", "queried_type": "email", "risk_level": "high", "first_seen": "2023-01-15", "last_seen": "2024-05-20", "breaches": [ {"name": "some-platform-breach", "date": "2023-01-15"} ] }常见返回码整理成了表格,方便对照:
| 状态码 | 含义 | 处理建议 |
|---|---|---|
| 200 | 查询成功,看 body 里的 status 字段 | 正常解析 |
| 400 | 参数错误,type 或 value 格式不对 | 检查请求体字段 |
| 401 | API Key 无效或缺失 | 检查密钥配置 |
| 403 | 无权限或套餐不允许该类型查询 | 联系服务商开通 |
| 429 | 触发限流或额度耗尽 | 停止请求,按退避策略重试 |
| 500 | 服务端异常 | 稍后重试,记录日志 |
有一个非常容易踩的坑:返回 200 不代表“没泄露”。判断是否泄漏要看 body 里的业务字段(比如status是否为found),而不是只看 HTTP 状态码。我见过有同事把 HTTP 200 直接当成“没有泄露”来处理,结果报告全错。HTTP 状态码代表请求是否成功执行,业务字段才代表查询结果是什么,这两个概念不能混。
3. 第二步:手机号、邮箱、身份证三类查询的完整代码
协议跑通之后,就可以写真正能用的代码了。这一章给三类数据各自的实现,你可以直接拿去改。
3.1 邮箱检测:第一个完整的查询函数
邮箱检测是最基础的场景,适合做第一个完整样例。查询函数需要处理四件事:输入清洗、鉴权、响应解析、限流异常。我写一个带自定义异常的版本:
import requests import os API_URL = os.getenv("LEAK_CHECK_API_URL", "https://api.leak-check.example/v1/check") API_KEY = os.getenv("LEAK_CHECK_API_KEY", "") class RateLimitError(Exception): pass def check_email(email: str) -> dict: email = email.strip().lower() resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={"type": "email", "value": email}, timeout=10, ) if resp.status_code == 429: raise RateLimitError("触发限流,请稍后重试") resp.raise_for_status() data = resp.json() return { "query": email, "found": data.get("status") == "found", "risk_level": data.get("risk_level", "unknown"), "first_seen": data.get("first_seen"), "last_seen": data.get("last_seen"), "breaches": data.get("breaches", []), }使用的时候直接调用:
result = check_email("user@example.com") if result["found"]: print(f"该邮箱已出现在 {len(result['breaches'])} 个泄漏事件中,风险等级:{result['risk_level']}") else: print("当前样本库中未发现该邮箱的泄漏记录")这里把 API Key 从环境变量里读,而不是硬编码在代码里,是必须养成的习惯。尤其是这类查询涉及极其敏感的个人信息,一旦代码和密钥一起被提交到公开仓库,别人拿你的密钥刷接口,账单会直接爆掉。RateLimitError单独定义而不是直接抛出 HTTP 异常,是为了上层做重试策略时能精准识别限流场景。
3.2 手机号查询:批量质检与脱敏展示
手机号场景在实际工作中大多是批量的,比如企业要排查一批客户手机号是否在最近某次拖库事件中出现。我写一个从 CSV 读取、逐条查询、脱敏输出的版本:
import csv import time import requests import os API_URL = os.getenv("LEAK_CHECK_API_URL", "https://api.leak-check.example/v1/check") API_KEY = os.getenv("LEAK_CHECK_API_KEY", "") def mask_phone(phone: str) -> str: return phone[:3] + "****" + phone[-4:] def check_phone(phone: str) -> dict: phone = phone.strip().replace("+86", "").replace("-", "").replace(" ", "") resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={"type": "phone", "value": phone}, timeout=10, ) resp.raise_for_status() return resp.json() def batch_check_phones(csv_path: str, delay: float = 1.0): with open(csv_path, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: phone = row["phone"] try: data = check_phone(phone) found = data.get("status") == "found" print(f"{mask_phone(phone)} - {'泄露' if found else '未发现'} - 风险等级 {data.get('risk_level', 'unknown')}") except Exception as e: print(f"{mask_phone(phone)} - 查询失败: {e}") time.sleep(delay)注意这里做了三件事:号码格式清洗、日志脱敏、请求间隔控制。脱敏不是可选项,打印完整手机号到日志里本身就是一次新的数据泄露。至于time.sleep(delay),在自己没有搞清楚服务商 QPS 上限之前,保守地把间隔设为 1 秒是最稳妥的做法。
如果你的数据量在几千条以上,建议把打印改成写文件,输出 CSV 或 Excel 报告,同时保留一份失败列表,方便跑完以后单独重试。批量的核心是可观测、可恢复,而不是一把梭。
3.3 身份证查询:与实名核验接口的配合使用
身份证查询的代码结构和邮箱类似,但身份证号码更敏感,我建议在代码层就做严格脱敏,并且明确区分“泄漏检测”和“实名核验”两个环节。
def check_idcard(idcard: str) -> dict: idcard = idcard.strip().upper() resp = requests.post( API_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json={"type": "idcard", "value": idcard}, timeout=10, ) resp.raise_for_status() return resp.json() def mask_idcard(idcard: str) -> str: return idcard[:6] + "********" + idcard[-4:]如果是企业内部的风险排查流程,完整闭环是这样的:
leak_result = check_idcard("110101199001011234") print(f"身份证 {mask_idcard('110101199001011234')} 泄漏状态:{leak_result.get('status')}") if leak_result.get("status") == "found": verify_result = verify_real_name(name="张三", idcard="110101199001011234") print(f"实名核验结果:{verify_result.get('match')}")这里verify_real_name指向另一个实名核验服务,不包含在 leak-check API 的职责范围内。身份证检测这块,我最想强调的一点就是:身份证号一旦泄露,只能尽量降低损失,没法像改密码一样换掉。所以检测结果如果命中,意味着这张证件的风险是长期的,建议告知对方重视后续监控,而不是查完就完事。
4. 第三步:结果解析、批量任务与避坑经验
代码能跑通之后,真正的技术含量在于怎么正确解析结果、怎么批量执行、怎么处理各种异常。这一章把三件事讲透。
4.1 判断“是否泄露”的正确姿势:置信度与风险分级
刚接触这类 API 的人,容易把“命中了”和“风险高”划等号。其实不完全是。想象一下:你的手机号出现在 2012 年一个早已倒闭的论坛的拖库记录里,和它出现在上个月一个支付平台的数据泄露事件里,风险等级能一样吗?
所以我在实际项目里,不会只输出“泄露/未泄露”,而是把结果转成风险评分。评分逻辑参考这几个因素:
- 命中数量:命中的泄漏事件越多,风险越高。
- 最近出现时间:越靠近当前时间,风险越高,老数据可能已经无效或密码已被更换。
- 关联的数据类型:如果命中的样本里同时包含密码哈希、支付信息这类高敏字段,分数更高。
- 事件类型:支付类、金融类平台的泄露,比普通论坛泄露更危险。
给一个简单的评分函数:
from datetime import datetime def risk_score(data: dict) -> int: score = 0 breaches = data.get("breaches", []) score += min(len(breaches) * 2, 20) last_seen = data.get("last_seen") if last_seen: last_dt = datetime.fromisoformat(last_seen) months_since = (datetime.now() - last_dt).days / 30 if months_since <= 6: score += 40 elif months_since <= 24: score += 20 else: score += 5 risk_level = data.get("risk_level", "unknown") if risk_level == "high": score += 40 elif risk_level == "medium": score += 20 return min(score, 100)这套评分不是官方标准,是按业务需要自定义的。但思路值得借鉴:不要直接把 API 结果甩给用户,而是把它变成一个可以指导行动的指标。用户看到“风险评分 85”和看到“status: found”,感知完全不一样。
4.2 批量查询的限流策略与队列设计
批量查询最忌讳的就是一股脑把几万个请求同时打出去。服务商的 QPS 上限通常写在文档里,就算没写,你也能从 429 返回码出现的频率里反推出来。
控制速率最简单的办法,就是固定延迟循环。如果一批数据量很大,最好支持断点续跑——记录已经跑到第几条,进程中断之后下次接着跑,而不是从头再来:
import json def batch_check_with_resume(csv_path: str, progress_path: str): start_index = 0 try: with open(progress_path) as f: start_index = json.load(f).get("last_index", 0) except FileNotFoundError: pass with open(csv_path, newline="", encoding="utf-8") as f: reader = list(csv.DictReader(f)) for i, row in enumerate(reader[start_index:], start=start_index): # 执行查询,省略具体实现 ... if i % 100 == 0: with open(progress_path, "w") as pf: json.dump({"last_index": i}, pf)遇到 429 的时候,别立刻重试,先停 5 秒再试一次。如果连续多次 429,直接退出,让日志告诉你服务商的真实限制在哪里,而不是硬着头皮把服务打挂。这是我被教育过多次之后学到的教训。
4.3 我在实际项目中踩过的几个坑
这部分全是真金白银换来的经验,分享出来帮大家少走弯路。
第一个坑:API Key 硬编码被提交到仓库。我第一次用这类接口时,图省事把密钥直接写在脚本里,后来不小心提交到了 Git 仓库。当天晚上收到服务商的告警邮件,说是检测到异常调用。后来才知道,GitHub 上到处是扫描密钥的机器人,拿到密钥第一时间就去刷接口。从那以后我所有的密钥都只放环境变量或密钥管理服务,仓库里一律用占位符,并且开启分支保护,避免代码被误推上去。密钥泄露和数据泄露一样,都是泄露,别在小事上栽跟头。
第二个坑:把 HTTP 200 当成了“没有泄露”。有个同事写了这样一个判断:if resp.status_code == 200: print("安全")。看到这个的时候我整个人都不好了。HTTP 200 只代表服务端正常响应了请求,不代表查询结果是没有泄露。正确的判断必须看 body 里的业务字段。这种错误在初版脚本里特别常见,看到这里的朋友一定要引以为戒。
第三个坑:身份证号做哈希后比对不上。有一次我想当然地认为,服务商支持把身份证哈希后查询会更安全,于是在本地算了个 MD5 传过去,结果老是不命中。后来看文档才发现,服务商的样本库用的是带盐或特定算法的哈希,直接用标准 MD5 对不上。身份证这类敏感查询,如果文档明确要求传明文并进行加密传输,那就老老实实按文档来;如果服务商支持你传哈希,也要搞清楚它要求的哈希算法。想当然的结果就是白跑一次查询,还浪费额度。
第四个坑:中文姓名乱码。在配合实名核验的场景里,我遇到过查询结果里的中文姓名变成乱码的情况。原因是请求或响应里的编码没处理好。解决方法是确保 requests 库使用 UTF-8,同时在写 CSV 时指定encoding="utf-8-sig",避免 Excel 打开文件时把中文搞乱。这个坑很小,但排查起来特别浪费时间。
5. 隐私红线:合规使用leak-check API的几条硬规矩
技术能力越强,越要清楚边界在哪里。leak-check 这类接口处在个人数据保护的敏感地带,使用不当很容易从“安全工具”变成“违规工具”。
5.1 数据来源授权:只能查谁的、不能查谁的
合规使用的前提就一句话:你只能查询自己拥有、或者经过明确授权的数据。
- 个人用户查自己的邮箱、手机号、身份证,没问题,这是自查。
- 企业排查自家客户数据是否泄露,前提是获得了客户授权,或者至少是处理已脱敏数据,属于正常安全运营。
- 但任何人都不应该批量查询陌生人的手机号或身份证,更不能拿查询结果去撞库、做社工分析、或者反向拼凑个人画像。
把接口用来查别人的隐私数据,已经不是“技术能力问题”,而是法律风险问题。这类行为一旦被处理,可能涉及侵犯公民个人信息罪,不是罚款就能解决的。我在团队里带人的时候,反复强调这一点:工具的合规边界,比工具本身的技术参数重要得多。
重点:合规的底线是“授权”。没有授权,再强的检测能力也不能调用。
5.2 结果处理与脱敏存储
查询结果该怎么存,也是一个容易忽略的合规细节。我的建议是:
- 存储最小化:只保存“是否命中”“风险等级”“最近发现时间”这类非原始字段,不保存完整手机号、完整身份证号。
- 如果必须要存原始值,那就加密存储,并且严格控制访问权限。
- 日志同理:查询日志里的号码和邮箱一律脱敏,不然日志系统被拖走的时候,你的日志本身就是下一个泄露源。
- 数据保留时间要短:风险排查类任务,处理完就可以删掉原始数据,没有长期保留的业务必要性。
这些规矩看起来繁琐,但真出事的时候,它们就是你的救命稻草。做安全的人如果自己都不注意数据保护,那就不能怪行业声誉被拉低。
5.3 与同类服务选型对比与工具链配合
最后简单对比一下主流方案,方便你按自己的场景选型:
| 服务/方案 | 覆盖数据维度 | 适用场景 | 注意点 |
|---|---|---|---|
| leak-check API | 邮箱、手机号、身份证,多维度 | 个人自查、企业批量排查 | 关注文档限流和维度支持 |
| Have I Been Pwned | 邮箱为主 | 技术团队自查、接入安全平台 | 数据偏海外,国内样本覆盖有限 |
| Firefox Monitor | 邮箱 | 个人用户 | 基于 HIBP 数据,功能轻量 |
| 实名核验类接口 | 姓名+身份证+手机号 | 身份一致性验证 | 需企业资质,不负责泄漏检测 |
leak-check API 的优势在于把多个维度合到了一个接口体系里。但对于面向海外用户的平台,还是建议同时接 HIBP 做互补,因为每一家的样本库覆盖范围不一样,没有哪个单一服务商能保证全量。我自己的经验是:leak-check 做国内数据维度的主查,HIBP 做海外邮箱维度的补充,两个结果合并之后再出风险报告,覆盖面会明显好很多。
最后分享一个我自己跑这套流程的习惯。接到一个新的泄漏检测需求,我从来不会直接上全量数据,而是先拿 20 条样本跑一遍,核对返回格式、状态码、限流表现,确认所有预期都正确之后,再放开批量任务。密钥只放环境变量,日志强制脱敏,结果只保留判断需要的字段。这套三步法做下来,帮我处理过个人的账号自查,也帮企业做过客户数据的风险排查,至今还没有因为接口使用不当出过合规问题。希望这篇实战笔记,也能让你少走几个弯路。