人脸不能是唯一入口:一套刷脸系统的多通道验证改造实战

 

示例代码的目标运行环境为 Android 8.1 / Android 11(Java 8)与服务端 Spring Boot 3.2(Java 17),本地人脸库为 SQLite 3;离线 SDK 按公开的「初始化 / 注册 / 检索 / 删除」四类调用形态做接口抽象,不绑定具体厂商私有 API。示例代码为改造骨架,未做真机运行验证;文中不含实测性能数据,凡属工程判断的内容在正文中已做标注说明。

一、先说清楚:这次要改的不是算法,是入口

2026 年 9 月 17 日,近期公开通报了一起健身房刷脸系统合规整改案例。通报写得很具体:辖区 5 家健身机构、日常活跃用户约 1.3 万人,均使用第三方 SaaS 系统,被查出的问题是「强制刷脸入场、将人脸授权与办卡协议捆绑、未取得用户单独同意、过期人脸数据长期留存」四条。

整改之后做了什么,同样写在通报里,按技术动作拆开看:

整改动作(通报表述)对应到工程上的东西
增设扫码、人工核验等入场途径入口层增加并行验证通道
前端页面对人脸信息脱敏展示展示层去标识化
后端仅保存加密特征码、无法复原原始人脸信息存储层只留特征值,不留原图
账号分级权限和操作日志留痕访问控制 + 审计日志
开通免费撤回授权、删除数据渠道同意状态可撤回,撤回即删
清理过期数据 1.9 万条retention 定时任务
建立全生命周期台账数据流转记录

注意通报末尾那条责任划分:相关主管部门依据《个人信息保护法》指出,健身房作为个人信息处理者须履行法定义务,技术公司作为受托方须配合完成系统整改。这句话对做交付的团队意义很直接——甲方被查出问题,SDK 集成方不是旁观者。

对应的法规依据是《人脸识别技术应用安全管理办法》(国家互联网信息办公室、公安部令第 19 号,2025 年 3 月 21 日公布,2025 年 6 月 1 日施行)第十条:

实现相同目的或者达到同等业务要求,存在其他非人脸识别技术方式的,不得将人脸识别技术作为唯一验证方式。个人不同意通过人脸信息进行身份验证的,应当提供其他合理、便捷的方式。

这条里有三个被工程团队经常读漏的点:

  • 触发条件是「存在其他非人脸识别技术方式」,不是「用户投诉了」。只要刷卡、扫码、密码、人工任何一种能达成同样目的,人脸就不能是独一份。
  • 替代方式要「合理、便捷」。给个需要打三次电话才能开通的人工通道,不算便捷。
  • 判断时点是「实现相同目的或者达到同等业务要求」。门口闸机的目的是「确认这是已付费会员」,那么会员码同样能达到这个目的,人脸就没有独占的理由。

二、先自查:你的系统算不算「唯一验证」

改造之前先定性。下面这张表是按第十条的构成要件拆的,只要第一列有任何一项命中「是」,系统就处在风险里。

自查项判定为「是」的含义涉及条文
用户拒绝刷脸后,是否无法完成业务直接构成唯一验证第十条
采集人脸是否与主服务协议打包勾选未取得单独同意第六条
是否存在「不录入人脸就不给开卡/不开通」强制,且非必要第四条、第十二条
用户要求删除人脸,是否有自助入口缺少便捷撤回方式第六条
原始人脸照片是否上传到了服务器超出设备内存储要求第八条
人脸数据是否有明确的过期清理机制超出必要保存期限第八条
公共区域设备是否设置了显著提示标识缺少告知第十三条
上线前是否做过个人信息保护影响评估并留档缺少事前评估与记录第九条

第八条在改造里常被忽略。原文是「除法律、行政法规另有规定或者取得个人单独同意外,人脸信息应当存储于人脸识别设备内,不得通过互联网对外传输」。很多系统的架构是设备端提特征、云端存特征并做比对——这在有单独同意的前提下可以走,但如果你连「单独同意」都没有拿到,这条就是硬伤。

三、改造目标:把「人脸」从入口降成通道之一

原始架构通常是这样:

闸机 / App → 人脸采集 → 特征提取 → 1:N 检索 → 开闸

人脸在这条链路里既是入口也是全部通路,任何一环断掉,业务就停。

改造后的目标架构:

              ┌─ 人脸通道(可关闭、可降级)
入口选择层 ───┼─ 二维码 / 会员码通道
              ├─ IC 卡 / 身份证通道
              ├─ 密码 / 短信码通道
              └─ 人工核验通道
                     ↓
              统一验证结果 + 统一审计日志

