简介:本资源是一套面向本科毕业设计的分布式身份认证系统用户端实现,基于Hyperledger Fabric区块链构建可信身份管理体系,适用于信息安全、区块链开发与Java后端方向的学习者与毕设开发者。项目采用SpringBoot框架搭建,完整覆盖用户注册、DID文档管理、凭证申领与验证等核心交互流程,具备工程可运行性与教学示范价值。压缩包共119个文件,含39个Java源码文件(如DidDoc、Issuer、AppServiceImpl等核心业务类)、39个编译后class文件、26个日志文件用于调试追踪、9个XML配置及2个YAML配置文件,整体仅183KB,轻量易部署。目前已有84人学习下载,资源结构清晰,包含全局异常处理、AOP切面控制、注册中心服务等典型企业级模块,可直接用于毕设开发、课程实验或区块链身份认证原理实践。
1. 这不是又一个 SpringBoot 登录页:它用 Hyperledger Fabric 把“我是我”变成链上可验证事实
你写过多少个「用户注册 + 登录 + JWT 验证」的 SpringBoot 模块?我数不清了——直到去年帮学院一个毕业设计组调通 Fabric CA 和 SDK 的交互链路,才真正意识到:传统账号体系里那个「密码重置」按钮,本质是中心化系统对你身份控制权的一次让渡。而这份基于 Hyperledger Fabric 的分布式身份可信认证用户端,干了一件更硬核的事:它不存密码,不发 token,而是把用户 DID(Decentralized Identifier)和对应的 DidDoc(去中心化身份文档)直接锚定在 Fabric 区块链上,每一次身份声明(比如「我是某高校毕业生」)都由链上背书节点签名验证,前端交互流程全程驱动链上状态机切换。它不是替代 SpringSecurity,而是把 Security 的信任根从数据库表迁移到共识层。适合正在做区块链方向毕设、想避开「增删改查+Vue 页面」同质化陷阱的同学,也适合想亲手拆解 DID 在联盟链落地细节的工程师——毕竟 Fabric 不是 Ethereum,没有 ERC-1056 那种开箱即用的 DID 合约,所有 DidDoc 管理、密钥轮换、服务端点注册,都得靠你手写 Fabric SDK 调用逻辑,再用 SpringBoot 封装成 REST 接口。项目虽小,但五脏俱全:从用户注册触发 Fabric CA 证书签发,到 Issuer 机构发起凭证颁发请求,再到客户端本地解析链上 DidDoc 并构造可验证凭证(Verifiable Credential),每一步都踩在 Fabric 权限模型和 DID 规范的交界处。
2. 从 SpringBoot 工程结构看 Fabric DID 的分层职责:为什么 ControllerAop 和 RegisterCenterServiceImpl 是关键枢纽
2.1 工程模块划分:三层信任边界如何映射到代码包结构
这个 SpringBoot 项目没用典型的controller-service-dao三层,而是按 Fabric 身份生命周期重构了包结构:
com.example.did.controller:只暴露/did/register、/did/issue、/did/verify三个核心端点,拒绝任何与链下用户表关联的 CRUD 接口。所有身份操作必须携带 DID 字符串或签名 payload,ControllerAop.class 就在这里拦截并校验签名有效性。com.example.did.service.impl:包含DidDocServiceImpl(管理链上 DidDoc 的 CRUD)、IssuerServiceImpl(模拟权威机构签发 VC)、RegisterCenterServiceImpl(对接 Fabric CA,处理证书注册与撤销)。注意:这里没有UserServiceImpl——因为 Fabric 中「用户」就是其私钥持有者,SpringBoot 层只维护 DID 与本地密钥文件路径的映射关系。com.example.did.model:DidDoc.class是核心数据结构,字段严格遵循 W3C DID Core 规范:id(DID URI)、verificationMethod(含公钥、类型、用途)、service(指向链上合约地址或 off-chain 服务端点)、authentication(签名验证方法数组)。PredefinedMaterials.class则预置了测试用的 CA 根证书、MSP 配置、通道配置等 Fabric 连接元数据。
提示:
BaseResponse.class统一封装返回体,但关键在于它的data字段永远不返回明文私钥或原始证书——所有敏感材料仅通过@JsonIgnore注解屏蔽,或由RegisterCenterServiceImpl生成后直接写入用户本地 keystore 文件,SpringBoot 层绝不缓存。
2.2 DidDocServiceImpl:如何用 Fabric SDK 把 JSON-LD 格式的 DidDoc 写进区块链
Fabric 不支持直接存储任意 JSON,所以 DidDoc 必须序列化为字节数组并存入指定 key。DidDocServiceImpl的核心逻辑如下:
// DidDocServiceImpl.java public void saveDidDoc(String did, DidDoc didDoc) throws Exception { // 1. 将 DidDoc 对象转为紧凑 JSON-LD 格式(移除空格、缩进) String jsonLd = JsonUtils.toJsonCompact(didDoc); // 2. 构造 Fabric 交易提案:key = "did:" + did, value = jsonLd.getBytes() TransactionProposalRequest request = new TransactionProposalRequest(); request.setChaincodeID(chaincodeID); request.setFcn("putDidDoc"); // 链码函数名 request.setArgs(new String[]{did, Base64.getEncoder().encodeToString(jsonLd.getBytes())}); // 3. 提交提案并等待背书 Collection<ProposalResponse> responses = channel.sendTransactionProposal(request); for (ProposalResponse response : responses) { if (!response.getStatus().equals(ChaincodeStatus.SUCCESS)) { throw new RuntimeException("DidDoc save failed: " + response.getMessage()); } } }这段代码背后有三个硬性约束:
- Key 命名规范:必须以
did:开头,这是 Fabric 链码中putDidDoc函数做前缀校验的依据; - Value 编码方式:用 Base64 而非 UTF-8 直传,避免 JSON 中的特殊字符(如
+,/)被 Fabric gRPC 层误解析; - 链码函数契约:
putDidDoc必须实现幂等写入(相同 DID 多次提交只更新最新版本),且需校验didDoc.id与传入did参数一致——否则攻击者可伪造他人 DidDoc。
2.3 RegisterCenterServiceImpl:Fabric CA 交互不是配个 URL 就完事
RegisterCenterServiceImpl承担着将 SpringBoot 用户请求翻译成 Fabric CA 操作的职责。它不直接调用 CA REST API,而是通过 Fabric SDK 的HFCAClient实例完成:
// RegisterCenterServiceImpl.java public Enrollment enrollUser(String username, String password) throws Exception { // 1. 创建 HFCAClient 实例,加载 CA TLS 证书 HFCAClient caClient = HFCAClient.createNewInstance(); caClient.setCARegistrationProperties(caProperties); // 包含 CA URL、TLS 证书路径 // 2. 生成 ECDSA 密钥对(非 RSA!Fabric 默认用 secp256r1) CryptoSuite cryptoSuite = CryptoSuite.Factory.getDefault(); Enrollment enrollment = caClient.enroll(username, password); // 3. 将 enrollment 秘钥保存到本地 keystore(路径由 username 决定) KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null, null); keyStore.setKeyEntry(username, enrollment.getKey(), password.toCharArray(), new Certificate[]{enrollment.getCert()}); Files.write(Paths.get("keystore", username + ".p12"), keyStoreToBytes(keyStore, password)); return enrollment; }关键参数说明:
caProperties必须包含tls-ca-rootcert字段,指向 Fabric CA 的根证书(通常为ca.crt),否则 TLS 握手失败;enroll()返回的Enrollment对象包含getKey()(私钥)和getCert()(X.509 证书),二者共同构成 Fabric 中的「身份凭证」;- Keystore 格式必须为 PKCS12:SpringBoot 默认的 JKS 格式不被 Fabric SDK 支持,且密码必须与用户名一致(这是 Fabric CA 的默认策略,不可绕过)。
3. 用户交互流程的四个链上动作:从注册到凭证验证的完整时序图落地
3.1 用户注册:不是插入数据库,而是向 Fabric CA 申请身份证书
用户点击「注册」按钮后,前端发送 POST 请求到/did/register,携带username和password。后端流程如下:
| 步骤 | SpringBoot 动作 | Fabric 层动作 | 关键约束 |
|---|---|---|---|
| 1 | RegisterCenterServiceImpl.enrollUser()被调用 | Fabric CA 生成 ECDSA 密钥对,签发 X.509 证书 | 用户名不能含特殊字符(CA 默认正则^[a-zA-Z0-9._-]+$) |
| 2 | DidDocServiceImpl.createInitialDidDoc()构建初始 DidDoc | 链码putDidDoc将 DidDoc 存入 world state | id字段格式必须为did:fab:<msp-id>:<username>,其中<msp-id>来自 Fabric 网络配置 |
| 3 | IssuerServiceImpl.registerAsIssuer()向链上注册发行方身份 | 调用registerIssuer链码函数,写入 issuer DID 和公钥 | 发行方必须已通过 Fabric CA 认证,且 MSP ID 与通道成员一致 |
注意:此时用户尚未获得任何「凭证」,只是拥有了链上可验证的 DID 和对应密钥。真正的身份证明来自后续 Issuer 的背书。
3.2 凭证颁发:Issuer 如何用 Fabric 签名生成可验证凭证(VC)
当用户提交学历证明材料后,Issuer 端调用/did/issue接口。IssuerServiceImpl.issueCredential()的核心逻辑是:
// 构造 VC 的核心 payload(符合 W3C Verifiable Credentials Data Model) Map<String, Object> vcPayload = new HashMap<>(); vcPayload.put("type", Arrays.asList("VerifiableCredential", "UniversityDegreeCredential")); vcPayload.put("issuer", issuerDid); // Issuer 的 DID vcPayload.put("issuanceDate", Instant.now().toString()); vcPayload.put("credentialSubject", subjectData); // 用户学籍信息 // 使用 Issuer 的私钥对 payload 签名(非 Fabric 签名,而是 JWT 或 LD-Signatures) String jwt = JwtUtils.sign(vcPayload, issuerPrivateKey, "ES256"); // 将 JWT 存入 Fabric world state,key = "vc:" + UUID.randomUUID() chaincodeClient.invoke("storeVC", "vc:" + UUID.randomUUID(), jwt);这里的关键是:Fabric 不参与 VC 签名过程,它只作为安全存储层。VC 的密码学签名由 Issuer 本地完成(使用 ES256 椭圆曲线算法),Fabric 仅保证该 JWT 不被篡改——因为任何修改都会导致链上哈希值变化,验证时比对失败。
3.3 凭证验证:前端如何用链上 DidDoc 验证 VC 签名有效性
用户拿到 VC JWT 后,验证流程完全离线进行(无需调用后端):
- 解析 JWT header,提取
kid(密钥 ID); - 根据
kid查询链上 DidDoc(调用/did/doc/{did}),定位verificationMethod数组中匹配的公钥; - 用该公钥验证 JWT signature,同时检查
issuanceDate是否在有效期内; - 若验证通过,前端显示「学历信息已由 XX 大学链上认证」。
提示:
ControllerAop.class在此环节起关键作用——它拦截所有/did/doc/*请求,强制校验请求头中的Authorization: Bearer <user-jwt>,确保只有持有对应 DID 私钥的用户才能读取自己的 DidDoc。这避免了 DidDoc 数据被爬虫批量抓取。
3.4 身份注销:撤销不是删数据,而是写入「已撤销」状态
用户点击「注销身份」时,DidDocServiceImpl.revokeDidDoc()执行:
public void revokeDidDoc(String did) throws Exception { // 1. 读取当前 DidDoc String currentJson = getDidDocFromChain(did); DidDoc doc = JsonUtils.fromJson(currentJson, DidDoc.class); // 2. 设置 revoked 字段为 true,并更新 updated 时间戳 doc.setRevoked(true); doc.setUpdated(Instant.now().toString()); // 3. 再次调用 putDidDoc 写入新版本(Fabric world state 支持多版本) saveDidDoc(did, doc); }Fabric 的 MVCC(多版本并发控制)机制保证:即使旧版本 DidDoc 仍存在于历史记录中,getDidDocFromChain()默认返回最新版本。验证方只需检查revoked字段即可判定当前 DID 是否有效——这比 Ethereum 上的链上撤销合约更轻量,也更符合 Fabric 的企业级场景。
4. Fabric DID 开发的四大避坑指南:那些让调试时间翻倍的隐性约束
4.1 现象:HFCAClient.enroll()报错Failed to connect to CA server
原因:Fabric CA 容器启动时未正确挂载 TLS 证书,或caProperties中的tls-ca-rootcert路径指向错误。Fabric SDK 要求该证书必须是 PEM 格式且以-----BEGIN CERTIFICATE-----开头,若用openssl x509 -in ca.crt -outform DER转成了二进制 DER 格式,SDK 会静默失败。
解决:进入 CA 容器执行cat /etc/hyperledger/ca-server-config/ca.crt,确认输出为 PEM 格式;在 SpringBoot 中将证书路径设为绝对路径(如/opt/app/ca.crt),避免 classpath 加载时因打包方式不同导致路径解析失败。
4.2 现象:putDidDoc链码调用返回ENDORSEMENT_POLICY_FAILURE
原因:通道的背书策略(Endorsement Policy)要求至少 2 个 Peer 背书,但实际只连接了 1 个 Peer 实例。channel.sendTransactionProposal()默认只向第一个可用 Peer 发送提案,若该 Peer 因网络问题未响应,整个交易失败。
解决:在Channel初始化时显式设置背书 Peer 列表:
channel.addPeer(peer1); // MSP ID: Org1MSP channel.addPeer(peer2); // MSP ID: Org2MSP channel.setEndorsers(Arrays.asList(peer1, peer2)); // 强制双背书4.3 现象:前端解析 DidDoc 时verificationMethod[0].publicKeyJwk字段为空
原因:DidDocServiceImpl.createInitialDidDoc()中构建verificationMethod时,直接用了enrollment.getCert()的 DER 编码,但 W3C DID 规范要求 JWK(JSON Web Key)格式。Fabric 的 X.509 证书需先转换为 JWK。
解决:引入nimbus-jose-jwt库,用以下代码生成 JWK:
X509Certificate cert = (X509Certificate) enrollment.getCert(); ECKey ecKey = ECKey.parseFromX509Certificate(cert); JWK jwk = ecKey.toPublicJWK(); // 自动转换为 {kty, crv, x, y} 结构4.4 现象:/did/issue接口返回 500,日志显示java.lang.NoClassDefFoundError: org/bouncycastle/crypto/params/ECPrivateKeyParameters
原因:Fabric SDK 依赖 Bouncy Castle 1.60+,但 SpringBoot 2.7.x 默认带的 BC 版本是 1.69,存在类签名冲突。IssuerServiceImpl中调用JwtUtils.sign()时触发了类加载器隔离问题。
解决:在pom.xml中强制排除低版本 BC,并声明高版本:
<dependency> <groupId>org.hyperledger.fabric-sdk-java</groupId> <artifactId>fabric-sdk-java</artifactId> <exclusions> <exclusion> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency>5. 链上 DidDoc 的动态服务发现:如何让前端自动找到 Issuer 的 VC 颁发接口
5.1 DidDoc 的service字段不是摆设:它承载着真实的服务路由能力
W3C DID 规范中,service数组定义了 DID 主体对外提供的服务端点。在这个项目里,Issuer的 DidDoc 会包含类似这样的service:
{ "id": "did:fab:Org1MSP:issuer1", "service": [ { "id": "vc-issuance", "type": "VerifiableCredentialService", "serviceEndpoint": "https://issuer-api.example.com/vc/issue" }, { "id": "did-resolver", "type": "DIDResolutionService", "serviceEndpoint": "https://resolver.example.com/1.0/identifiers/" } ] }IssuerServiceImpl.registerAsIssuer()在注册 Issuer 时,会将serviceEndpoint写入链上 DidDoc。这意味着:前端无需硬编码 Issuer API 地址,只需知道 Issuer 的 DID,就能通过/did/doc/{did}获取其服务端点,实现真正的去中心化服务发现。
5.2 前端动态调用 Issuer API 的完整代码示例
// 前端 JS:根据 Issuer DID 动态获取 VC 颁发地址 async function getIssuerEndpoint(issuerDid) { try { const response = await fetch(`/did/doc/${issuerDid}`); const didDoc = await response.json(); // 查找 type 为 VerifiableCredentialService 的 service const vcService = didDoc.service?.find(s => s.type === 'VerifiableCredentialService' ); if (!vcService || !vcService.serviceEndpoint) { throw new Error(`No VC service found for ${issuerDid}`); } return vcService.serviceEndpoint; // 返回 https://issuer-api.example.com/vc/issue } catch (err) { console.error('Failed to resolve issuer endpoint:', err); throw err; } } // 调用示例 async function requestVC() { const issuerEndpoint = await getIssuerEndpoint('did:fab:Org1MSP:issuer1'); // 构造 VC 请求 payload(含用户 DID 和待证明属性) const payload = { "subjectDid": "did:fab:Org1MSP:user123", "credentialType": "UniversityDegreeCredential", "attributes": { "degree": "Bachelor", "major": "Computer Science" } }; const response = await fetch(issuerEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return response.json(); }这段代码的价值在于:它把 Issuer 的服务地址从「配置项」变成了「链上可验证事实」。如果 Issuer 迁移服务器,只需更新链上 DidDoc 的serviceEndpoint字段,所有前端自动生效——无需发版、无需运维介入。
5.3 验证服务端点真实性的三重校验机制
仅仅读取serviceEndpoint不够安全,必须验证该端点确实属于该 DID 主体:
| 校验层级 | 方法 | 代码位置 | 失败后果 |
|---|---|---|---|
| TLS 证书绑定 | 检查 HTTPS 端点的 TLS 证书 Subject Alternative Name 是否包含 Issuer DID | 前端fetch()自动完成 | 浏览器报NET::ERR_CERT_COMMON_NAME_INVALID |
| DID 文档签名 | Issuer API 响应头中返回Link: <https://resolver.example.com/...>; rel="describedby",指向其链上 DidDoc | IssuerController添加响应头 | 前端拒绝解析未声明的端点 |
| 链上服务注册 | RegisterCenterServiceImpl在注册 Issuer 时,将serviceEndpoint的 SHA256 哈希值写入 Fabric world state,供后续比对 | registerIssuer链码函数 | 后端拦截非法端点请求 |
从那以后我每次部署新 Issuer 节点,都强制走一遍
curl -X POST http://localhost:7054/api/v1/enroll -d '{"id":"issuer1","secret":"issuer1pw"}'获取 enrollment,再手动调用putDidDoc更新链上 DidDoc 的service字段——哪怕多花 2 分钟,也比上线后被中间人劫持强。希望帮到你。
本文还有配套的精品资源,点击获取