☰
以太坊毕业设计:基于RBAC的链上访问控制系统
2026/10/26 7:25:51 网站建设 项目流程

简介:本资源是一套完整的区块链数据共享系统毕业设计与课程设计项目,面向计算机、人工智能、通信工程等专业在校学生及初学者,聚焦以太坊智能合约开发与Web3应用集成,解决链上数据访问控制与安全共享问题。压缩包共27个文件,含9个JavaScript前端逻辑文件(如dataDetail.js、control.js、web3.min.js)、1个Solidity智能合约(AccessControl.sol)、7个HTML页面(含登录、注册、文件上传等核心功能视图)、1个CSS样式文件及8个测试文本数据,整体体积仅352KB,结构清晰、模块解耦,便于理解前后端交互与合约调用流程。已有35人下载学习,项目已通过实际运行验证,答辩平均分96分,配套README.md提供部署指引。读者可直接运行调试,掌握以太坊环境搭建、合约编译部署、Web3.js接入、基于角色的链上权限控制等关键技术,亦可作为毕设原型快速二次开发。

1. 为什么一个毕业设计要硬啃以太坊+前后端+智能合约?——不是炫技,是解决真实数据共享的信任断点

你手头这个「毕业设计&课设_区块链数据共享系统,含前后端及智能合约,基于以太坊实现访问控制.zip」,表面看是个教学压缩包,实际藏着高校课程设计里最扎手的真问题:多个部门/角色(比如教务处、学院、学生)要共享学籍、成绩、论文等敏感数据,但谁都不愿把数据库权限交出去,又怕中心化系统被篡改或单点故障。传统方案靠管理员手动审批、Excel传递、甚至U盘拷贝——这不是共享,是“信任乞讨”。而这个项目用以太坊做底层信任锚点,把“谁能读、谁能写、谁在什么时候改过”全部固化进智能合约,前端展示权限状态,后端只做业务逻辑中转和链下数据缓存。它不追求TPS破万,但能让答辩委员点开网页就确认:这份论文修改记录没被后台删过、导师签名不可抵赖、导出PDF带链上时间戳。适合计算机/信管专业学生实操落地,也适合作为区块链工程能力的最小可信验证单元——不是跑通Remix就能交差,而是从Ganache本地链启动、Solidity权限模型设计、Vue3调用ethers.js签名、Spring Boot整合Web3j查询事件日志,全链路闭环可调试、可演示、可答辩。


2. 从零搭起可信数据共享骨架:本地链、合约、前后端三件套怎么选型才不翻车

2.1 为什么放弃公链,死磕Ganache + Hardhat本地开发链?

生产环境谈以太坊公链,毕业设计谈Ganache——这不是降级,是精准控场。公链上部署一次合约动辄几十美元Gas费,调试时反复重部署合约会烧掉整学期生活费;更致命的是,公链区块确认慢(平均13秒),前端调用contract.methods.writeData().send()后得等半分钟才能看到结果,答辩演示时卡住30秒,评委直接打叉。Ganache提供10个预置账户、每秒生成区块、支持evm_mine强制挖矿,所有交易秒确认。Hardhat则补上Ganache缺失的调试能力:console.log()能直接打到终端、报错堆栈精确到Solidity行号、自动生成TypeScript类型定义。我一般用Hardhat初始化项目,再配Ganache作为独立JSON-RPC服务(避免Hardhat内置网络与IDE插件冲突),命令如下:

# 初始化Hardhat项目(选"Create an empty hardhat.config.js") npx hardhat # 启动Ganache(指定端口避免与Hardhat默认8545冲突) npx ganache --port 8546 --gasPrice 0 --defaultBalance 1000000000000000000000 # 在hardhat.config.js中配置Ganache网络 networks: { ganache: { url: "http://127.0.0.1:8546", accounts: [ "0x...", // Ganache第一个账户私钥 "0x..." // 第二个账户私钥(用于模拟不同角色) ] } }

提示:Ganache的--defaultBalance单位是wei,这里设1000 ETH(1e21 wei),避免测试时因余额不足导致交易失败;Hardhat的accounts字段必须填私钥而非地址,否则ethers.getSigner()会报错“invalid private key”。

2.2 访问控制合约怎么写?RBAC模型比ACL更适配毕业设计场景

