基于区块链的二维码门禁系统:防复制、可审计的通行凭证方案
2026/9/16 12:23:53 网站建设 项目流程

简介:基于区块链的二维码门禁系统是一套将区块链技术与二维码门禁场景相结合的实战源码包,面向计算机、信息安全、物联网、数据科学等专业学生与研发人员,可支撑毕业设计、课程设计、大作业或初期项目立项演示。资源共含81个文件,以66个Java归档依赖库为主,包括区块链开发包、二维码处理组件、数据库连接驱动、树莓派硬件控制库等类别;另有5个Java源码、5个编译后的类文件,以及工程配置文件和使用说明文档,压缩包整体约49.25MB。目前已有95人学习。项目代码均经过运行验证,可直接导入集成开发环境调试。内容覆盖区块链链码调用、二维码生成与解析、数据库持久化、树莓派输入输出控制等完整链路,并附有项目说明文档,帮助读者梳理模块关系和调用流程。配套的依赖库按功能分类放置,便于针对不同功能模块单独研究;既适合新手按图索骥完成环境搭建,也能为团队初期立项提供可演示的雏形,整体是一份高价值的综合性学习资料。

1. 基于区块链的二维码门禁系统的核心矛盾不在扫码,而在凭证可复制

普通二维码门禁最大的软肋,不是二维码生成得慢,也不是摄像头识别不了,而是拍一张照片就能进门,改一下有效期就能变成长期凭证。基于区块链的二维码门禁系统,本质是把“谁在什么时间用什么门禁凭证开过哪扇门”这件事变成独立第三方可审计的链上记录,同时把原本只在服务端验签的二维码,改造成由私钥签名的短期通行证。解压这类源码包后,通常能看到签发服务、链上存证合约、门禁端验证 SDK 三个主目录,外加一份说明文档。适合需要访客审计、多园区统一授权、或对门禁操作记录有对账要求的团队阅读和改造。

2. 门禁系统为什么需要链上存证,以及源码包里通常放了什么

2.1 普通签名方案和链上存证的边界在哪里

如果只是防止二维码被篡改,一套 RSA 或 ECDSA 签名就足够,门禁端拿到公钥验签,签名对不上就拒绝。这个方案的问题是:验证方必须信任签发方,一旦签发私钥泄露,所有旧码全部失效,而且没有任何公开痕迹可以让第三方确认“这个码是什么时候签的、由哪把私钥签的、当时关联的门和设备是什么”。区块链进来解决的不是签名问题,而是“记录的可验证性”。

常见的做法是联盟链,比如 Hyperledger Fabric、FISCO BCOS,团队内部维护若干共识节点;也有直接使用以太坊测试网的轻量方案。选联盟链而不是公链,不是因为公链性能一定不够,而是门禁事件是典型的高频、低价值数据,如果每一笔都走公开链,Gas 费用和出块时间都不划算。链上只存哈希、门编号、设备编号、时间戳,不存用户手机号和具体物理位置,避免隐私问题。

2.2 源码包中的模块划分与关键文件

如果你拿到一个“基于区块链的二维码门禁系统完整源码+说明.zip”,解压后通常会看到这么几层:

目录职责关键文件
issuer/签发二维码,维护用户与门禁授权关系src/sign.js,src/token.js
contracts/链上存证合约,记录验签事件AccessLog.sol,Migrations.sol
verify/门禁端本地验签,读卡器或扫码枪接入src/verify.js,device_bridge.py
docs/搭建、参数、接口说明README.md,config.md
scripts/合约部署、初始化节点账号deploy.js,gen_keys.sh

里面的issuerverify通常不是同一套代码,签发端跑在服务器或边缘节点,验签端跑在门禁一体机或树莓派、Jetson 这类设备上。注意门禁端不能依赖网络可用,必须支持离线验签,链上存证只在开门后异步执行。

2.3 一次完整开门请求是怎么流转的

我把典型流程拆成六个步骤,源码包里的测试用例基本都是按这个顺序断言的:

  1. 用户在小程序或后台申请门禁权限,管理员审核通过。
  2. 签发服务生成一个带时间戳、用户 ID、门编号、有效期的载荷。
  3. 签发服务用私钥对载荷签名,把载荷和签名拼成 JSON,编码成二维码。
  4. 门禁设备扫码,解析出载荷,用本地公钥验证签名和时间窗。
  5. 验证通过,设备开门,同时把“载荷哈希 + 门编号 + 设备编号 + 时间”打包发给链上存证服务。
  6. 存证服务调智能合约写入链上,完成审计闭环。

