ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java+Vue区块链电子投票系统:从零实现防篡改存证

2026/9/19 9:14:49 拓冰建站 浏览量
Java+Vue区块链电子投票系统:从零实现防篡改存证 简介一份基于Java与Vue的区块链电子投票防篡改系统设计项目实例面向具备Java、Vue基础的软件工程师、全栈开发者及区块链技术爱好者也适合从事电子政务、数字治理、信息安全等领域的技术人员。文档围绕学校选举、社区自治、企业股东会、政府公共事务等需要高可信度投票的场景阐述如何利用区块链去中心化、不可篡改和可追溯特性结合Spring Boot与Vue构建安全透明的投票平台。资源为1个docx文档约90KB内容涵盖项目背景与目标、整体模型架构、区块链底层数据结构、SHA-256加密工具类、投票数据模型、智能合约判重与自动计票、Java后端认证流程、Vue前端投票组件及部署方案并配有Block区块类、Blockchain区块链类等核心代码示例。已有79人学习适合作为二次开发参考模板快速上手定制化去中心化投票或数据存证系统。1. 电子投票为什么必须上区块链一次数据库改票就能摧毁整个信任体系电子投票系统的最大痛点不是并发性能而是结果如何被信任。传统架构里票数存进 MySQL一条 UPDATE 就能改票SQL 注入或误操作都会让结果作废。区块链的防篡改能力本质上只是多了一条可公开验证的哈希证据链票一旦写入区块后续区块会把前面内容锁死改任何一笔历史投票都会让整条链的哈希校验失败。基于 JavaVue 的电子投票系统最常见的落地形态是 Spring Boot 负责投票接口和区块生成Vue 负责交互与可视化区块链模块独立承担存证与校验不参与业务查询只负责证明数据没被动过。很多人把精力全放在共识和性能上却忽略了上链前的数据入口——入口是裸的链再稳也没用。下面按数据结构、Java 实现、Vue 对接、落地校验四个层面展开重点放在真正决定防篡改成败的链路上。2. 系统分层与账本模型Java 后端、Vue 前端和区块链层的职责边界2.1 一次投票请求的三层流转与模块划分信号先看整体结构。一个由 JavaVue 构成的区块链电子投票系统通常不会把区块链做成独立的微服务而是作为 Spring Boot 工程内的核心模块与投票业务解耦。这样部署简单投票接口和区块生成能在同一个事务边界里协调省去跨服务调用的网络开销前端 Vue 只通过 REST 接口操作不直接接触链上数据。这种前后端分离的架构本身就是 springboot vue 前后端分离项目的标准形态区别只在中间多了一层区块链账本。一次投票请求的流转可以拆成六步。用户在前端 Vue 页面点击候选人axios 把请求 POST 到 Spring Boot 的 /api/vote后端先校验投票人是否重复投票、投票时间是否在有效窗口内校验通过后把投票内容连同投票人的哈希标识、时间戳组装成一个投票事务随后把事务打包进新区块执行简化版工作量证明最后把新区块追加进链同时把投票记录和区块哈希写进 MySQL。这个顺序不能乱先落库还是先上链必须二选一并固定下来。我一般选择先落库标记为待上链区块生成成功后再更新为已上链这样万一进程崩溃数据库里的 pending 状态可以作为补偿重试的凭证。三个模块的职责按这张表切分最清晰模块技术选型核心职责关键实现点前端展示层Vue 3 axios ECharts投票操作、结果展示、链状态可视化vue-router 参数传选票定时轮询校验结果后端业务层Spring Boot MyBatis-Plus投票接口、资格校验、事务控制同一事务内写库并生成区块区块链存证层Java 自研区块模块区块生成、工作量证明、链校验SHA-256 哈希链前块哈希锁定历史从 0 开始搭建一个区块链平台时最容易犯的错是三个模块混在一起写业务接口里直接操作区块列表导致事务边界和校验逻辑完全耦合。正确做法是后端提供独立的 BlockchainService业务层只调它的 addVote 和 validateChain 两个方法内部细节对上层不可见。后端接口层对外暴露的最小集合是四个GET /api/candidates 获取候选人、POST /api/vote 提交投票、GET /api/chain/validate 校验链、GET /api/block/{hash} 查询区块详情。这四个接口对应前端看候选、投票、查健康、核凭证四个动作少一个都不完整。2.2 区块数据结构前块哈希、时间戳与投票哈希的打包方式区块是防篡改的最小单位每个区块需要保存七个字段。previousHash 是关键字段——它把相邻区块串成链任何对历史区块内容的修改都会导致该区块哈希变化进而与它后面那个区块保存的 previousHash 不一致整链校验时立刻暴露。演示工程里最常用的字段设计如下字段类型说明indexint区块高度从 0 开始timestamplong出块时间毫秒级用于投票时间窗口验证voterHashString投票人唯一标识的 SHA-256 哈希voteDataString候选人或票型编码previousHashString前一个区块的哈希防篡改核心hashString当前区块哈希由全部字段计算得到nonceint工作量证明随机数控制出块难度有一个隐私细节必须注意链上不能存明文 voterId 和投票选项描述。区块链数据一旦写入就无法删除投票场景又涉及个人信息保护所以常见做法是存 voterId 的 SHA-256 哈希验证时用哈希匹配voteData 用候选人 ID 编码不要带姓名。2.3 数据库表设计投票记录与区块存证的关联方式业务库的表设计要服务于快速定位链上证据这个目标。投票记录表最核心的字段是 tx_hash 和 block_index它们是把业务记录与区块链关联起来的桥梁字段类型说明idbigint主键voter_hashvarchar(64)投票人哈希与链上 voterHash 一致candidate_idint候选人 IDtx_hashvarchar(64)该票所在区块的哈希block_indexint区块高度statustinyint0 待上链1 已上链2 校验失败create_timedatetime业务落库时间设计上要以链为唯一事实源业务表只做索引和查询加速。查询某张票是否被篡改时只做一次关联根据 voter_hash 拿到 tx_hash 和 block_index调用区块链模块的 getBlock(blockIndex)把区块里保存的 voterHash、voteData 重新计算一遍哈希与 tx_hash 比对。这个比对逻辑放在后端 Service 里统一处理前端只接收 boolean 结果不把哈希计算细节暴露到页面层。这也是面试里常被问到的地方区块链和数据库并存时谁是事实源答案永远是链。提示业务表数据只作查询加速任何以业务表覆盖链上数据的行为都会破坏防篡改语义。3. Java 实现投票上链与防篡改校验从区块生成到整链验证核心类只有两个Block 区块实体和 BlockchainService 区块链服务前者定义数据结构后者封装上链与校验。代码可以在 Spring Boot 工程里直接跑通。3.1 区块实体与 SHA-256 哈希计算的最小实现import org.apache.commons.codec.digest.DigestUtils; public class Block { private int index; private long timestamp; private String voterHash; private String voteData; private String previousHash; private String hash; private int nonce; public Block(int index, long timestamp, String voterHash, String voteData, String previousHash) { this.index index; this.timestamp timestamp; this.voterHash voterHash; this.voteData voteData; this.previousHash previousHash; this.hash calculateHash(); } public String calculateHash() { String input index timestamp voterHash voteData previousHash nonce; return DigestUtils.sha256Hex(input); } // getter / setter 略 }calculateHash 把区块所有业务字段和 nonce 拼接成一个字符串再做 SHA-256 摘要。任何字段的改动都会让摘要完全变化这是篡改能被立即发现的基础。DigestUtils 来自 Apache Commons Codec生产环境不想引依赖的话可以用 JDK 自带的 MessageDigest 封装一个 sha256Hex 工具方法逻辑完全一样。注意拼接顺序必须固定否则不同实例算出的哈希不一致nonce 参与拼接是因为它是工作量证明的变量后续出块时靠它改变哈希结果。初始化时链上要有创世区块否则 addVote 里 chain.get(chain.size() - 1) 会抛越界异常。创世区块的 previousHash 固定为约定值通常全 0业务字段可为空只负责让整条链有起点。工程里用 PostConstruct 在服务启动时检查链是否为空PostConstruct public void init() { if (chain.isEmpty()) { Block genesis new Block(0, System.currentTimeMillis(), genesis, genesis, 0); chain.add(genesis); } }3.2 投票打包与简化工作量证明控制出块节奏Service public class BlockchainService { private static final int DIFFICULTY 4; private final ListBlock chain new CopyOnWriteArrayList(); public Block addVote(String voterHash, String voteData) { Block previous chain.get(chain.size() - 1); Block newBlock new Block( previous.getIndex() 1, System.currentTimeMillis(), voterHash, voteData, previous.getHash()); return mineBlock(newBlock); } private Block mineBlock(Block block) { String prefix 0.repeat(DIFFICULTY); while (!block.getHash().substring(0, DIFFICULTY).equals(prefix)) { block.setNonce(block.getNonce() 1); block.setHash(block.calculateHash()); } chain.add(block); return block; } }addVote 先从链尾拿到前一个区块构造新区块后交给 mineBlock。mineBlock 循环改变 nonce 并重算哈希直到哈希前四位变成 0000。这个规则保证每个区块都要付出一定的计算代价代价由 DIFFICULTY 控制设为 4 表示平均尝试 16 次才能找到一个满足条件的 nonce。改历史区块时不只要改动那一个区块的哈希还要把它之后所有区块全部重算一遍计算成本构成篡改的第一道门槛。注意投票场景里 DIFFICULTY 设为 3 或 4 就足够按比特币的难度会让系统慢到没法用。工作量证明在投票系统里的定位是校验兜底和演示不可抵赖不是性能挑战。线上对出块速度有要求的话也可以只保留前块哈希校验、跳过 PoW但演示版本建议保留能直观展示改票后整链校验失败。另外演示工程里的普通 ArrayList 在并发投票下不安全两个请求同时走到 chain.add 可能互相覆盖生产写法是用 CopyOnWriteArrayList 或给 addVote 加同步保证区块追加原子性。面试聊到区块链和业务并发结合时线程安全是被问得最多的地方。3.3 整链完整性校验定位篡改断点validateChain 是防篡改系统的体检方法每次投票后以及定时巡检时都会调用public MapString, Object validateChainWithDetail() { MapString, Object result new HashMap(); for (int i 1; i chain.size(); i) { Block current chain.get(i); Block previous chain.get(i - 1); if (!current.getHash().equals(current.calculateHash())) { result.put(valid, false); result.put(brokenIndex, i); result.put(reason, 区块自身哈希不一致); return result; } if (!current.getPreviousHash().equals(previous.getHash())) { result.put(valid, false); result.put(brokenIndex, i); result.put(reason, 与前一区块连接断裂); return result; } } result.put(valid, true); return result; }校验分两层。第一层重算当前区块哈希拦截直接修改区块内容的篡改第二层比对当前区块记录的 previousHash 与前一个区块的真实哈希拦截改完旧区块后还想保持链完整的一类操作。两层都通过才算链完整。返回 Map 而不是 boolean是为了告诉调用方链在哪断的、为什么断。曾经在演示环境手工改了第 3 个区块的 voteData再用这个方法体检返回 brokenIndex3 和与前一区块连接断裂。定位断点很关键否则知道链坏了却不知道坏在哪排查成本极高。校验结果含义处理动作validtrue整条链未被篡改页面展示绿色健康状态brokenIndex 且 reason区块自身哈希不一致某个区块内容被直接修改按索引定位投票人冻结结果brokenIndex 且 reason与前一区块连接断裂区块被替换或重建检查是否有异常写入链路4. Vue 前端投票交互与链上数据可视化对接Vue 部分按 Vue 3 Composition API 组织。用户能感知的防篡改能力全在投票凭证 链状态可视化两件事上投票后拿到区块哈希随时可以在页面上看到整条链是否健康。4.1 投票页面与 REST 接口的对接从候选人加载到提交票import { ref, onMounted } from vue import axios from axios const candidates ref([]) const voting ref(false) async function loadCandidates() { const res await axios.get(/api/candidates) candidates.value res.data } async function submitVote(candidateId) { voting.value true try { const res await axios.post(/api/vote, { candidateId: candidateId, token: localStorage.getItem(vote_token) }) voteReceipt.value res.data.hash voting.value false } catch (e) { voting.value false alert(投票失败 e.response.data.message) } } onMounted(loadCandidates)接口对接有三个容易踩的细节。第一token 是后端下发的投票凭证整个投票生命周期只允许使用一次前端必须从 localStorage 读取避免凭证在请求参数里裸奔第二submitVote 里用 voting 状态锁防止用户双击按钮连续提交两个相同请求这是幂等的第一道防线第三投票成功后把区块哈希展示在页面上让用户自己保存——这是可验证的落地形式用户可以拿哈希去区块详情页核对自己的票。如果项目里用 vue-router 传候选人信息推荐用 query 参数而不是把整个对象放进 state页面刷新时 state 会丢query 会保留在 URL 里。vue 项目实战里这类状态丢失问题很常见加一个 URL 参数同步逻辑能省大量排查时间。前端与后端接口的对应关系按这张表对接不会漏接口方法Vue 中的调用位置说明/api/candidatesGET页面加载时候选人列表/api/votePOST点击候选人后提交投票返回区块哈希/api/chain/validateGET定时轮询链健康状态/api/block/{hash}GET投票凭证查询区块详情与哈希比对4.2 用 ECharts 绘制区块校验状态与投票统计链状态可视化最直观的做法是把区块高度和 PoW 计算量画成散点图import * as echarts from echarts const chartDom document.getElementById(chainChart) const myChart echarts.init(chartDom) myChart.setOption({ xAxis: { type: category, name: 区块高度, data: blockList.value.map(b # b.index) }, yAxis: { type: value, name: nonce }, series: [{ type: scatter, data: blockList.value.map(b [b.index, b.nonce]) }] })为什么用 nonce 而不是哈希值做纵轴哈希是 64 位十六进制字符串直接放进坐标轴可读性极差nonce 是每个区块工作量证明找到的随机数数值随机波动能直观反映每个区块的出块成本。结合后端 /api/chain/validate 的结果在图上把校验不通过的区块标红运维人员一眼就能看到链上的异常点。图表容器要配合窗口变化做自适应在 mounted 里挂 window resize 监听并调用 myChart.resize()unmounted 时移除监听否则浏览器窗口拉大后图表会被裁切。4.3 轮询链状态与防篡改提示用 setTimeout 递归代替 setIntervallet timer null async function checkChainStatus() { const res await axios.get(/api/chain/validate) chainStatus.value res.data.valid if (!res.data.valid) { alert(区块链校验失败断点区块 res.data.brokenIndex) } timer setTimeout(checkChainStatus, 30000) } onMounted(() { timer setTimeout(checkChainStatus, 30000) }) onUnmounted(() clearTimeout(timer))setInterval 的问题是如果一次请求耗时超过 30 秒下一次请求会叠加发送造成请求堆积setTimeout 递归保证上一次请求完成后再排下一次天然避免并发堆积。轮询间隔取 30 秒比较合适投票系统不是行情系统不需要秒级刷新太频繁会占用后端校验接口的 CPU因为整链校验每次要重算所有区块哈希。页面销毁时记得 clearTimeout否则组件卸载后回调里操作 DOM 会报错这是 Vue 组件里最常见的资源泄漏点。5. 防篡改系统落地的三个关键校验点时间窗口、幂等与双写一致性区块链本身只负责存证后不可改但不该投的票不能进链靠链做不到必须在业务层卡住。这里讲三个最容易被忽视的校验点也是把演示系统变成可用系统的关键。5.1 校验点一投票时间窗口必须以服务器时间为准区块里的 timestamp 如果取前端提交的时间用户可以改本机时间绕过投票截止也能把历史票伪装成当前票。正确做法是后端收到请求时自己取 System.currentTimeMillis()再与配置的 startTime、endTime 比对long now System.currentTimeMillis(); if (now voteStartTime || now voteEndTime) { throw new VoteException(当前不在投票时间窗口内); }这个校验必须在生成区块之前执行不要放在落库之后否则会出现票已经上链但业务上超时的尴尬状态。前端可以做一个倒计时展示但前端时间只作提示不作依据。5.2 校验点二幂等控制防止同一票在链上重复投票接口天然需要幂等因为网络超时后前端往往会重试。如果重试请求又走了一次上链链上就会出现两张内容一样的票。业务表要为 voter_hash 建唯一索引状态机从待上链到已上链单向流动ALTER TABLE vote_record ADD UNIQUE KEY uk_voter (voter_hash); UPDATE vote_record SET status 1 WHERE voter_hash ? AND status 0;第二行 SQL 影响行数为 0 时说明这条票已处理过直接返回已有区块哈希不再执行上链。这个方案同时解决重复上链和查询已有凭证两个问题比单纯在前端加防双击标记可靠得多。5.3 校验点三业务库与链的双写一致性双写一致的核心原则是数据库状态驱动上链重试。流程是先 INSERT 一条 status0 的投票记录再调用 BlockchainService.addVote成功后把 status 更新为 1。如果上链过程中进程崩溃会残留 status0 的记录用定时任务补偿Scheduled(fixedDelay 60000) public void retryPendingVotes() { ListVoteRecord pending voteRecordMapper.selectByStatus(0); for (VoteRecord record : pending) { if (System.currentTimeMillis() - record.getCreateTime().getTime() 60000) { Block block blockchainService.addVote(record.getVoterHash(), String.valueOf(record.getCandidateId())); record.setTxHash(block.getHash()); record.setBlockIndex(block.getIndex()); record.setStatus(1); voteRecordMapper.updateById(record); } } }触发条件设为记录存在超过 60 秒仍处于 pending说明正常流程已失败。补偿的意义是把链和库从强一致降级为最终一致让系统在进程崩溃、网络抖动时依然能把该上链的票补齐。补偿任务本身要保证幂等addVote 在生成新区块之前先按 voterHash 在链上查一次命中就直接返回已有区块。最终验收时把业务库的 tx_hash 与链上对应区块的 hash 做一次全量比对不一致的记录标红——这是防篡改系统上线前必须做的一轮冗余校验比只看 validateChain 结果更能发现双写链路里的隐蔽断点。本文还有配套的精品资源点击获取