ARTICLE DETAIL

资讯详情

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

基于区块链的二维码门禁系统:防复制、可审计的通行凭证方案

基于区块链的二维码门禁系统:防复制、可审计的通行凭证方案 简介基于区块链的二维码门禁系统是一套将区块链技术与二维码门禁场景相结合的实战源码包面向计算机、信息安全、物联网、数据科学等专业学生与研发人员可支撑毕业设计、课程设计、大作业或初期项目立项演示。资源共含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.jscontracts/链上存证合约记录验签事件AccessLog.sol,Migrations.solverify/门禁端本地验签读卡器或扫码枪接入src/verify.js,device_bridge.pydocs/搭建、参数、接口说明README.md,config.mdscripts/合约部署、初始化节点账号deploy.js,gen_keys.sh里面的issuer和verify通常不是同一套代码签发端跑在服务器或边缘节点验签端跑在门禁一体机或树莓派、Jetson 这类设备上。注意门禁端不能依赖网络可用必须支持离线验签链上存证只在开门后异步执行。2.3 一次完整开门请求是怎么流转的我把典型流程拆成六个步骤源码包里的测试用例基本都是按这个顺序断言的用户在小程序或后台申请门禁权限管理员审核通过。签发服务生成一个带时间戳、用户 ID、门编号、有效期的载荷。签发服务用私钥对载荷签名把载荷和签名拼成 JSON编码成二维码。门禁设备扫码解析出载荷用本地公钥验证签名和时间窗。验证通过设备开门同时把“载荷哈希 门编号 设备编号 时间”打包发给链上存证服务。存证服务调智能合约写入链上完成审计闭环。第 4 步的关键是“验签不依赖服务器”所以门禁设备的时钟不能偏差太大否则会拒绝有效二维码。第 6 步的存证动作如果失败门已经开了但要记录为待重放事件等网络恢复后再补。源码包里的“说明.zip”里一般会写明这两点但实际部署时很多人会忽略。3. 用代码把签发、验签、链上存证跑通3.1 用 Python 生成带签名的二维码签发端我习惯用 Python 的qrcode加cryptography库原因是依赖少、门禁端可以复用同一个已验证的私钥格式。先看签发代码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(), passwordNone) 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_keysTrue).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_keysTrue的方式序列化才能保证签名一致性。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_keysTrue).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 配置核心是chainId和gasPrice要与目标链一致。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_seconds330): 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返回分支里而不是门禁业务层之外。本文还有配套的精品资源点击获取
返回列表