替代通道怎么选,先看这张对比表。选的依据不是技术先进性,而是「在这个场景里能不能稳定达成同样的目的」:

通道改造量通行体验主要局限适用倾向
二维码 / 会员码低,多数 SaaS 已带快,需掏手机可被转发(需做动态码)会员制场所、闸机
IC 卡 / 门禁卡中,需发卡与补卡流程卡片丢失需挂失园区、宿舍、固定人群
身份证读取中,需读卡器需携带证件政务、酒店、实名核验
短信 / 动态码慢,依赖手机信号弱网环境不可用自助终端、低频场景
密码泄露风险、易忘App 内二次确认
人工核验低,但人力成本高需排班,夜间压力大兜底通道,必须保留

其中二维码要做成动态码,静态会员码被截图转发的风险是实打实的;IC 卡要补齐挂失与重发流程,否则「卡丢了就进不去」和「脸过不了就进不去」在用户体验上是同一个问题。

关键的架构约束只有一条:人脸通道必须可以被单独关闭,且关闭后业务仍然完整可用。这条约束如果满足不了,改再多文案也仍然落回第十条的问题里。

四、代码:验证通道抽象层

先把所有验证方式收进同一个接口。这么做的好处是,人脸在代码里不再是一个特殊分支,而是 channels 列表里的一项。

public interface IdentityVerifier {

    /** 通道标识,用于审计日志与灰度控制 */
    String channel();

    /** 通道是否可用:设备状态、授权状态、开关状态三者都要看 */
    boolean isAvailable();

    /** 执行一次验证,不允许抛业务异常,失败以结果对象返回 */
    VerifyResult verify(VerifyRequest request);
}

请求与结果对象统一:

public final class VerifyRequest {
    public final String scene;      // 例如 "GATE_ENTRANCE" / "APP_LOGIN"
    public final String userIdHint; // 可选,扫码/刷卡场景可携带
    public final byte[] credential; // 各通道自定义:人脸帧、码值、卡号
}

public final class VerifyResult {
    public final boolean passed;
    public final String channel;    // 实际生效的通道
    public final String reasonCode; // 失败原因码,进日志
    public final String matchedUserId;
}

人脸通道实现。注意 isAvailable() 里除了 SDK 状态,还要看业务开关:

public final class FaceVerifier implements IdentityVerifier {

    private final FaceSdk sdk;
    private final FeatureStore store;
    private final FeatureSwitch featureSwitch;

    @Override
    public String channel() {
        return "FACE";
    }

    @Override
    public boolean isAvailable() {
        if (!featureSwitch.isFaceEnabled()) {
            return false;                 // 业务开关:整体停用
        }
        if (!sdk.isLicenseValid()) {
            return false;                 // 授权失效,常见的高发故障
        }
        return sdk.isEngineReady();       // 引擎初始化状态
    }

    @Override
    public VerifyResult verify(VerifyRequest request) {
        if (!isAvailable()) {
            return VerifyResult.fail("FACE", "FACE_CHANNEL_UNAVAILABLE");
        }
        FaceFeature f = sdk.extract(request.credential);
        if (f == null) {
            return VerifyResult.fail("FACE", "FACE_EXTRACT_FAILED");
        }
        Match m = store.searchTop1(f);
        if (m == null || m.score < threshold()) {
            return VerifyResult.fail("FACE", "FACE_NO_MATCH");
        }
        return VerifyResult.pass("FACE", m.userId);
    }
}

五、代码:路由器与自动降级

路由器的职责是「用户没指定通道时,按可用性挑一个;指定了的就走指定的」,并且把降级过程记进日志——第十条要求的是提供替代方式,日志是事后证明你提供了替代方式的硬凭据。

public final class VerifyRouter {

    private final List channels;

    public VerifyResult verify(VerifyRequest request, String preferred) {
        // 1) 用户显式选择的通道优先
        if (preferred != null) {
            IdentityVerifier v = find(preferred);
            if (v != null && v.isAvailable()) {
                return v.verify(request);
            }
            // 指定通道不可用:不阻断,继续走备选
            Audit.log("CHANNEL_FALLBACK", preferred);
        }
        // 2) 按注册顺序挑第一个可用通道
        for (IdentityVerifier v : channels) {
            if (v.isAvailable()) {
                return v.verify(request);
            }
        }
        // 3) 全部不可用:必须落到人工,而不是直接拒绝
        return ManualVerifier.enqueue(request);
    }
}

第 3 步是改造的核心。原来「全部失败 = 拒绝通行」,现在「全部失败 = 转人工」——这一行改动,就是把系统从「人脸是独一份的方式」拉回到「人脸是方式之一」。

