基于区块链技术的学历信息征信系统源码讲解

近在学习孔壹学院的基于区块链技术的学历信息征信系统项目,说实话代码其实看懂并不是很难,毕竟我本来就是搞前端的,对后端也有一定的理解。难其实是难在环境部署。(对于我来说)

关于部署本文章就不多赘述了,这篇文章主要是对于源码的一个讲解。为了方便大家的访问,我已经将源码fork至gitee,有需要的可以自行clone。

项目结构图

这是一个基于浏览器的区块链项目,采取的是MVC架构模式,M——Model(模型),V——View(视图),C——(Controller)。这里简单介绍一下MVC架构模式,View视图层就是用户所看到的界面,也就是前端工程师所干的事情。Model是数据层,也就是程序需要操作的数据或信息。在这两个中间的是Controller控制层,它负责根据用户从"视图层"输入的指令,选取"数据层"中的数据,然后对其进行相应的操作,产生最终结果;也就是进行业务逻辑的处理。

structure

Snipaste_2020-08-25_15-49-59

项目结构描述

chaincode

顾名思义,放置链码的文件夹。什么是链码?其实链码本质上就是区块链中新手一直搞不懂的智能合约。只不过在Hyperledger Fabric中它被称为chaincode(链码)。 Hyperledger Fabric中链码一般分为系统链码和用户链码。系统链码是Fabric提供给我们用于Fabric 节点自身的处理逻辑, 包括系统配置、背书、校验等工作的。系统链码共有五种类型:配置系统链码(CSCC)生命周期系统链码(LSCC)查询系统链码(QSCC)背书管理系统链码(ESCC)。具体作用可查询这里。用户链码则是我们开发者根据业务的需求编写出来的,其本质就是一个个的函数,它们运行在链码容器中,通过 Fabric 提供的接口与账本状态进行交互。ChainCode要在区块链网络中运行,需要经过链码安装链码实例化两个步骤。

  • 其中eduStruct.go文件中定义了一个名为Education和一个HistoryItem的结构体。其中struct Education就是一个人的学历信息,包括姓名、性别、省份证号、专业等等。

  • eduCC.go文件主要调用了fabric提供的一些sdk,然后定义了一些函数,这些函数的功能有:根据身份证号查询信息、添加信息、修改信息等。

  • main.go是对于这一个整个chaincode文件夹的总结,提供了main函数。熟悉go语言的同学应该都知道,每一个go源文件都是以一个package somepackage开头的。每一个package包含着多个go源文件,而这些源文件中,只能有一个main函数,这个函数是这个package生成可执行程序的入口点(基本类似于C语言中的main函数)。那让我们来看看这个main函数到底做了什么:

    func main(){
        err := shim.Start(new(EducationChaincode))
        if err != nil{
            fmt.Printf("启动EducationChaincode时发生错误: %s", err)
        }
    }
    

    其实很简单,调用了fabric-sdk的shim函数接口的Start方法,具体定义可在这里查询。这个函数接收一个ChainCode对象并返回一个变量;当链码的启动失败时,它会返回一个错误对象否则返回nil

    那么定义于eduCC.go中的那些业务逻辑的函数何时会被调用呢?让我们跳转到chaincode/main.go中的Invoke函数。可以看到,这个函数接收的参数是一个shim.ChaincodeStubInterface类型的变量stub,这里要注意一下:

    func (t *EducationChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response{
        return shim.Success(nil)
    }
    

    对Go语言了解的不够深的小伙伴们可能看不太懂这个函数到底哪个是参数了,其实我一开始也没看懂。经过Google,懂了。func关键字后紧跟的小括号里指明了这个Init函数时EducationChaincode这个类的方法,然后调用这个方法的对象以指针形式以参数t传入函数内,本质就是一个语法糖,这个t是一个自定义的参数,而不是传入的参数。可以从下面的Invoke函数中看到,会调用一系列的t.somefunc。这里的somefunc就是我们在eduCC.go里定义的那一系列函数(chaincode)啦!

    讲到这里,还要再介绍一个知识(怕有的小伙伴不知道):开发人员编写的链码中,必须要实现InitInvoke两个方法。其中:

    • **Init:**在链码实例化或升级时被调用, 完成初始化数据的工作。
    • invoke:更新或查询提案事务中的分类帐本数据状态时,Invoke 方法被调用, 因此响应调用或查询的业务实现逻辑都需要在此方法中编写实现。

    这样大家应该就能理解的更透彻了。

fixtures

这个文件夹是生成区块链底层网络环境所需的,其本质就是启动多个docker容器。

  • pull_images.sh是一个bash脚本,用来pull所需docker镜像。
  • docker-compose.yml是一个配置文件,通过这个文件,docker-compose up命令可以开启配置文件当中的docker容器,将up改为down则是关闭。
  • 另外两个文件夹则是有关区块链加密的,具体可以自行查询资料。

img

这个文件夹与本项目无关,是教程中用于辅助的图。项目中的图在web文件夹中的static目录下。

sdkInit

这个文件夹是用于将链码的安装以及实例化操作进行自动化,避免了每次启动项目都要在命令行中敲繁琐的代码。

  • fabricInitInfo.go可以看到,这个文件很简单,就简单的定义了一个InitInfo的结构体。该结构体定义了智能合约(chaincode)所创建的通道(channel)名称、组织名称、chaincodeID、username等。
  • start.go这个文件可以看到它定义了三个函数,分别为:SetupSDKCreateChannelInstallAndInstantiateCC。它们的作用分别是初始化SDK、创建通道并将指定的Peers加入、安装和实例化智能合约(chaincode)并创建客户端实例。

但是细心的你可能会发现了,这个package里并没有main函数,也就是说这些函数只是定义了,并没有执行,那么它们是在哪里执行的呢?聪明的你可能早就发现了,在项目的根目录中有一个名为main.go的文件,它——就是我们的答案了。

可以看到,在文件开头的import中有一个:"github.com/kongyixueyuan.com/education/sdkInit",这就将这一整个文件夹都给引入进来了,它们在main.go中就会以sdkInit这个前缀名存在着。可以看到main函数的第一行:initInfo := &sdkInit.InitInfo{……}就是初始化了一个InitInfo的结构体,该结构体的定义可以在sdkInit/fabricInitInfo.go中查看。随后你可以看见定义在sdkInit/start.go中的三个函数都依次被执行了。关于sdkInit这个文件夹的作用就讲到这里。

###service

这个文件夹是业务层,在业务层中,我们使用 Fabric-SDK-Go 提供的接口对象调用相应的 API 以实现对链码的访问,最终实现对分类账本中的状态进行操作。其实到了这里就有点类似于我们日常页面的开发了,前后端分离,这里就类似于后端的接口了。只不过这里的接口是通过调用sdk去通过访问链码来间接对数据进行修改。这样的好处就是通过了区块链进行数据的REST,保证了数据的真实、可靠。

  • domain.go该文件定义了三个结构体,两个函数。函数的作用分别是注册链码以及接受链码结果。
  • eduService.go该文件封装了了一系列对于数据的操作的函数。调用时机的话请继续往下看。

web

这个目录是用来构建前端页面以及路由的。首先来看根目录中的唯一一个文件:webServer.go。该文件就定义了一个方法:WebStart

Snipaste_2020-08-25_22-02-22

看到这里我相信一些有过全栈开发经验的小伙伴们已经懂了。这里我简单举两个例子来说一下吧。

  • http.HandleFunc("/", app.LoginView)
    

    这段代码表明,当我们访问web服务的根目录(即HandleFunc函数的第一个参数"/")时,调用app.LoginView这个方法;该方法的定义可于web/controller/controllerHandler.go中查找到(先不具体讲,到后面会具体讲)。

  • http.HandleFunc("/addEdu", app.AddEdu)
    

    这段代码表明,当我们访问/addEdu目录时(注意:这里是相对路由,完整的应该为:localhost:9000/addEdu),调用app.AddEdu这个方法。该方法同样可于web/controller/controllerHandler.go中查到。

总结:webServer.go文件用来定义页面路由规则。

  • controller该目录下有四个go文件:

    • controllerHandler.go该文件定义了webServer.go中访问对于路由所调用的方法(函数)。
    • controllerResponse.go 该文件主要实现对用户请求的响应,将响应结果返回给客户端浏览器。
    • userinfo.go该文件定义了用户名和密码。
    • upload.go该文件定义了上传文件的方法。
  • static

    • css——一些样式文件
    • fonts——页面所需字体
    • images——页面所需图片
    • js——引入了bootstrap和jquery两个js库,简化操作。
  • tpl

    html文件以及网站图标。

main.go

最终,我们可以看到在文件夹根目录中有一个main.go文件,我们可以看一下它开头的import:

import (
	"os"
	"fmt"
	"encoding/json"
	"github.com/kongyixueyuan.com/education/sdkInit"
	"github.com/kongyixueyuan.com/education/service"
	"github.com/kongyixueyuan.com/education/web/controller"
	"github.com/kongyixueyuan.com/education/web"
)

可以看到,该文件仅引用了一些基本的自带库,其余的都是我们已经解析过的代码。它对他们进行了一个结合,使整个项目的逻辑更加清晰,各部分的耦合性降低。有利于开发者后续的维护。

数据的传输过程

看完了上面的项目结构描述,相信你大体上已经明白了这个项目是如何搭建的了。不过你应该对于数据的传输还是不太了解,因为这个项目使用的是CouchDB作为StateDB进行数据的存储的,但是你会发现上面的代码没有一处涉及到了CouchDB。所以这里我再将数据是如何从一个go语言结构体传输到CouchDB进行保存的这个过程来讲述一下。

First

首先,让我们来回顾一下这个项目。我们会发现一共有三个地方对学历信息的增删改查接口进行了封装。它们分别位于:

  • chaincode/eduCC.go
  • service/eduService.go
  • web/controller/controllerHandler.go

这里要教大家一个小技巧,如何分别两个go package是否有依赖关系?看文件开头的import。如果一个文件引入了另一个文件,那么说明它们之间有依赖关系,则可以忽略引入外部文件的文件。也就是说我们直接看最原始的封装。如果我们仔细看了web/controller/controllerHandler.go的代码,我们可以发现:它引入了service这个package。接下来我们观察controllerHandler.go中封装的接口,例如:func AddEdu。我们可以看到,它先获取html表单中的数据,然后将数据传入到app.Setup.SaveEdu()这一个函数中。让我们来看看这个函数的定义在哪里。首先由函数定义可知,app是一个*Application类型变量,也就是Application这个Struct(结构体)的指针。那么Application是在哪里定义的呢?让我们打开web/controller/userInfo.go,可以看到:

type Application struct {
	Setup *service.ServiceSetup
}

这个结构体中只有一个变量:Setup。而该变量又是service.Service.Setup结构体的指针。所以我们再次跳转到service/domain.go。可以看到:

type ServiceSetup struct {
	ChaincodeID	string
	Client	*channel.Client
}

到了这里,我们就看到了app *Application的本质了:一个定义于service/domain.go中的ServiceSetup结构体的一个指针变量。

于是app.Setup.SaveEdu()这个函数的定义也就很容易找到了,它其实就是ServiceSetup结构体的一个类方法。定义可在service/eduService.go中找到。由此我们可以知道:定义于web/controller/controllerHandler.go中的对于数据的增删改查接口本质上是service中的。所以我们不再关注web/controller

Second

接下来,我们打开service/eduService.go,还是分析SaveEdu这个方法。可以看到,该函数首先调用了registerEvent方法进行链码事件的注册;然后到了关键的地方:

req := channel.Request{ChaincodeID: t.ChaincodeID, Fcn: "addEdu", Args: [][]byte{b, []byte(eventID)}}
respone, err := t.Client.Execute(req)

首先调用了channel.Request方法,它指明了要调用的链码,以及链码内要Invoke的函数args,args是序列化的结果,序列化是自定义的,只要链码能够按相同的规则进行反序列化即可。我们可以看到,在上面有这样的一行代码:

b, err := json.Marshal(edu)

这就是将传入到SaveEdu中的参数edu进行序列化,再传入到链码的方法addEdu中。

随后我们通过Client.Execute(req)使用通道客户端的Execute接口调用链码。

Finally

在上一章节,我们可以的得知:service/eduService.go中定义的接口其本质并没有变,还是调用了别的地方的函数。可是为什么这个文件的import中并没有引入chaincode这个package呢?因为它们共处于一个区块链网络环境中,节点之间可以相互通信。当通过channel.RequestClient.Execute后,对应的chaincode就会被执行,peer节点就会进行背书。所以并不需要要引入对应的包,但是务必要保证函数名相同。我们还是从SaveEdu这个方法接着讲下去。

上一章中,我们知道它是调用了链码中的addEdu方法,所以我们跳转到chaincode/eduCC.go,这里我们可以看到addEdu方法,也就是定义于web/controller中的AddEdu方法的庐山真面目了。让我们简单看一下这个函数吧:

func (t *EducationChaincode) addEdu(stub shim.ChaincodeStubInterface, args []string) peer.Response {

	var edu Education
	err := json.Unmarshal([]byte(args[0]), &edu)

	// 查重: 身份证号码必须唯一
	_, exist := GetEduInfo(stub, edu.EntityID)
	if exist {
		return shim.Error("要添加的身份证号码已存在")
	}

	_, bl := PutEdu(stub, edu)

	err = stub.SetEvent(args[1], []byte{})

	return shim.Success([]byte("信息添加成功"))
}

这里我将代码进行了一些简化,但是麻雀虽小,五脏俱全。该函数首先对传入进来的字节数组进行反序列化,然后对于所添加信息的校验唯一性;如果唯一,则执行PutEdu()这个方法。这个方法,就是数据写入到CouchDB的步骤了。该函数的定义在addEdu的上方。可以看到,该函数先将传进来的json数据进行序列化;序列化完成后就开始了写入数据,即stub.PutState()方法。该方法是Fabric提供的go-sdk中封装好的一个接口。这个函数本质就是在StateDB中添加或修改一条数据。因为这里我们的StateDB指定为了CouchDB,所以就会写入至我们启动好的CouchDB中。有关这些接口的使用方法,可以参考这篇文章

到了这里,一个添加信息的方法就被我们理解了。数据是如何从页面中的html表单,到后端的接口,再到业务层中的调用链码,再到链码中的对于CouchDB的操作,我们已经心中有数了。其他的方法本质都是一样的,读者可以自行查看源码。

区块链中的数据不可篡改

上一次的周报中,老师对该项目发出了一个疑问:为什么学历信息可以修改,区块链中的数据不是不可以修改的吗?首先,区块链网络本身是有加密保护的,而且该加密算法为不可逆算法,这一步就阻隔了大部分的恶意攻击。因为对于数据的增删改查都是在区块链网络中进行的;当攻击者无法进入该网络,自然无法通过sql注入、xss共计等方法来进行数据的获取与篡改。其次,就算攻击者有天大的本事,破解了加密算法,进入到了区块链网络中,它能修改的也只是CouchDB即StateDB中的数据。这里再介绍一下Hyperledger Fabric中的一个核心概念:Ledger(账本)。

In Hyperledger Fabric, a ledger consists of two distinct, though related, parts – a world state and a blockchain. Each of these represents a set of facts about a set of business objects.

官网说:一个账本由两个彼此不同但却相互关联的部分组成:

  • world state(世界状态)
  • blockchain(区块链)

世界状态是什么呢?其实就是我们说的StateDB,也就是一个数据库,它存储了一组账本状态的当前值。世界状态可以经常改变,因为我们可以创建、更新、删除状态。

其次是区块链。它是一个记录了促成当前世界状态的所有改变的交易日志。交易被收集在附加到区块链的区块中,能帮助我们理解所有促成当前世界状态的改变的历史。区块链数据结构与世界状态相差甚远,因为一旦把数据写入区块链,就无法修改,它是不可篡改的

一个账本是由区块链和世界状态组成,其中世界状态是由区块链决定的,我们也可以说世界状态是源自区块链的。

区块链中记录的是交易的日志,促成当前时间状态的改变的历史。其实就相当于CouchDB中的_rev字段一样(有关CouchDB,可以学习一下我的这篇文章),它会记录下每一次变动并且保证该记录不可变。这样,就算黑客修改了我们世界状态(即数据库)中的数据,我们也可以知道他做了什么。因为当他修改数据时,区块链中就会生成不可变的记录。

如果想更深入的了解,可以前往官网学习。

结语

以上就是对于这个项目的源码分析了,如果还有什么疑问可以评论区留言或在About页面找到我的联系方式与我取得联系。如果这篇文章对你有所帮助,欢迎Star

参考

从0到1:Hyperledger Fabric开发精要

Hyperledger Fabric docs