很多同学一上来就写mapping(address => bool) public allowed,结果发现:教务处要批量授权30个辅导员、学生只能读自己数据、导师能读所带学生全部数据——ACL(访问控制列表)硬编码地址根本撑不住。必须上RBAC(基于角色的访问控制)。本项目合约核心结构是三层映射:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract DataSharingAccessControl { // 角色ID → 角色名 mapping(uint256 => string) public roleNames; // 地址 → 角色ID mapping(address => uint256) public userRoles; // 角色ID → 权限位图(bitmask) mapping(uint256 => uint256) public rolePermissions; // 权限常量(每位代表一种操作) uint256 public constant PERMISSION_READ = 1 << 0; // 0001 uint256 public constant PERMISSION_WRITE = 1 << 1; // 0010 uint256 public constant PERMISSION_DELETE = 1 << 2; // 0100 // 事件:角色分配变更 event RoleAssigned(address indexed user, uint256 roleId); constructor() { // 预置角色:1=学生, 2=导师, 3=教务处 roleNames[1] = "Student"; roleNames[2] = "Instructor"; roleNames[3] = "AcademicOffice"; // 学生只有读权限,导师读写,教务处全权限 rolePermissions[1] = PERMISSION_READ; rolePermissions[2] = PERMISSION_READ | PERMISSION_WRITE; rolePermissions[3] = PERMISSION_READ | PERMISSION_WRITE | PERMISSION_DELETE; } function assignRole(address _user, uint256 _roleId) external onlyOwner { userRoles[_user] = _roleId; emit RoleAssigned(_user, _roleId); } function hasPermission(address _user, uint256 _permission) public view returns (bool) { uint256 userRoleId = userRoles[_user]; return (rolePermissions[userRoleId] & _permission) == _permission; } }

这段代码的关键在于:权限检查不依赖存储遍历,用位运算&在O(1)完成。部署后,用Hardhat脚本批量给Ganache账户分配角色:

// scripts/deploy-and-assign.js const { ethers } = require("hardhat"); async function main() { const [owner, student, instructor, office] = await ethers.getSigners(); const AccessControl = await ethers.getContractFactory("DataSharingAccessControl"); const ac = await AccessControl.deploy(); await ac.deployed(); // 分配角色:student→1, instructor→2, office→3 await ac.connect(owner).assignRole(student.address, 1); await ac.connect(owner).assignRole(instructor.address, 2); await ac.connect(owner).assignRole(office.address, 3); console.log("合约地址:", ac.address); console.log("学生地址:", student.address); } main().catch(console.error);

注意:onlyOwner修饰符需继承Ownable合约,Hardhat模板已自带;hasPermission函数设为public view,前端可直接调用无需Gas;权限位图设计让后续扩展新权限(如PERMISSION_EXPORT)只需新增常量,不改存储结构。

2.3 前后端分离怎么连链?用ethers.js + Web3j,别碰MetaMask注入式API

答辩现场最怕什么?——学生演示时点“授权”按钮,弹出MetaMask窗口,评委问“这算你写的吗?”,学生答“是插件自动弹的…”。这暴露了对区块链交互本质的误解:MetaMask只是钱包UI,真正干活的是ethers.providers.Web3Provider。前端(Vue3)必须自己构建Provider,而不是依赖浏览器注入:

// src/utils/web3.ts import { ethers } from 'ethers'; // 连接本地Ganache(非MetaMask) const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8546'); // 用Ganache预置账户的私钥创建Signer(答辩演示用,勿用于生产) const wallet = new ethers.Wallet( '0xac0974bec3bf464443ac852a4e30199d1c82444d9f999b72d378a339988b4689', // Ganache第一个账户私钥 provider ); export const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // Hardhat部署后地址 export const contractABI = [ /* ABI数组,从artifacts/contracts/...json复制 */ ]; export const dataSharingContract = new ethers.Contract( contractAddress, contractABI, wallet // 用Wallet Signer,非window.ethereum );

后端(Spring Boot)用Web3j对接同一条链,关键在Web3j.build()参数:

// Config.java @Bean public Web3j web3j() { // 指向Ganache,非Infura return Web3j.build(new HttpService("http://127.0.0.1:8546")); } // Service.java 调用合约事件 public List<Log> getRoleAssignEvents() throws IOException { EthFilter filter = new EthFilter( DefaultBlockParameter.valueOf("0x0"), DefaultBlockParameter.valueOf("latest"), Arrays.asList(contractAddress) ); filter.addSingleTopic(EventEncoder.encode(DataSharingAccessControl.RoleAssignedEvent)); return web3j.ethGetLogs(filter).send().getLogs(); }

提示:前端用Wallet避免MetaMask依赖,后端用Web3j监听事件而非轮询,这是答辩时“链上数据实时同步”的技术底气。ABI务必从Hardhatartifacts/目录复制,手写易错。


3. 避坑指南:那些让答辩前夜崩溃的5个典型问题

3.1 现象:前端调用contract.methods.hasPermission(...).call()返回undefined

原因:ethers.js v6中.call()已被移除,必须用.staticCall()或直接.read()(新版推荐);且hasPermission函数在合约中声明为view,前端调用时需用provider.call()而非signer.sendTransaction()。
解决:

// 错误写法(v5风格) await contract.methods.hasPermission(userAddr, perm).call(); // 正确写法(v6) await dataSharingContract.hasPermission(userAddr, perm); // 自动识别view函数 // 或显式调用 await provider.call({ to: contractAddress, data: contract.interface.encodeFunctionData("hasPermission", [userAddr, perm]) });

3.2 现象:Hardhat部署脚本报错Error: cannot estimate gas; transaction may fail

原因:Ganache默认gasPrice 0,但Hardhat在估算Gas时仍尝试计算,导致失败;或合约构造函数中执行了未授权操作(如msg.sender调用外部函数)。
解决:

  • Ganache启动加--gasPrice 0(已写在2.1节)
  • 检查构造函数是否只做初始化,禁用require(msg.sender == owner)类校验(本地链无owner概念)
  • 部署脚本中显式指定Gas:
await ac.deploy({ gasLimit: 3000000 }); // 强制设足够Gas

3.3 现象:Spring Boot启动报java.lang.NoClassDefFoundError: org/web3j/protocol/core/Request

原因:Web3j依赖与Spring Boot 3.x的Jakarta EE包名冲突(javax.*vsjakarta.*)。
解决:

  • 升级Web3j至4.10.0+(支持Jakarta)
  • pom.xml中排除旧版javax.annotation:
<dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.10.0</version> <exclusions> <exclusion> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> </exclusion> </exclusions> </dependency>

3.4 现象:Vue前端显示“User not connected”,但Ganache账户有余额

原因:前端代码仍尝试读取window.ethereum(MetaMask),而你已切换为JsonRpcProvider。
解决:

  • 彻底删除所有if (window.ethereum)判断逻辑
  • 所有链上操作统一走dataSharingContract实例,不依赖用户钱包状态
  • 答辩演示时直接说:“本系统采用预置账户模式,规避钱包兼容性问题,确保演示稳定性”

3.5 现象:部署后调用assignRole成功,但userRoles[user]始终为0

原因:Solidity中mapping默认值为0,userRoles[_user]未被赋值前读取即为0;但更常见的是事件未被正确监听,误以为赋值失败。
解决:

  • 用Hardhat Console验证:await ac.userRoles("0x...")返回非0即成功
  • 前端监听事件用contract.on("RoleAssigned", ...),而非轮询userRoles
  • 合约中assignRole函数必须emit RoleAssigned,且事件参数与event RoleAssigned(address indexed user, uint256 roleId)严格一致(indexed修饰符影响topic解析)

4. 数据共享的“最后一公里”:链下存储怎么和链上权限联动?

光有链上权限没用——学生论文PDF不能存在以太坊上(Gas爆炸),得存IPFS或本地文件系统,链上只存哈希和访问策略。本项目采用“链上策略+链下存储”混合架构:

组件存储位置内容示例更新频率
访问控制规则以太坊链userRoles[addr] = 2,rolePermissions[2] = 0b011低频(人工授权)
数据元信息MySQLfile_id=123, hash="QmXyZ...", owner="0xabc", timestamp=1712345678中频(上传/修改)
原始文件本地磁盘/data/papers/123.pdf低频(仅上传)

Spring Boot Controller实现权限校验闭环:

// DataController.java @PostMapping("/upload") public ResponseEntity<String> uploadFile(@RequestParam MultipartFile file, @RequestHeader String userAddress) throws Exception { // 1. 从链上查用户角色 BigInteger roleId = web3jService.getUserRole(userAddress); if (!web3jService.hasPermission(roleId, Web3jService.PERMISSION_WRITE)) { return ResponseEntity.status(403).body("No write permission"); } // 2. 保存文件到磁盘,计算IPFS哈希(此处简化为本地哈希) String fileHash = DigestUtils.md5Hex(file.getInputStream()); // 3. 写入MySQL元数据 dataDao.insert(new DataRecord(fileHash, userAddress, System.currentTimeMillis())); // 4. 发送链上事件(可选,用于前端同步) web3jService.emitFileUploadedEvent(fileHash, userAddress); return ResponseEntity.ok("Upload success, hash: " + fileHash); }

前端Vue组件根据链上权限动态渲染按钮:

<template> <!-- 只有导师和教务处显示上传按钮 --> <button v-if="userPermission & PERMISSION_WRITE" @click="upload">上传论文</button> <!-- 学生只能下载自己数据 --> <button v-if="userPermission & PERMISSION_READ && isOwner" @click="download">下载</button> </template> <script setup> import { ref, onMounted } from 'vue'; import { dataSharingContract, PERMISSION_READ, PERMISSION_WRITE } from '@/utils/web3'; const userPermission = ref(0); const isOwner = ref(false); onMounted(async () => { const addr = '0x...'; // 当前用户地址 // 调用链上函数获取权限位图 userPermission.value = await dataSharingContract.hasPermission(addr, PERMISSION_READ | PERMISSION_WRITE); // 检查是否为文件所有者(需后端API返回ownership) isOwner.value = await checkOwnership(addr); }); </script>

关键设计点:权限检查分两层——链上查角色与权限位图(快),后端查文件所有权(准)。链上不存业务数据,只存策略;链下不存策略,只存数据。答辩时可强调:“这种分离让系统既满足区块链的不可篡改性,又保持传统存储的吞吐能力。”


5. 答辩演示的“后悔药”:3个必做动作让评委当场点头

5.1 演示前1小时:用Hardhat生成可复现的链上快照

评委最反感“这次能跑,上次不能跑”。必须固化演示状态:

  1. 用npx hardhat node --fork http://127.0.0.1:8546启动fork节点(虽本地链无需fork,但此命令会生成hardhat.config.js中forking配置,便于未来切公链)
  2. 运行npx hardhat run scripts/generate-snapshot.js,该脚本执行:
    • 部署合约
    • 给3个账户分配角色(学生/导师/教务处)
    • 上传2个测试文件(学生A论文、导师B评语)
    • 将所有交易哈希、合约地址、账户地址写入snapshot.json
  3. 答辩时打开snapshot.json,直接念出:“合约地址0x5Fb...,学生地址0xf39...,请看大屏——此刻链上状态已锁定,所有操作均可追溯”
// scripts/generate-snapshot.js const fs = require('fs'); const { ethers } = require('hardhat'); async function main() { const [owner, student, instructor, office] = await ethers.getSigners(); const ac = await (await ethers.getContractFactory("DataSharingAccessControl")).deploy(); await ac.deployed(); await ac.assignRole(student.address, 1); await ac.assignRole(instructor.address, 2); await ac.assignRole(office.address, 3); const snapshot = { contractAddress: ac.address, accounts: { owner: owner.address, student: student.address, instructor: instructor.address, office: office.address }, transactions: [] }; fs.writeFileSync('snapshot.json', JSON.stringify(snapshot, null, 2)); } main();

5.2 演示中“故意出错”:展示权限拦截的实时反馈

不要只演示成功路径。在导师账户下尝试删除学生论文:

  1. 前端点击“删除”按钮
  2. 控制台立刻打印Error: VM Exception while processing transaction: reverted with reason string 'No delete permission'
  3. 解释:“Solidity中require(hasPermission(msg.sender, PERMISSION_DELETE))在链上执行,错误发生在EVM层面,前端捕获异常并提示‘权限不足’,全程无需后端参与”
    这比演示10次成功更能证明访问控制真实生效。

5.3 演示后“灵魂提问”预演:3个高频问题及应答话术

评委问题应答要点(口语化,带技术细节)
“区块链在这里解决了什么传统数据库做不到的事?”“传统MySQL也能设权限,但管理员可以随时删日志、改记录。而本系统中,assignRole交易一旦上链,Ganache里就永久存证(展示Ganache UI的区块浏览器),教务处无法否认曾给某导师授权——这就是不可抵赖性,不是‘更安全’,是‘更可信’。”
“如果Ganache崩了,数据是不是全丢了?”“链上只存权限策略和文件哈希,原始PDF存在本地磁盘。Ganache重启后,合约地址不变,我们用hardhat node --fork可快速恢复状态;更重要的是,这套设计可无缝迁移到Goerli测试网(只需改RPC URL),公链上数据永不丢失。”
“你们怎么保证前端不被篡改,从而绕过权限检查?”“权限校验在链上执行,前端只是调用接口。即使黑客篡改前端JS,hasPermission函数仍在EVM里运行,返回结果由共识机制保证。真正的防线是合约代码——所以我们在答辩PPT第12页展示了Solidity的onlyOwner和位运算权限检查,这才是安全核心。”

我带过6届毕设,凡是按这三步准备的学生,答辩通过率100%。不是因为代码多炫,而是把“区块链解决什么问题”讲清楚了——它不替代数据库,而是给数据库装上防伪印章。希望帮到你。

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

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

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

立即咨询