第 4 步的关键是“验签不依赖服务器”,所以门禁设备的时钟不能偏差太大,否则会拒绝有效二维码。第 6 步的存证动作如果失败,门已经开了,但要记录为待重放事件,等网络恢复后再补。源码包里的“说明.zip”里一般会写明这两点,但实际部署时很多人会忽略。

3. 用代码把签发、验签、链上存证跑通

3.1 用 Python 生成带签名的二维码

签发端我习惯用 Python 的qrcodecryptography库,原因是依赖少、门禁端可以复用同一个已验证的私钥格式。先看签发代码:

import qrcode import json import base64 import time from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import load_pem_private_key # 加载签发私钥,实际部署时从 KMS 或加密环境变量读取 with open("./keys/issuer_private.pem", "rb") as f: private_key = load_pem_private_key(f.read(), password=None) def issue_ticket(user_id: str, door_id: str, duration: int = 300) -> str: payload = { "uid": user_id, "door": door_id, "exp": int(time.time()) + duration, "iat": int(time.time()), } # 使用紧凑 JSON,减少二维码内容长度 message = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() signature = private_key.sign(message, ec.ECDSA(hashes.SHA256())) token = { "payload": payload, "sign": base64.urlsafe_b64encode(signature).decode() } content = json.dumps(token, separators=(",", ":")) img = qrcode.make(content) img.save(f"{user_id}_{door_id}.png") return content print(issue_ticket("u_1024", "g_03"))

这段代码的要点是payload里面的四个字段尽量精简,exp是过期时间,iat是签发时间。验签端处理时要重新按sort_keys=True的方式序列化,才能保证签名一致性。sign用 URL-safe Base64,避免二维码里出现+/这类会被扫码枪转义的字符。二维码内容本身没有做加密,任何人都能读出来,所以不要把手机号、身份证放进去。

3.2 链上只存哈希,不存明文凭证