人工通道在代码里也要是正式的一等公民,不能是「记在纸上的流程」。它至少要有排队、留痕、超时提醒三件事:

public final class ManualVerifier implements IdentityVerifier {

    private final TicketQueue queue;
    private final AuditLog audit;

    @Override
    public String channel() {
        return "MANUAL";
    }

    @Override
    public boolean isAvailable() {
        return queue.hasDutyStaff();     // 当前是否有人在岗
    }

    @Override
    public VerifyResult verify(VerifyRequest request) {
        String ticket = queue.enqueue(request.scene, System.currentTimeMillis());
        audit.write("MANUAL_TICKET", ticket, request.scene);
        // 人工核验是异步的,返回受理号而不是即时结果
        return VerifyResult.pending("MANUAL", ticket);
    }
}

hasDutyStaff() 这个判断要接值班表,而不是恒返回 true。该案例中整改的难点恰恰在这里——多家 24 小时健身房依赖刷脸闸机实现夜间无人值守,直接拆除设备将增加夜班人力成本或迫使关停夜间时段。通报给出的结论是整改不宜「一刀切」,通报记录的整改动作是在原有基础上增设扫码、人工核验等入场途径。至于夜间无人值守时段具体怎么配替代通道,通报没有涉及,需按各场所排班实际来定(工程判断,非通报原文)。

降级触发条件建议单独成表,方便上线后对照排查:

触发条件检测位置降级去向
人脸业务开关关闭FeatureSwitch扫码 / 刷卡
授权文件过期或校验失败sdk.isLicenseValid()扫码 / 刷卡
引擎初始化失败sdk.isEngineReady()扫码 / 刷卡
连续 N 次活体未通过通道内计数提示改用其他方式
摄像头硬件异常设备自检人工核验
断网且本地库不可用本地库健康检查人工核验

六、代码:单独同意与撤回后的清理

第六条除了单独同意,还有后半句:「个人有权撤回同意,个人信息处理者应当提供便捷的撤回同意的方式」。落到代码里,撤回必须是一个真实的数据删除动作,而不是把状态位改成 0。

public enum ConsentState { NONE, GRANTED, WITHDRAWN }

public final class ConsentService {

    private final ConsentRepo repo;
    private final FeatureStore store;
    private final AuditLog audit;

    /** 同意:必须独立记录来源与时间,不能挂在主协议上 */
    public void grant(String userId, String source, long ts) {
        repo.save(userId, ConsentState.GRANTED, source, ts);
        audit.write("CONSENT_GRANT", userId, source);
    }

    /** 撤回:状态变更 + 特征值删除 + 缓存清理 + 日志,四步缺一不可 */
    public void withdraw(String userId) {
        repo.save(userId, ConsentState.WITHDRAWN, "USER_SELF_SERVICE", System.currentTimeMillis());
        int deleted = store.deleteByUserId(userId);   // 本地人脸库特征值
        store.evictCache(userId);                     // 内存缓存
        audit.write("CONSENT_WITHDRAW", userId, "deleted=" + deleted);
    }
}

配套的存储侧约定,建议写进设计文档:

数据项是否保留位置说明
原始人脸照片 / 视频帧不保留提取完特征即释放
人脸特征值保留设备本地库第八条要求的默认位置
用户标识(脱敏)保留设备本地库用于检索结果回绑
同意记录保留服务端评估与举证需要
审计日志保留服务端第九条要求记录留档
验证通过记录按业务期限服务端到期即清

第九条明确「个人信息保护影响评估报告和处理情况记录应当至少保存 3 年」,所以日志侧的保留期和特征值侧的保留期要分开配置,不能一刀切。

七、代码:过期清理与保存期限

第八条第二款:「人脸信息的保存期限不得超过实现处理目的所必需的最短时间」。这一条在代码上就是一个带参数的清理任务,但参数必须有业务依据,不能随手填 365。

@Component
public class RetentionJob {

    // 30 是占位默认值,不是推荐值:需按业务回看周期确定,本文不给出通用取值
    @Value("${face.retention.days:30}")
    private int retentionDays;

    @Scheduled(cron = "0 30 3 * * ?")   // 每天凌晨 3:30
    public void purge() {
        long deadline = System.currentTimeMillis() - retentionDays * 86400000L;
        int rows = verifyLogDao.deleteBefore(deadline);
        int features = featureStore.deleteOrphan();   // 无对应同意记录的特征值
        audit.write("RETENTION_PURGE", "logs=" + rows, "features=" + features);
    }
}

