双因子认证要的是两个不同类别的证据:你知道的、你有的、你是什么。两个密码不算双因子。 门禁的判断就是一个「与」:卡过、脸也过,才开门。四行表格把全部情况穷举完了。 性能上限不在代码,在传感器。 摄像头糊、光不够、卡贴歪,算法再好也没用。
这是全书的最后一篇。第 14 章讲了人脸侦测与识别,这一篇把它和 RFID 拼成一个真东西。 教材附录 C 只有一节:【RFID 门禁系统】。 这一篇全真跑:卡号校验、人脸距离计算、与逻辑判定、日志留痕,都是本机实测。 人脸那一半用的是 14.3 篇那张真图。 只有一处是复刻:没有真的 RFID 读卡器,卡号是写在字典里的。 刷卡这一步在真机上是一次串口读,本课直接给值。
一、为什么要两个因子
| 方案 | 能挡住什么 | 挡不住什么 |
|---|---|---|
| 只刷卡 | 没卡的人 | 捡到卡、借到卡的人 |
| 只认脸 | 陌生人 | 照片、双胞胎、光线差时认错 |
| 卡 + 脸 | 捡到卡也没用,长得像也没用 | 两个都被拿到(概率极低) |
这就是双因子认证:两个不同类别的证据。 「密码 + 短信验证码」也是双因子, 因为一个是「你知道的」,一个是「你有的」。 反过来:两个都是密码就不算双因子。 那只是「两个密码」, 泄露方式往往是同一个——比如都记在同一张便利贴上。 类别大致三类:你知道的(密码、口令)、你有的(卡、手机、密钥)、你是什么(指纹、人脸、虹膜)。 同一类里选两个,等于没加。
为什么「卡 + 脸」比「卡 + 密码」强? 因为卡和脸的窃取方式完全不重叠:卡能被复制,脸不能(至少不能靠一张照片)。
二、刷卡这一半:只认 UID
演示第二段三张卡:04A2B3C4 是王工,04D5E6F7 是小李,04FF0011 是作废卡。
刷 04A2B3C4 进入人脸比对,刷 04DEADBE 没登记被拒,刷 04FF0011 已作废被拒。
三种结果,三条分支,一条都不能漏。
很多老门禁只读卡号,而卡号是能克隆的。 网上几十块的读写器就能复制一张。
所以光刷卡的门禁,安全性不取决于技术,取决于「卡有没有被借出去」。这就是为什么要加第二个因子。 作废卡这条分支容易被漏。员工离职了,卡还留在系统里,那就等于门一直开着。 「删除」和「作废」要分开:删除会让人查不到历史,作废还能追溯。
三、人脸这一半:比对特征
演示第三段:同一个人加一点噪声,距离 9.1;换成一坨随机像素,距离 74.5。 同一个人距离小,不同的人距离大,中间有条线。这条线就是阈值——线上开门,线下拒绝。 口径要说清:这里的「另一个人」是一坨随机像素,不是真人脸。 真实的两个人脸距离会近得多,所以真机上的阈值要靠真实数据调,不能照抄 30。
这跟 14.3 篇那个 0.6 是同一件事:face_recognition 的 128 维特征,阈值 0.6。
自己算像素距离,阈值就得另外定。
阈值定在哪,本质是安全性和便利性的取舍。 定低了,本人被拒之门外;定高了,长得像的人能混进去。
这不是技术问题,是产品决策。
四、两个都过才开门
| 场景 | 卡 | 脸 | 结果 |
|---|---|---|---|
| 卡对、脸对 | 过 | 过 | 开门 |
| 卡对、脸不对 | 过 | 不过 | 拒绝 |
| 卡不对、脸对 | 不过 | 过 | 拒绝 |
| 卡不对、脸不对 | 不过 | 不过 | 拒绝 |
| 作废卡、脸对 | 不过 | 过 | 拒绝 |
只有第一行开门。 卡和脸都得过,缺一样都不行——这就是「与」的逻辑,也是双因子的全部意思。
写成代码就是 if card_ok and face_ok。注意是 and 不是 or。 这个字写错,门禁就等于没有。
第五行值得单独说:卡作废了,就算脸对也不能开。 逻辑上它已经被 card_ok 挡掉了,
但如果代码里把「作废」判成 False 而不是「不在允许集」,就会漏。
这类边界最容易被忽略,因为它只在「离职员工回来试」的时候才暴露。而那时候已经晚了。
五、每次尝试都要留痕
演示第五段四条日志加一条省略:时刻、卡号、距离、结果。记录条数 5。 门禁日志必须记「谁、什么时候、过了没、为什么没过」。 没过的记录尤其重要——连续失败五次,那就是有人在试。 这条规则比任何算法都管用,因为它是唯一能发现「正在被攻击」的信号。
演示末尾把第一条日志打成了 JSON。 这不是装饰:存成 JSON 就能直接被第 7 章那套流程收走,一天一个文件, 或者直接写进 SQLite(12.1 篇)。 还有一个现实问题:日志会越写越多。 要么按天分文件,要么定期归档。 一个几百 MB 的日志文件,出事的时候你打不开。 最后一句得说清楚:人脸是敏感个人信息。 做门禁、考勤这类应用,采集前要告知、要取得同意, 存下来的特征也要当敏感数据保护。
六、上限不在代码,在传感器
这套东西的性能上限不在代码,在传感器。 摄像头糊、光不够、卡贴歪,算法再好也没用。 所以做这类项目的顺序永远是:先解决「拍得清」,再谈「认得出」。 具体一点:补光、调角度、固定相机高度、让人站的位置有个地贴。 这些「不技术」的事,效果比换个更贵的算法明显得多。
这也是 14.2 篇那句话的落地版:工业上自动数零件的难点不在算法,在打光。
同一个道理,换个场景还是它。
全书到这里就完了。
回头看这六篇:13 章让机器跟人说话,14 章让机器看东西,附录 A 讲语法加速器,
B 和 C 把软件接到真实世界。
从 print('hello') 到一个能开门的系统,中间没有魔法,只有一层层的取舍。
七、动手 Demo:门禁:两个因子都过才开门
先看三种方案的强弱,再刷卡:登记卡、没登记的卡、作废卡。 然后算人脸距离,把「卡」和「脸」放进同一张表里判,最后留一份日志。
import json
import os
import cv2
import numpy as np
from PIL import Image
IMG_DIR = 'img' # 放 astronaut.png / coffee.png / chelsea.png
def load(name):
return np.array(Image.open(os.path.join(IMG_DIR, name)).convert('RGB'))
print(' ① 为什么要两个因子')
print(' ' + '-' * 68)
print()
print('| 方案 | 能挡住什么 | 挡不住什么 |')
print('|---|---|---|')
for r in (('只刷卡', '没卡的人', '捡到卡、借到卡的人'),
('只认脸', '陌生人', '照片、双胞胎、光线差时认错'),
('卡 + 脸', '捡到卡也没用,长得像也没用', '两个都被拿到(概率极低)')):
print('| %s | %s | %s |' % r)
print()
print(' 这就是「双因子认证」:两个不同类别的证据。')
print(' 「密码 + 短信验证码」也是双因子,因为一个是「你知道的」,一个是「你有的」。')
print(' 两个都是密码就不算双因子——那只是「两个密码」,泄露方式往往是同一个。')
print()
print(' ② 刷卡这一半:只认 UID')
print(' ' + '-' * 68)
cards = {'04A2B3C4': '王工', '04D5E6F7': '小李', '04FF0011': '作废卡'}
print(' 系统里登记的卡:')
for uid, who in cards.items():
print(' %s → %s' % (uid, who))
print()
for uid in ('04A2B3C4', '04DEADBE', '04FF0011'):
who = cards.get(uid)
if who is None:
print(' 刷 %s → 没登记,拒绝' % uid)
elif who == '作废卡':
print(' 刷 %s → 已作废,拒绝' % uid)
else:
print(' 刷 %s → %s,进入人脸比对' % (uid, who))
print(' 很多老门禁只读卡号,而卡号是能克隆的——网上几十块的读卡器就能复制一张。')
print(' 所以光刷卡的门禁,安全性取决于「卡有没有被借出去」。')
print()
print(' ③ 人脸这一半:比对特征')
print(' ' + '-' * 68)
arr = load('astronaut.png')
gray = cv2.cvtColor(cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2GRAY)
cas = cv2.CascadeClassifier(os.path.join(cv2.data.haarcascades,
'haarcascade_frontalface_default.xml'))
x, y, w, h = cas.detectMultiScale(gray, 1.1, 5)[0]
face = cv2.resize(gray[y:y + h, x:x + w], (100, 100)).astype(float)
rng = np.random.RandomState(20261102)
same = np.clip(face + rng.normal(0, 9, face.shape), 0, 255)
other = np.clip(rng.normal(120, 40, face.shape), 0, 255)
def dist(a, b):
return float(np.sqrt(((a - b) ** 2).mean()))
print(' 同一个人(加了一点噪声):距离 %.1f' % dist(face, same))
print(' 另一个人(一坨随机像素):距离 %.1f' % dist(face, other))
print(' 同一个人距离小,不同的人距离大,中间有条线。这条线就是阈值——线上开门,线下拒绝。')
print(' 注意这里的「另一个人」是一坨随机像素,不是真人脸。')
print(' 真实的两个人脸之间距离会近得多,所以真机上的阈值要靠真实数据调。')
print()
print(' ④ 两个都过才开门')
print(' ' + '-' * 68)
THRESH = 30.0
cases = [
('04A2B3C4', face + rng.normal(0, 9, face.shape), '卡对、脸对'),
('04A2B3C4', other, '卡对、脸不对'),
('04DEADBE', face + rng.normal(0, 9, face.shape), '卡不对、脸对'),
('04DEADBE', other, '卡不对、脸不对'),
('04FF0011', face + rng.normal(0, 9, face.shape), '作废卡、脸对'),
]
print()
print('| 场景 | 卡 | 脸(距离) | 结果 |')
print('|---|:---:|:---:|:---:|')
for uid, f, desc in cases:
card_ok = cards.get(uid) not in (None, '作废卡')
d = dist(face, f)
print('| %s | %s | %s | %s |'
% (desc, '过' if card_ok else '不过',
'过(%.0f)' % d if d < THRESH else '不过(%.0f)' % d,
'开门' if (card_ok and d < THRESH) else '拒绝'))
print()
print(' 只有第一行开门。卡和脸都得过,缺一样都不行——这就是「与」的逻辑,也是双因子的全部意思。')
print()
print(' ⑤ 每次尝试都要留痕')
print(' ' + '-' * 68)
log = []
for uid, f, desc in cases:
card_ok = cards.get(uid) not in (None, '作废卡')
d = dist(face, f)
log.append({'时刻': '2026-11-02 08:%02d:%02d' % (30 + len(log), 11 * len(log) % 60),
'卡号': uid,
'距离': round(d, 1),
'结果': '开门' if (card_ok and d < THRESH) else '拒绝'})
for r in log[:4]:
print(' %s %s 距离 %.1f %s' % (r['时刻'], r['卡号'], r['距离'], r['结果']))
print(' …')
print(' 记录条数 %d' % len(log))
print(' 门禁日志必须记「谁、什么时候、过了没、为什么没过」。')
print(' 没过的记录尤其重要——连续失败五次,那就是有人在试。')
print(' 这条日志存成 JSON 就能直接被第 7 章那套流程收走:一天一个文件、或者直接写进 SQLite。')
print()
for ln in json.dumps(log[0], ensure_ascii=False, indent=2).split('\n'):
print(' ' + ln)
print(' 最后一句实话:这套东西的性能上限不在代码,在传感器。摄像头糊、光不够、卡贴歪,算法再好也没用。')
print(' 所以做这类项目的顺序永远是:先解决「拍得清」,再谈「认得出」。')
实跑输出:
--------------------------------------------------------------------
门禁:两个因子都过才开门
--------------------------------------------------------------------
① 为什么要两个因子
--------------------------------------------------------------------
| 方案 | 能挡住什么 | 挡不住什么 |
|---|---|---|
| 只刷卡 | 没卡的人 | 捡到卡、借到卡的人 |
| 只认脸 | 陌生人 | 照片、双胞胎、光线差时认错 |
| 卡 + 脸 | 捡到卡也没用,长得像也没用 | 两个都被拿到(概率极低) |
这就是「双因子认证」:两个不同类别的证据。
「密码 + 短信验证码」也是双因子,因为一个是「你知道的」,一个是「你有的」。
两个都是密码就不算双因子——那只是「两个密码」,泄露方式往往是同一个。
② 刷卡这一半:只认 UID
--------------------------------------------------------------------
系统里登记的卡:
04A2B3C4 → 王工
04D5E6F7 → 小李
04FF0011 → 作废卡
刷 04A2B3C4 → 王工,进入人脸比对
刷 04DEADBE → 没登记,拒绝
刷 04FF0011 → 已作废,拒绝
很多老门禁只读卡号,而卡号是能克隆的——网上几十块的读卡器就能复制一张。
所以光刷卡的门禁,安全性取决于「卡有没有被借出去」。
③ 人脸这一半:比对特征
--------------------------------------------------------------------
同一个人(加了一点噪声):距离 9.1
另一个人(一坨随机像素):距离 74.5
同一个人距离小,不同的人距离大,中间有条线。这条线就是阈值——线上开门,线下拒绝。
注意这里的「另一个人」是一坨随机像素,不是真人脸。
真实的两个人脸之间距离会近得多,所以真机上的阈值要靠真实数据调。
④ 两个都过才开门
--------------------------------------------------------------------
| 场景 | 卡 | 脸(距离) | 结果 |
|---|:---:|:---:|:---:|
| 卡对、脸对 | 过 | 过(9) | 开门 |
| 卡对、脸不对 | 过 | 不过(74) | 拒绝 |
| 卡不对、脸对 | 不过 | 过(9) | 拒绝 |
| 卡不对、脸不对 | 不过 | 不过(74) | 拒绝 |
| 作废卡、脸对 | 不过 | 过(9) | 拒绝 |
只有第一行开门。卡和脸都得过,缺一样都不行——这就是「与」的逻辑,也是双因子的全部意思。
⑤ 每次尝试都要留痕
--------------------------------------------------------------------
2026-11-02 08:30:00 04A2B3C4 距离 9.1 开门
2026-11-02 08:31:11 04A2B3C4 距离 74.5 拒绝
2026-11-02 08:32:22 04DEADBE 距离 9.1 拒绝
2026-11-02 08:33:33 04DEADBE 距离 74.5 拒绝
…
记录条数 5
门禁日志必须记「谁、什么时候、过了没、为什么没过」。
没过的记录尤其重要——连续失败五次,那就是有人在试。
这条日志存成 JSON 就能直接被第 7 章那套流程收走:一天一个文件、或者直接写进 SQLite。
{
"时刻": "2026-11-02 08:30:00",
"卡号": "04A2B3C4",
"距离": 9.1,
"结果": "开门"
}
最后一句实话:这套东西的性能上限不在代码,在传感器。摄像头糊、光不够、卡贴歪,算法再好也没用。
所以做这类项目的顺序永远是:先解决「拍得清」,再谈「认得出」。
第二段那张卡表里,04FF0011 是「作废卡」而不是「不存在」。
这个区别很重要:作废的卡还能查到历史。
第三段那两个距离(9.1 和 74.5)用的是第 14 章那张真脸。
注意「另一个人」是一坨随机像素,不是真人——真实的两张脸会近得多。
第四段那张表是全篇的核心。四行里只有第一行开门,其余全拒——写错一个 and,这张表就废了。
第五段那个 JSON 只有四个字段,但它能被第 7 章的流程直接收走。日志的格式比日志的量重要。
最后那句「上限在传感器」不是客套。做过门禁的人都知道,调算法的时间远不如调光的时间值。
30 秒实验
- 把演示第四段的
THRESH从 30 改成 20,看第二行(卡对、脸不对)会不会变成开门。 - 把演示第二段
cards里的04FF0011整条删掉,再刷它,看走的是哪个分支——跟「作废」有什么区别? - 把演示第五段的日志改成写进一个 SQLite 文件(12.1 篇那套),再写一条查询数「今天失败了几次」。
常见误区
- 「双因子就是两种验证方式。」必须是两个不同类别。两个密码、两张卡都不算。
- 「卡号唯一,不会重复也不会被复制。」卡号能克隆,几十块的读写器就行。 刷卡能证明「你有这张卡」,证明不了「你是本人」。
- 「算法调好就万事大吉。」上限在传感器。 光不够、图糊了,算法再好也认不出。
自测三问(点开对答案)
1. 为什么「卡 + 脸」算双因子,而「密码 + 另一个密码」不算? —— 双因子要求两个证据属于不同类别:你知道的、你有的、你是什么。 卡是「你有的」,脸是「你是什么」,两类不同。 两个密码都属于「你知道的」,泄露方式往往也一样(比如都写在同一张纸上), 所以只是「两个密码」,不是双因子。 2. 门禁的判断为什么要用「与」而不是「或」? —— 因为双因子的意思是「两个都成立才放行」。 用「或」的话,只要有卡或者只要脸对就能进,等于把安全性降到较弱的那一半。 演示里五行表格只有第一行开门,其余全拒,这就是「与」的全部含义。 3. 为什么说门禁的性能上限不在代码,在传感器? —— 因为算法再好,输入是糊的、暗的、歪的,它也认不出来。 摄像头分辨率、补光、拍摄角度、人站的位置,这些决定了上限。 所以做这类项目的顺序是「先解决拍得清,再谈认得出」,调光往往比调算法有用得多。全书完。回头看,从第一行的 print 到这里,中间没有魔法,只有一层层的取舍。
- ☐ (全书完)