拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP在线生成查询产品防伪证书系统:防伪码生成与查询链路详解

PHP在线生成查询产品防伪证书系统:防伪码生成与查询链路详解 简介PHP在线生成查询产品防伪证书系统源码.zip是一套面向企业及开发者的防伪证书生成与查询解决方案适用于搭建产品防伪验证平台。源码基于PHPMYSQL开发需使用PHP5.1~5.3环境压缩包内自带90套授权证书模板及PSD公章源文件支持后台统一管理和代理商登录入口便于快速部署上线。资源包共1017个文件大小133.2MB主要包含79个PHP核心文件、130个JS脚本、74个CSS样式以及大量GIF/JPG/PNG图片模板和PSD分层源文件覆盖前端展示、交互逻辑、图片资源与证书设计各层面目录结构完整。已有540人学习/下载适合需要快速搭建完整产品防伪查询系统的PHP开发者或中小企业IT人员。通过后台初始账号admin/admin和代理商账号体系可快速了解证书模板切换、防伪查询流程及多角色登录机制降低了从零开发的门槛。1. PHP在线生成查询产品防伪证书系统先想清楚防伪到底防什么做农产品电商的朋友被仿冒品搞得头疼市面上防伪SaaS一年几千块数据还捏在别人手里想改个证书样式都要提工单等排期。用 PHP 在线生成查询产品防伪证书系统这套思路自己搭一套一个虚拟主机加一个域名就能跑起来。核心逻辑不复杂给每件产品发一个唯一防伪码消费者扫码或手动输入系统实时返回一份带产品信息和查询次数的防伪证书。它解决的不是加密算法多高深而是让消费者在 10 秒内确认这是真货——这个信任成本才是防伪真正要解决的问题。适合有 PHP 基础的中小企业开发者、接外包的兄弟以及不想被第三方平台绑死的品牌方。下面从防伪码生成、查询链路到部署避坑按实际落地顺序讲。2. 防伪系统的技术拆解唯一码、证书模板与查询链路2.1 防伪码怎么生成才不重复随机数、唯一索引与字符表选择防伪码是整套系统的命根子。一个合格的防伪码要同时满足四个条件全局唯一、不可预测、便于人工输入、长度适中。唯一性靠数据库唯一索引兜底不可预测性靠随机数源人工输入友好性靠字符表设计。常见的翻车做法是把订单号、自增ID直接当防伪码用或者用srand(3284724); rand();这种固定随机种子生成。前者会被别人按顺序遍历后者在 PHP 7 之后的版本里会因为 srand 重置序列而产生大量重复码。我之前排查过一个客户的项目1 万条码导进去报了一半的 duplicate key原因就是源码里写死了种子。正确做法是用random_bytes()取真随机数再映射到自定义字母表。字母表去掉容易混淆的字符数字 0 和字母 O、数字 1 和字母 I/L只保留ABCDEFGHJKLMNPQRSTUVWXYZ23456789共 32 个字符。这样即使消费者印在贴纸上模糊了输错概率也会低很多。function generate_code(int $length 16): string { $alphabet ABCDEFGHJKLMNPQRSTUVWXYZ23456789; $bytes random_bytes($length); $code ; for ($i 0; $i $length; $i) { $code . $alphabet[ord($bytes[$i]) % strlen($alphabet)]; } return $code; }这段代码先把random_bytes()生成的二进制字节按位取整数值再对字符表长度取模得到字符下标。ord($bytes[$i])返回 0 到 255 的整数% 32之后映射到字符表里。取模会带来轻微的概率偏差但对防伪场景完全够用如果对均匀性有洁癖可以用(ord($bytes[$i]) * strlen($alphabet)) 8这种无偏映射不过没必要。码长 16 位时理论空间是 32 的 16 次方约 1.2 乘以 10 的 24 次方即使每秒遍历 10 亿个码也要几十万年。长度不建议低于 14 位太短容易碰撞超过 20 位消费者手动输入时体验会明显变差。2.2 证书内容与模板渲染把查询结果变成可验证的页面防伪证书不是法律文书本质是验证结果可视化。证书页上至少要有这样几块内容产品名称和型号、批次号、生产日期、防伪码本身、首次查询时间、累计查询次数、当前查询时间。最后三项是关键因为它们构成了这个码之前有没有被人查过的完整证据链。一件正品第一次被扫码时显示首次查询之后每次扫码都提示该码已被查询过 N 次首次查询时间 2024-xx-xx消费者看到这些信息就能自行判断——比如刚买的产品显示被查过 50 次大概率是码泄露了。证书页的渲染方式有两种各有适用场景。第一种是动态渲染每次查询实时查数据库拼 HTML。好处是信息永远最新坏处是高并发时数据库压力大。第二种是首次查询后把证书页落盘成静态 HTML 或 PDF后续请求直接返回静态文件。好处是抗压性能好坏处是证书内容被固化后如果批次信息需要修正已经落盘的页面不会自动更新。我之前建议客户用折中方案证书页始终动态渲染但加 Redis 缓存 30 到 60 秒。正常消费者一次扫码只看一次结果不会在几秒内反复刷新这个缓存窗口足够挡住突发流量又不会让信息滞后太久。模板方面用 PHP 原生拼接字符串最快但一定要对产品名、批次号这些外部输入做htmlspecialchars()转义否则化妆品品牌名里带个引号就能把页面搞乱。不想手动拼 HTML 的话用 Twig 模板引擎也很顺手逻辑和表现分离后后期改证书样式不用动 PHP 代码。2.3 查询链路三个关键环节输入标准化、匹配判断、反馈分级查询链路从消费者扫码或输码开始。二维码携带的是带参 URL指向query.php?codeXXXX手动输入则是用户在网页表单里填码。不管哪种入口进入后端后的第一步都是标准化处理trim()去掉首尾空格strtoupper()统一转大写。这一步必须做因为印刷字体里小写 l 和大写 I 长得几乎一样不统一会让输入和存储永远对不上。匹配环节直接用防伪码的数据库唯一索引查询单条记录查找是微秒级操作。这里有个容易被忽视的细节码不存在时的反馈文案要和码存在但已停用码存在但输入错误区分开但码不存在本身不要返回查无此码这种精确信息——因为攻击者可以通过文案差异判断码是否有效等于帮人家验证猜的码。统一返回请核对防伪码后重试即可。反馈环节要分级处理这是防伪系统体验的核心。码存在且从未被查过展示正品信息并写入首次查询时间码存在且被查过展示完整查询历史并提示用户自行判断码对应的批次已停用展示该批次产品已召回请联系售后。每种情况都要在页面上用醒目的颜色区块区分——绿色是正品橙色是异常灰色是过期或停用。消费者没有耐心读小字说明颜色和图标比文字更有说服力。3. 用 PHP 跑通最小防伪系统数据库设计与核心代码3.1 数据表设计防伪码表与查询日志表分离数据库设计是整个系统里最需要一次做对的部分。我见过有人把查询日志和防伪码放在同一张表里结果查询量一大表膨胀到几百万行每次防伪码检索都被日志 INSERT 拖慢。正确姿势是两张表防伪码主表只管码的状态和查询统计日志表只追加记录每次查询行为两者通过 code_id 关联。下面是核心建表语句字符集用 utf8mb4引擎用 InnoDBCREATE TABLE anti_counterfeit_codes ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL COMMENT 防伪码, batch_id INT UNSIGNED NOT NULL COMMENT 批次ID, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, first_query_time DATETIME DEFAULT NULL COMMENT 首次查询时间, query_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 累计查询次数, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_code (code), KEY idx_batch (batch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE anti_query_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code_id BIGINT UNSIGNED NOT NULL COMMENT 关联防伪码ID, ip VARCHAR(45) NOT NULL COMMENT 客户端IP含IPv6长度, user_agent VARCHAR(255) DEFAULT NULL, query_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_code_id (code_id), KEY idx_query_time (query_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;防伪码表里code字段加了唯一索引这是防重复的最后一道闸门。即使生成代码有 bugINSERT 时数据库也会拒绝重复码并抛出异常让你能及时发现问题而不是悄无声息地产出大量同码。batch_id关联批次表用于证书页展示产品信息status字段用于批次的停用和启用比如印刷错误后要把整批码作废。日志表的ip字段用 VARCHAR(45) 是为了兼容 IPv6 地址IPv6 最长 39 字符加上掩码就是 45很多人默认用 VARCHAR(15)存 IPv6 时直接报错。3.2 防伪码批量生成脚本CLI 方式一口气产十万码生成防伪码的场景是生产一批产品贴一批标得支持一次生成几千到几万条码并且能按批次导入主表。写一个命令行脚本比在网页后台生成更合理——CLI 没有超时压力不占 Web 进程生成完直接输出统计信息。下面是脚本的核心部分#!/usr/bin/env php ?php $pdo new PDO(mysql:host127.0.0.1;dbnameanti_fake, anti_user, password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); $batchId (int)($argv[1] ?? 1); $total (int)($argv[2] ?? 10000); $alphabet ABCDEFGHJKLMNPQRSTUVWXYZ23456789; $codeLen 16; $insert $pdo-prepare(INSERT INTO anti_counterfeit_codes (code, batch_id, query_count) VALUES (?, ?, 0)); $generated 0; for ($i 0; $i $total; $i) { $bytes random_bytes($codeLen); $code ; for ($j 0; $j $codeLen; $j) { $code . $alphabet[ord($bytes[$j]) % strlen($alphabet)]; } try { $insert-execute([$code, $batchId]); $generated; } catch (PDOException $e) { if ($e-getCode() 23000) { $i--; // 唯一键冲突重新生成一个 continue; } throw $e; } if (($generated % 1000) 0) { echo 已生成 {$generated} 条\n; } } echo 批次 {$batchId} 生成完成{$generated} 条\n;这个脚本用 PDO 预处理语句在循环里复用避免了每次 INSERT 都重复解析 SQL 的开销生成 10 万条码的耗时主要花在random_bytes上一般几十秒到两三分钟取决于服务器性能。命令行的第二个参数是生成数量第三个参数是批次 ID运行方式类似php generate_codes.php 12 50000表示给批次 12 生成 5 万条码。遇到唯一键冲突时捕获 23000 错误后回退循环变量重新生成一个码。正常情况下 16 位随机码的碰撞概率几乎为零但保留这个重试逻辑能让脚本在极端情况下自愈不会中途崩掉。生成完成后用SELECT COUNT(*) FROM anti_counterfeit_codes WHERE batch_id 12核对数量确认没有多生成或漏生成。3.3 查询接口与证书页核心查询逻辑的完整代码查询接口是整个系统的门面消费者看到的就是这个页面的返回结果。代码逻辑分为三步参数校验、查码、按状态反馈。下面是去掉模板渲染后的核心链路?php $pdo new PDO(mysql:host127.0.0.1;dbnameanti_fake, anti_user, password, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]); $code strtoupper(trim($_GET[code] ?? )); if (!preg_match(/^[A-Z0-9]{12,20}$/, $code)) { exit(防伪码格式不正确请核对后重新输入); } $stmt $pdo-prepare(SELECT * FROM anti_counterfeit_codes WHERE code ? LIMIT 1); $stmt-execute([$code]); $row $stmt-fetch(); if (!$row) { http_response_code(404); exit(未查询到该防伪码请核对后重试); } if ($row[status] 0) { exit(该批次产品已停用请联系品牌方售后处理); } $now date(Y-m-d H:i:s); $ip $_SERVER[REMOTE_ADDR] ?? ; $ua substr($_SERVER[HTTP_USER_AGENT] ?? , 0, 255); $pdo-prepare(INSERT INTO anti_query_logs (code_id, ip, user_agent) VALUES (?, ?, ?)) -execute([$row[id], $ip, $ua]); if ($row[first_query_time] null) { $pdo-prepare(UPDATE anti_counterfeit_codes SET first_query_time ?, query_count 1 WHERE id ? AND first_query_time IS NULL) -execute([$now, $row[id]]); $isFirst true; } else { $pdo-prepare(UPDATE anti_counterfeit_codes SET query_count query_count 1 WHERE id ?) -execute([$row[id]]); $isFirst false; } // 这里把 $row、$isFirst、$now 传给模板渲染证书页正则校验^[A-Z0-9]{12,20}$直接限制了输入只能由大写字母和数字组成长度 12 到 20 位这是防注入的第一道屏障。注意先strtoupper再trim的顺序——如果先 trim 再转大写用户在码中间输入的空格不会被处理导致查询永远失败。首次查询的 UPDATE 语句里带上了AND first_query_time IS NULL条件这是处理并发查询的关键。两个请求同时查到某条码的first_query_time为空时两个都会尝试执行这条 UPDATE但数据库的行级锁会保证只有一个请求能成功更新另一个请求更新 0 行不会把首次查询时间和次数写错。如果不加这个条件高并发下可能两个请求都把自己当成首次查询把query_count覆盖成 1丢失真实的查询次数。3.4 轻量后台管理批次生成、CSV 导入与查询统计后台管理不需要做成重型后台一个单文件 admin.php 加 session 登录就足够。核心功能有三块按批次生成新码、导入印刷厂提供的码、查看各批次查询统计。导入外部码是防伪系统里很常见的需求——很多品牌找印刷厂做标签印刷厂会预先生成一批码印在贴纸上然后把码文件给品牌方导入系统。格式通常是 CSV每行一个码。导入脚本要做好两件事格式校验和去重。$fh fopen(codes_from_printer.csv, r); $insert $pdo-prepare(INSERT INTO anti_counterfeit_codes (code, batch_id) VALUES (?, ?)); $imported 0; while (($line fgetcsv($fh)) ! false) { $code strtoupper(trim($line[0] ?? )); if (!preg_match(/^[A-Z0-9]{8,20}$/, $code)) { continue; // 跳过格式非法的行但不中断整个导入 } try { $insert-execute([$code, $batchId]); $imported; } catch (PDOException $e) { if ($e-getCode() 23000) { continue; // 重复码跳过记录到日志里最后核对 } throw $e; } } $duplicateCount file($argv[1] ?? ) ? 0 : 0;导入时遇到重复码直接跳过而不是中断是经过踩坑后的选择。印刷厂给的码文件可能有几行重复中断整个导入会让后续几千条有效码也导不进去。跳过之后可以用SELECT COUNT(*) FROM anti_counterfeit_codes WHERE batch_id ?和原文件行数对比算出差额就是重复或非法的数量。统计查询用一条聚合 SQL 搞定每批次产了多少码、有多少被查过、首次查询率一目了然SELECT b.batch_no, b.product_name, c.total, SUM(c.query_count 0) AS queried, SUM(c.first_query_time IS NOT NULL) AS first_queried FROM anti_batches b LEFT JOIN anti_counterfeit_codes c ON c.batch_id b.id GROUP BY b.id;这套后台做完之后整个防伪系统的闭环就出来了生成码、贴标出货、消费者查询、后台看统计。剩下的工作就是把系统接到真实产品上处理二维码打印和并发压力。4. 把防伪系统接到真实产品上二维码、批次绑定与性能优化4.1 二维码生成与标签打印URL 携带参数而非明文码二维码是消费者触达查询页面的主要入口。二维码内容不能直接放明文防伪码——如果消费者扫出来是一串乱码他不知道接下来要去哪查应该放完整的查询 URL让扫码后直接打开证书页。格式形如https://your-domain.com/query.php?codeABC123DEF456GHI7。PHP 生成二维码的常见做法有两个老牌的phpqrcode类库或者用 Composer 安装endroid/qr-code。前者单文件引入即可适合不想引入 Composer 的环境后者 API 更现代支持更多输出格式。无论用哪个都要注意三个参数纠错级别、尺寸、边距。打印在标签上时因为热敏纸可能褶皱、沾染脏污建议用最高纠错级别H 级屏幕展示用 M 级以上就够。标签打印还有一个容易被忽略的点防伪码下方同时印上可读的码字字体用等宽字体比如 Courier New 或 DejaVu Sans Mono。消费者扫码失败时还能手动输入等宽字体保证每个字符宽度相同眼睛不容易看花。标签上加一行刮开涂层扫码查真伪的引导文案能显著提高查询率——很多消费者根本不知道那串码是干什么用的。4.2 批次表与产品信息绑定证书页的信息来源证书页上展示的产品名、型号、生产日期、有效期这些信息不能散乱地存在防伪码表里要通过批次表统一管理。批次表的设计要满足一批产品对应一套信息的规则因为同一批印刷的标签通常对应同一规格的产品。CREATE TABLE anti_batches ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, batch_no VARCHAR(32) NOT NULL UNIQUE COMMENT 批次编号, product_name VARCHAR(100) NOT NULL COMMENT 产品名称, spec VARCHAR(100) DEFAULT NULL COMMENT 规格型号, production_date DATE DEFAULT NULL COMMENT 生产日期, expire_at DATE DEFAULT NULL COMMENT 有效期截止日, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product (product_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;生成防伪码时传入batch_id查询时防伪码表通过batch_idJOIN 批次表取出产品信息拼到证书页上。expire_at字段的设计有讲究查询时如果当前日期超过expire_at证书页仍然显示正品信息——因为码是真的、产品曾经是正品——但额外提示该产品已过有效期请勿使用。防伪和保质是两个维度前者回答是不是真的后者回答能不能用不要混在一起导致误判。批次停用功能也依赖这张表。比如发现某批标签印刷质量差、有些码模糊不清或者产品因质量问题被召回把批次表的status改成 0查询接口就会返回该批次产品已停用。这个操作要能单独针对批次而不影响其他批次所以status放在批次表而不是防伪码表上一个开关管一整批。4.3 查询接口高并发优化Redis 缓存与计数降级防伪查询接口的业务特征非常集中每次查询都要对防伪码表做一次 UPDATE把query_count加一。数据库行锁的粒度是记录级别同一个码被大量并发查询时所有请求会排队等锁吞吐量上不去。双十一这种大促场景爆款产品的防伪码可能一小时内被扫几万次。优化思路分两层。第一层是页面缓存完整证书页渲染好之后写入 Rediskey 为cert:{code}过期时间 30 到 60 秒。缓存命中时直接输出 HTML完全跳过数据库查询和 UPDATE把重复查询从计算降级为读缓存。消费者正常使用下同一件产品在几十秒内被同一个人重复扫码的场景极少缓存对真实用户体验几乎没有影响。第二层是计数器降级。如果连 Redis 都不想引入可以改一下 UPDATE 策略不是每次查询都实时更新query_count而是只保证首次查询写库后续查询每累计到 5 次或 10 次才批量写一次。这样大部分查询变成纯 SELECT数据库压力立刻降下来。代价是统计滞后但对防伪场景完全可以接受——消费者看到的查询次数差个几次不影响判断。$cacheKey cert: . $code; $html $redis-get($cacheKey); if ($html ! false) { echo $html; // 注意此时不记录日志统计有 30 秒延迟 exit; } // 正常查库、渲染 $redis-setex($cacheKey, 30, $renderedHtml);查询日志在缓存命中时也会跳过所以日志统计会呈现突然少了一段监控时要有心理预期。我之前上线这套方案后看到日志曲线出现锯齿状缺口排查半天才发现是缓存生效的正常表现不是丢日志。5. 防伪系统常见问题与避坑记录从伪加密 ZIP 到查询逻辑漏洞5.1 现象源码压缩包伪加密解压报错从网上拿到这套系统的源码下载下来是个 zip 包双击解压却提示输入密码而卖家压根没给密码。这不是系统本身的问题是压缩包被人动过手脚。“zip 伪加密”是个常见的坑zip 文件头的通用位标记general purpose bit flag第 0 位被改成了 1工具软件误以为这是个加密压缩包实际上数据本身并没有加密。解决这个问题有两个路径。Windows 下用 7-Zip 打开如果能正常列出文件列表直接全选复制到本地目录即可7-Zip 对伪加密的容忍度比系统自带解压高得多。Linux 下用自带命令修复zip -F fake_encrypted.zip --out fixed.zip unzip fixed.zip-F参数会尝试修复压缩包的损坏头信息把伪加密标志修正后输出到新文件。如果修复失败直接用 Python 的 zipfile 模块读取很多情况下能绕过这个位标志直接解压。处理完记得用unzip -l fixed.zip先看下文件清单确认里面有query.php、admin.php、database.sql这些关键文件再部署别解压出来才发现拿到的是残缺包。5.2 现象固定随机种子导致防伪码大量重复系统上线没几天后台导入一批新码时数据库报出几十条 duplicate key 错误。查代码发现生成防伪码的地方写着srand(3284724); rand();——这是源码里最常见的反模式。固定种子意味着每次运行生成的随机序列完全相同在 PHP 里 srand 一次之后后续所有 rand() 调用都按同一序列输出只要生成码的循环次数一样产出的码就一样。修复方式很简单把srand和rand()全部换成random_bytes()方案代码见 2.1 节。如果是从旧系统迁移过来的存量码先用一条 SQL 找出重复项SELECT code, COUNT(*) AS c FROM anti_counterfeit_codes GROUP BY code HAVING c 1;把所有重复码提取出来用一个临时映射表标记哪些是无效的、哪些是保留的再把无效码从库里删除。这里要小心删除之前必须确认重复码有没有被贴到产品上如果已经贴了这批产品要补印新标否则消费者的扫码结果会和数据库对不上。5.3 现象查询接口被遍历刷码日志出现连续号段某个批次上线后查询日志里出现大量连续的 codeAAAA0001、AAAA0002 这样的记录全部来自同一个 IP 段。这是典型的枚举攻击攻击者发现你的防伪码是顺序生成的于是从某个起始值开始一路扫下去把有效码全部收集走印成假标签批量造假。根因是码的设计没有随机性。解决分两步走第一步把码生成逻辑改成真随机16 位随机码空间足够大枚举成本高到不现实第二步在查询接口加频率限制同一个 IP 在 60 秒内最多查询 10 次超过就弹验证码或直接返回错误。用 Redis 实现限流只要几行代码$ipKey rate: . ($_SERVER[REMOTE_ADDR] ?? ); $count $redis-incr($ipKey); if ($count 10) { exit(查询过于频繁请 60 秒后再试); } $redis-expire($ipKey, 60);incr在 key 不存在时自动从 0 开始计数expire设置 60 秒过期保证计数不会永久累积。注意这个限流只针对单个 IP如果攻击者用代理池换 IP还要配合查询日志的事后分析比如统计同一防伪码在短时间内被不同 IP 查询的次数是否异常。5.4 现象证书页被脚本批量下载用来伪造防伪证明有客户反馈说网上出现了一批带他家 logo 的防伪证书卖给仿冒品页面内容和他家官网查询结果一模一样。查了下日志发现攻击者先买了一件正品拿到真实防伪码然后写脚本循环请求query.php?codeXXX把每次都一样的证书页完整保存下来作为造假工具的一部分。防伪证书本质是个动态页面它的可信度来自实时性。解决思路是让证书页每次渲染都不一样增加批量复制的成本。做法有几条页面上叠加当前查询时间和查询次数精确到秒格式类似本次查询时间2024-05-20 14:32:07随机生成一个简单的背景水印图案每次加载颜色或排布不同在证书底部显示本页面由系统实时生成手工截图或打印不具备验证效力的提示文字。这些手段堵不死专业攻击但能让批量复制失效——因为存下来的页面永远是某一次查询的历史快照下一次查询时间对不上消费者稍微留意就能发现破绽。防伪的核心从来不是让伪造完全不可能而是让伪造的成本和暴露风险高到不值得。5.5 现象Nginx 环境下 query.php 访问 404本地 PHP 内置服务器跑得好好的部署到云服务器 Nginx 环境后访问https://域名/query.php直接 404但访问静态 HTML 正常。这个问题绝大多数情况下是 Nginx 配置里没有把 .php 请求转发给 PHP-FPM。Nginx 不像 Apache 那样默认支持 PHP 解析必须显式配置 location 规则。一个能正常工作的配置片段如下server { listen 80; server_name anti.example.com; root /var/www/anti_fake; index index.php; location / { try_files $uri $uri/ 404; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }location ~ \.php$用正则匹配所有以 .php 结尾的请求fastcgi_pass指向 PHP-FPM 的监听套接字SCRIPT_FILENAME告诉 FastCGI 要执行的脚本路径。容易踩的细节是try_files那行如果location /里的try_files写成try_files $uri $uri/ 404;且没有后面的location ~ \.php$Nginx 会先按静态文件查找 query.php找不到就直接 404根本到不了 PHP-FPM。排查时先curl -I https://域名/query.php?codetest看返回头再tail -f /var/log/nginx/error.log看有没有 Primary script unknown 之类的报错。5.6 现象接口被跨域请求拦截小程序和网页各自报错证书查询接口部署在主站域名下微信小程序里用wx.request请求接口时报url not in domain list网页端在另一个子域名下用 AJAX 请求时报 CORS 错误。这两个本质都是跨域问题但解决方案不同。小程序端需要在微信公众平台的后台配置 request 合法域名把接口域名加进去并且域名必须备案、支持 HTTPS。网页端的跨域需要后端在响应头里加上 CORS 允许声明header(Access-Control-Allow-Origin: https://your-web-domain.com); header(Access-Control-Allow-Methods: GET, POST); header(Access-Control-Allow-Headers: Content-Type);这里要注意不要把Access-Control-Allow-Origin设置成*。防伪接口涉及产品验证数据限定来源域能挡住一部分跨站恶意请求。还需要给接口兼容 OPTIONS 预检请求浏览器在发送跨域 POST 前会先发一个 OPTIONS后端要直接返回 200 空响应否则正式请求发不出去。6. 进阶给防伪系统加上微信验证、有效期状态流转与自检机制6.1 微信小程序扫码验证的落地方式二维码携带的 URL 可以有两种走向直接跳转网页版查询页或者跳转到微信小程序。后者体验更好但需要在小程序后台配置 request 合法域名并处理用户授权流程。常见做法是扫码后先打开一个中间页页面上有一行打开小程序验真的按钮通过微信的wx.openMiniProgram跳转这样绕开了普通链接直接拉起小程序的限制。小程序端请求查询接口时后端通过code参数正常查询额外根据$_SERVER[HTTP_REFERER]或者自定义请求头判断来源如果是小程序则返回 JSON 格式数据而非 HTML$data [ valid true, product_name $row[product_name], first_query_time $isFirst ? $now : $row[first_query_time], query_count $row[query_count], ]; header(Content-Type: application/json); echo json_encode($data);小程序端拿到 JSON 后在自定义的证书页上渲染。这里的接口要注意返回数据的完整性首次查询标记、查询次数、有效期状态都要包含小程序端才能根据状态展示不同的验证结果页。6.2 有效期与状态流转设计防伪码的完整状态机应该是待激活已生成未查询、已激活首次被查询过、过期超过有效期、停用批次被召回。每个状态对应查询页上不同的展示逻辑。过期和停用要分开处理过期产品提示该产品已过有效期请勿使用停用批次提示该批次产品已停用请联系售后。状态判断在查询链路里用三个字段组合完成first_query_time判断是否激活expire_at判断是否过期status判断是否停用。6.3 自检机制日志审计与人工抽检系统上线后最容易被忽略的是验证系统本身是否正常工作。我习惯每周做一次抽检用 SQL 随机取 10 个已激活的码人工打开查询页核对首次查询时间是否和实际扫码记录吻合检查有没有出现首次查询时间在码生成之前这种数据异常。再抽查一批未激活的码确认它们能正常走完首次查询→写入时间→页面展示流程。日志审计方面重点关注首次查询时间集中在同一秒的批次那通常意味着有人在批量刷码而不是真实用户扫码。做这套系统最大的教训是我头一版把证书页做得很好看防伪码却用了 8 位短随机数结果被人家按顺序遍历整批码全部废掉贴好的标签只能撕下来重印。后来把码加到 16 位、证书页加上实时水印再没出过批量造假的事。防伪不是技术竞赛是让普通消费者一眼看懂这是真的。先想清楚你的查询入口是扫码还是手动输入入口决定体验体验决定消费者愿不愿意用。把这个地基打牢再去折腾短信验证、小程序、PDF 证书这些增值功能。希望帮到你。本文还有配套的精品资源点击获取
返回列表