retentionDays 的取值逻辑建议这样定:门禁通行场景,验证记录通常只需用于当日的纠纷追溯与月度对账,按「业务需要回看的周期上限 + 一个对账缓冲」来定;人脸特征值的有效期则跟着会员/员工的有效期走,状态失效即触发删除。这两个周期不必相等,分开配置反而更容易向甲方解释。

存量盘点要在改造前先做一次,用来确定工作量:

-- 只留了人脸、没有任何替代凭证的用户数
SELECT COUNT(*) FROM user u
WHERE u.face_feature_id IS NOT NULL
  AND NOT EXISTS (SELECT 1 FROM credential c WHERE c.user_id = u.id);

这个数直接决定第三节的入口改造要铺多少量——如果占比很高,说明还需要在首次进入时引导用户补录替代凭证。

八、代码:存量补签与影响评估留档

前面几节都是新系统的写法。已上线的系统还有一个更麻烦的问题:人脸早就采集好了,但当时拿的是打包勾选的同意。按第六条这不成立,需要补——对应通报里的「存量会员补签单独授权」。

补签迁移有一条容易踩的线:不能因为用户没补签就拒绝其入场,那样做又落回第十条了。正确做法是未补签只停用人脸通道,用户改用其他通道,业务不受影响。

@Component
public class ConsentBackfillJob {

    private final ConsentRepo repo;
    private final FeatureStore store;
    private final NotifyService notify;

    /** 每天扫描一次,推进补签状态机 */
    @Scheduled(cron = "0 0 10 * * ?")
    public void advance() {
        // 历史采集、尚未发起补签
        for (String uid : repo.findByState(ConsentState.LEGACY)) {
            notify.sendConsentRequest(uid);          // 站内 + 短信,文案独立成条
            repo.moveTo(uid, ConsentState.PENDING);
        }
        // 超过补签期限未响应:停用人脸通道并删除特征值
        for (String uid : repo.findTimeoutPending(backfillDeadlineDays)) {
            store.deleteByUserId(uid);
            repo.moveTo(uid, ConsentState.TIMEOUT);
            Audit.write("CONSENT_BACKFILL_TIMEOUT", uid);
        }
    }
}

状态流转建议按下面这张表实现,尤其是 TIMEOUT 那一行的处理:

状态含义人脸通道其他通道
LEGACY历史采集,无单独同意可用,但每次提示补签可用
PENDING已发出补签请求可用可用
GRANTED已补签可用可用
WITHDRAWN明确拒绝停用并删除特征值可用
TIMEOUT超期未响应停用并删除特征值可用

TIMEOUT 到期视为未取得同意,特征值删除,但用户资格不受影响——这一条同时满足第六条的同意要求和第十条的不得阻断业务,是迁移方案里关键的一处。

第九条还要求事前做个人信息保护影响评估并留档,报告与处理情况记录至少保存 3 年。这个在代码上就是一个常驻的记录结构,建议直接把条文四项落成四组字段,避免评估做完了却和条文对不上:

public final class PiaRecord {
    public String scenario;            // 应用场景标识
    public String purpose;             // 处理目的
    public String method;              // 处理方式
    // (一) 合法性、正当性、必要性结论
    public String necessityConclusion;
    // (二) 对个人权益的影响与降低影响的措施
    public String impactAndMitigation;
    // (三) 泄露、篡改、丢失、毁损或被非法获取的风险与危害
    public String riskAndHarm;
    // (四) 保护措施是否与风险程度相适应
    public String measureAdequacy;
    public String assessor;
    public long   assessAt;
    public int    retainYears = 3;     // 第九条要求的下限
}

改造本身就是一次「目的、方式发生变化」,按第九条应当重新评估。所以这个记录不是上线前补一份材料,而是每次改通道、改存储位置、改保留期都要更新。

九、提示标识与无障碍通道

第十三条要求在公共场所安装人脸识别设备应当设置显著提示标识。这条常被做成「贴一张纸」,但贴完没有系统侧的任何对应物,事后既说不清贴没贴,也说不清采集区域划在哪。建议把它配置化:

public final class DeviceSignageConfig {
    public String  deviceId;
    public boolean noticePosted;        // 是否已设置显著提示
    public String  noticePhotoId;       // 现场照片留档
    public String  captureZoneDesc;     // 采集区域说明
    public boolean privateSpace;        // 是否涉私密空间,恒为 false
    public long    checkedAt;
}

privateSpace 这个字段看着多余,但它是给巡检用的:第十三条第二款禁止在宾馆客房、公共浴室、公共更衣室、公共卫生间等私密空间内部安装设备,留一个恒为 false 的显式字段,巡检表里就能有这一项,而不是靠人记。