为什么要专门写一个智能合约?因为普通数据库记录可以被管理员删除或篡改,链上存证要把每次开门事件变成一个不可变的审计项。以 Solidity 为例,最小可用的存证合约长这样:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract AccessLog { event Logged(bytes32 indexed ticketHash, uint256 timestamp, address device); // 用哈希作为主键,避免重复上链 mapping(bytes32 => uint256) private ticketToBlock; function logAccess(bytes32 ticketHash, bytes32 deviceId) external { require(ticketToBlock[ticketHash] == 0, "duplicated"); ticketToBlock[ticketHash] = block.number; emit Logged(ticketHash, block.timestamp, msg.sender); } }

ticketHash一般用什么算?常见做法是把签名、门编号、过期时间拼起来做一次 SHA-256。上链后,任何人拿到现场二维码,都可以算出同一个哈希去链上查是否存在对应记录,但查不到用户身份。device参数用msg.sender记录是哪个门禁设备地址提交的,相当于设备指纹。合约里require防重复,同一个二维码被转发到另一台门禁刷第二次时,如果后端已经把这个哈希上过链,就能发现重放;但如果设备先离线验签了,重放检测就要靠设备端缓存,这是后面第 5 章的内容。

3.3 门禁端验签的字段和参数表

门禁端的验签逻辑不能直接把服务端代码拷过来,原因有两点:一是设备 CPU 可能较弱,非对称验签不能频繁做;二是二维码里必须容忍 30 秒内的时钟偏差。一个成熟的验签函数长这样:

import time import json import base64 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import ( encode_dss_signature, decode_dss_signature ) def verify_ticket(token_dict: dict, public_pem: bytes, max_clock_skew: int = 30) -> bool: payload = token_dict["payload"] exp = payload["exp"] now = int(time.time()) if exp < now - max_clock_skew: return False if payload["iat"] > now + max_clock_skew: return False message = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode() sign_bytes = base64.urlsafe_b64decode(token_dict["sign"]) public_key = ec.EllipticCurvePublicKey.from_pem(public_pem) try: public_key.verify(sign_bytes, message, ec.ECDSA(hashes.SHA256())) except InvalidSignature: return False return True

这里有个很容易踩的坑:Pythoncryptography库的 ECDSA 验签在旧版本上要求signature是 DER 编码,而签发端生成的是原始r||s格式。上面的代码没有处理这个差异,实际源码包里通常会带一个_convert_signature工具函数。下面是核心参数表,部署时请对着调:

参数名称建议值说明
exp有效期300 秒访客码建议 3-5 分钟,固定员工码可以放宽但风险高
max_clock_skew30 秒设备和服务端 NTP 同步误差的余量
signature_formatr-s与签发端保持一致,否则验签失败率极高
QR 容错级别M门禁屏常有反光,L容错不够,H会让图案太密
载荷大小< 500 字节超过后扫码识别率下降

4. 用 Docker Compose 在本地部署整套门禁系统,再调这四个参数

4.1 最小环境怎么拉起来

我不会建议第一步就把链、签发服务和门禁设备全部配齐。常见做法是先跑通一个“签发 -> 验签 -> 上链”的最小闭环。源码包里的docker-compose.yml通常定义了三个服务:postgres存用户和授权关系、chain跑一个开发节点、api跑签发和存证接口。解压到本地后,命令大概是:

cd door-blockchain-system cp .env.example .env docker-compose up -d postgres chain npm install npx hardhat node --port 8545 & npx hardhat run scripts/deploy.js --network localhost npm run dev

最后一行npm run dev会启动签发服务,服务默认监听3000端口。hardhat node启动的是内存区块链,重启后区块消失,不能用于生产。部署时把network换成goerli或自有链 RPC 地址即可。如果源码包里没有hardhat.config.js,需要自己加一个 network 配置,核心是chainIdgasPrice要与目标链一致。

4.2 必调参数和它们的业务含义

部署时真正需要调整的参数不是很多,但每个都直接影响门禁可用性。

参数所在文件默认值建议值影响
TOKEN_EXPIRE_SECONDS.env300300太短员工频繁扫码失败,太长截图风险大
BLOCK_CONFIRMATIONS存证服务配置010 表示节点打包即确认,容易回滚丢记录
GATE_CLOCK_SKEW门禁端配置3030超过 60 秒应检查 NTP 服务
CHAIN_SAVE_RETRY存证服务配置35网络抖动时的重试次数
二维码像素签发服务256512门禁屏大但距离远时调高

BLOCK_CONFIRMATIONS是一个容易被忽略的坑。开发时用的hardhat node是即时出块,等 0 个确认没问题。生产链上如果出块时间慢,存证服务等 1 个确认再返回成功,门禁端体验会稍差,但审计记录更可靠。我看到很多项目把确认数调到 0,链上一回滚,门禁记录就对不上了。

4.3 常见的三个启动报错和排查路径

第一类报错是验签失败,签发日志和门禁日志都看不到链上记录。先检查两个服务的TIME_ZONE是否一致,再检查签名格式是不是r-s。第二类报错是二维码扫不出来,尤其在门禁一体机上。常见原因是扫码枪默认的识别模式是纯数字条码,需要先把扫码枪设置成二维码模式,有些设备还要把“后缀回车”关掉,否则会把回车带进载荷里,验签永远失败。第三类报错是合约部署时Gas estimation failed,这通常不是 Gas 不够,而是合约构造函数里有require或链上已经存在同名合约,排查时不看 Gas,先看当前账户有没有ETH

5. 离线优先的进阶玩法:用滑动窗口和重放缓存解决断网验签

门禁设备经常部署在网络不稳定的弱电井、地下车库或园区边缘,不能因为链上节点暂时不可达就把人锁在外面。最后的技巧是让设备端“先验签、后补录”,同时用滑动窗口控制重放风险。

设备端维护一个固定长度的窗口,比如 256 个槽位,每个槽位存最近见过的ticketHash和验签时间。设备在本地验签合法后,先检查ticketHash是否在窗口里,如果在,直接拒绝;如果不在,窗口更新,并用异步任务把哈希提交给链上存证服务。这样即使链上完全不可用,设备也能连续工作几个小时。

窗口长度不是越大越好。门禁设备的存储通常只有几十兆,一个ticketHash是 32 字节,256 个条目也才 8KB,但你要考虑 Redis 或 SQLite 的寻址开销。更可靠的做法是只缓存“最近 5 分钟”的哈希,因为一个二维码的签发有效期最多 300 秒,超过这个时间本来就会被exp拦掉。把窗口时间设置成TOKEN_EXPIRE_SECONDS + 30秒,等于让重放窗口和过期窗口对齐。

具体落到代码上,验签通过后加一行批量去重:

import time from collections import deque class ReplayGuard: def __init__(self, window_seconds=330): self._hits = deque() self._window = window_seconds def seen(self, ticket_hash: str) -> bool: now = time.time() while self._hits and self._hits[0][0] < now - self._window: self._hits.popleft() for ts, h in self._hits: if h == ticket_hash: return True self._hits.append((now, ticket_hash)) return False

这个方法的价值在于,即使二维码被截图发给别人,别人在设备上刷了,第一台设备会放行,第二台设备看到同样的ticketHash会在本地拒绝。链上的存证查询是事后审计,真正拦住转发的是设备端这个滑动窗口。部署时记得把这个守护类嵌入到门禁 SDK 的verify_ticket返回分支里,而不是门禁业务层之外。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询