第五条末尾还有一句容易被漏掉的:「处理残疾人、老年人人脸信息的,还应当符合国家有关无障碍环境建设的规定」。落到通道设计上,就是人工通道不能只是「有」,还得可用:

场景无障碍要求实现建议
视力障碍用户无法自主对准摄像头人工通道常驻,不依赖刷脸
老年用户动作指令配合度低活体动作放宽或提供非活体通道
面部损伤用户特征值不稳定刷卡/扫码为主通道
轮椅用户摄像头高度不匹配设备侧增加低位摄像头或人工核验

这一组要求在门禁场景尤其现实。摄像头按站立身高装的,坐轮椅的用户进不了采集区域——如果人脸是唯一入口,实际效果就是把这部分用户挡在门外。这类问题不在条文的直接规定范围内,但第五条要求处理残疾人、老年人人脸信息符合国家有关无障碍环境建设的规定,改造时一并考虑是稳妥做法。

十、改造里常漏的六件事

按实际交付中常见顺序排:

漏项表现后果
只加通道不加开关人脸代码仍是主链路,关不掉出事时无法快速止损
同意书塞进主协议「勾选即同意」的旧文案未改单独同意不成立
撤回只改状态特征值还躺在本地库撤回形同虚设
提示标识缺失设备装了但没贴提示第十三条不满足
人工通道要申请用户拒绝刷脸后需走审批不满足「便捷」
日志没留通道信息只记通过/不通过无法自证已提供替代方式

第六项值得单独说。事后要说明系统确实提供了替代方式,能拿出来的凭据只有日志;如果日志里只有 PASS / FAIL,就区分不出「用户刷脸失败被拒」和「用户改用了别的通道通过」。所以 VerifyResult.channel 这个字段必须落库,不能只在内存里用一下就丢。

十一、改造后的验收清单

上线前跑一遍,全部通过再切全量。

序号验收项判定方式
1关闭人脸开关后业务仍完整可用现场演练
2每个入口至少两个可用通道配置核对
3拒绝刷脸的用户能自助完成验证走查
4同意为独立动作,非打包勾选UI 走查
5撤回入口自助可达,无需人工审批走查
6撤回后特征值确实从本地库删除查库确认
7原始图像不在服务端留存抓包 + 查库
8人脸数据默认存于设备内架构核对
9过期清理任务已配置且有日志查调度记录
10公共场所设备有显著提示标识现场核对
11日志含通道字段,可导出查日志样本
12事前影响评估已做并留档查档案

配套的灰度节奏建议按设备分批,先小范围验证降级链路真的能跑通:

批次范围观察重点
第一批单台设备 / 单个网点降级链路是否真的能走通
第二批单一场景全量替代通道的通行效率
第三批全场景人工通道压力、日志完整性

十二、写在末尾

该案例中值得记录的一句判断——「问题不在技术本身,而在于数据使用边界失守」,以及整改没有一刀切地拆设备,而是在原有基础上增设扫码、人工核验等入场途径。这个结果对做集成的团队是友好的:不需要推翻已交付的系统,需要的是把入口打开。

从工程角度看,这套改造真正有价值的地方不是合规本身,而是它顺带解决了一个长期存在的问题——原来人脸链路一断业务就停,现在授权过期、摄像头故障、引擎初始化失败这些高频故障,都只是降级到另一个通道而已。稳定性收益其实是白捡的。

不过这套改造在不同场景里卡住的地方差别很大,通用思路解决不了具体排期。可自查:正在跑的刷脸系统,人脸之外到底还有没有第二条能用的通道?

具体说,是这三种里的哪一种:

  • 本来就有刷卡或扫码通道,只是没人维护、形同虚设;
  • 人脸确实是独一份,这次要新增通道,卡在硬件采购还是接口改造;
  • 通道好加,卡在存量那批数据——人脸早就采集完了,但当时拿的是打包勾选的同意,补签推不动。

可按「场景 + 现有通道 + 卡在哪一步」这三项梳理。另外有一类情况尚无标准方案:24 小时无人值守的时段,替代通道到底怎么配才不增加夜班人力——通报里没涉及,只能按各场所的排班实际来定,有实际处理过的可以补充做法。

相关文章专栏

专栏一:

专栏二:

专栏三:

专栏四:

参考来源

百度人脸识别离线SDK官网登陆:百度智能云-管理中心
具体授权政策和适配列表请以对应厂商最新文档为准。本文仅讨论技术架构设计,具体合规判定以当地监管要求为准!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值