基于fabric的校园征信系统——beego框架后端代码编写及fabric-sdk-go的调用
beego框架
beego 是一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性(interface、struct 嵌入等)而设计的一个框架。
路由配置
我们首先要做的就是设置我们的路由,然后根据路由依次实现对应的处理函数。
package routers
import (
"school-credit/controllers"
"github.com/astaxie/beego"
)
func init() {
beego.Router("/addStuInfo", &controllers.MainController{}, "post: AddStuInfo")
beego.Router("/getStuInfo", &controllers.MainController{}, "get: GetStuInfo")
beego.Router("/addSchInfo", &controllers.MainController{}, "post: AddSchInfo")
beego.Router("/getSchInfo", &controllers.MainController{}, "get: GetSchInfo")
beego.Router("/addCreInfo", &controllers.MainController{}, "post: AddCreInfo")
beego.Router("/getCreInfo", &controllers.MainController{}, "get: GetCreInfo")
}
Controller层业务逻辑
接下来在controller层应该以此实现如下函数:
func (c *MainController) AddStuInfo() {
}
func (c *MainController) GetStuInfo() {
}
func (c *MainController) AddSchInfo() {
}
func (c *MainController) GetSchInfo() {
}
func (c *MainController) AddCreInfo() {
}
func (c *MainController) GetCreInfo() {
}
我们以添加和查询学生基本信息为例,先获取前端传来的信息,再调用model层的SDK实现向区块中写入或读取数据,再将操作信息传给前端页面:
func (this *MainController) AddStuInfo() {
//获取前端传入的数据
StuName := this.GetString("StuName")
StuSex := this.GetString("StuSex")
NativePlace := this.GetString("NativePlace")
Birthday := this.GetString("Birthday")
Nation := this.GetString("Nation")
IDNum := this.GetString("IDNum")
//判断ID是否为空
if IDNum == "" {
return
}
AdmissionDate := this.GetString("AdmissionDate")
GraduationTime := this.GetString("GraduationTime")
SchName := this.GetString("SchName")
SubName := this.GetString("SubName")
//将参数放入字符串数组
var args []string
args = append(args, "addStuInfo")
args = append(args, StuName)
args = append(args, StuSex)
args = append(args, NativePlace)
args = append(args, Birthday)
args = append(args, Nation)
args = append(args, IDNum)
args = append(args, AdmissionDate)
args = append(args, GraduationTime)
args = append(args, SchName)
args = append(args, SubName)
//TODO 调用model层函数实现数据上链
}
func (c *MainController) GetStuInfo() {
var args []string
IDNum := this.GetString("IDNum")
args = append(args, "getStuInfo")
args = append(args, IDNum)
//TODO 调用model层函数实现链上数据查询
}
接下来我们需要编写model层的功能代码,以支持上层开发
Fabric-SDK-GO
fabric go sdk是Hyperledger Fabric官方提供的Go语言开发包,应用程序可以利用fabric go sdk与fabric网络进行交互并访问链码。
编写SDK配置文件
首先我们需要将fabric生成的证书文件(crypto-config)以及通道文件(channel-artifacts)复制到项目目录中,后续需要使用到这些文件。
我们可以参考$GOPATH/src/github.com/hyperledger/fabric-sdk-go/test/fixtures/config/config_test.yaml下的模板文件进行SDK配置文件的编写
client使用sdk与fabric网络交互,需要告诉sdk两类信息:
- 我是谁:即当前client的信息,包含所属组织、密钥和证书文件的路径等, 这是每个client专用的信息。
- 对方是谁:即fabric网络结构的信息,channel、org、orderer和peer等 的怎么组合起当前fabric网络的,这些结构信息应当与configytx.yaml中是一致的。这是通用配置,每个客户端都可以拿来使用。另外,这部分信息并不需要是完整fabric网络信息,如果当前client只和部分节点交互,那配置文件中只需要包含所使用到的网络信息。
参考文章 fabric sdk go新手入门
相关配置如下:
# 配置文件的名字,可以不写
name: "school-credit-network"
version: 1.0.0
# client 相关配置
client:
# 此 SDK 实例属于哪个组织
organization: OrgSchool
# 日志级别
logging:
level: info
# 证书所在目录
cryptoconfig:
path: /home/surface/workspace/src/school-credit/conf/crypto-config
# 这种方式就是把用户名和密码直接存储在本地的一个文件中,而用户和密码对通过一个别名来引用,这样可以避免密码明文格式可能会存在的安全问题
credentialStore:
path: /tmp/schoolcredit-service-store
# 区块链密码服务提供者,指定加密策略
BCCSP:
security:
enabled: true
default:
provider: "SW"
hashAlgorithm: "SHA2"
softVerify: true
level: 256
tlsCerts:
# 证书池策略,默认为false,提高身份认证速率
systemCertPool: true
client:
keyfile: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgschool.rjxy.cn/users/User1@orgschool.rjxy.cn/tls/client.key
certfile: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgschool.rjxy.cn/users/User1@orgschool.rjxy.cn/tls/client.crt
# channel 相关配置
channels:
# channelID
schoolchannel:
# orderer 组织必须指定
orderers:
- orderer.rjxy.cn
# 添加到该 channel 中的组织的 peer 列表
peers:
peer0.orgschool.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.orgschool.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
policies:
queryChannelConfig:
minResponses: 1
maxTargets: 1
retryOpts:
attempts: 5
initialBackoff: 500ms
maxBackoff: 5s
backoffFactor: 2.0
# channelID
unionchannel:
# orderer 组织必须指定
orderers:
- orderer.rjxy.cn
# 添加到该 channel 中的组织的 peer 列表
peers:
peer0.orgschool.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.orgschool.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer0.orgenterprise.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.orgenterprise.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer0.orgcredit.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.orgcredit.rjxy.cn:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
policies:
queryChannelConfig:
minResponses: 1
maxTargets: 1
retryOpts:
attempts: 5
initialBackoff: 500ms
maxBackoff: 5s
backoffFactor: 2.0
# organizations 相关配置
organizations:
OrgSchool:
# configtx.yaml organizations -> ID
mspid: OrgSchoolMSP
cryptoPath: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgschool.rjxy.cn/users/{userName}@orgschool.rjxy.cn/msp
peers:
- peer0.orgschool.rjxy.cn
- peer1.orgschool.rjxy.cn
OrgEnterprise:
# configtx.yaml organizations -> ID
mspid: OrgEnterpriseMSP
cryptoPath: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgenterprise.rjxy.cn/users/{userName}@orgenterprise.rjxy.cn/msp
peers:
- peer0.orgenterprise.rjxy.cn
- peer1.orgenterprise.rjxy.cn
OrgCredit:
# configtx.yaml organizations -> ID
mspid: OrgCreditMSP
cryptoPath: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgcredit.rjxy.cn/users/{userName}@orgcredit.rjxy.cn/msp
peers:
- peer0.orgcredit.rjxy.cn
- peer1.orgcredit.rjxy.cn
Orderer:
mspID: OrdererMSP
cryptoPath: /home/surface/workspace/src/school-credit/conf/crypto-config/ordererOrganizations/rjxy.cn/users/Admin@rjxy.cn/msp
# orderer 相关配置
orderers:
orderer.rjxy.cn:
url: localhost:7050
grpcOptions:
ssl-target-name-override: orderer.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/ordererOrganizations/rjxy.cn/tlsca/tlsca.rjxy.cn-cert.pem
# peer 相关配置
peers:
peer0.orgschool.rjxy.cn:
url: localhost:7051
eventUrl: localhost:7053
grpcOptions:
ssl-target-name-override: peer0.orgschool.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgschool.rjxy.cn/tlsca/tlsca.orgschool.rjxy.cn-cert.pem
peer1.orgschool.rjxy.cn:
url: localhost:8051
eventUrl: localhost:8053
grpcOptions:
ssl-target-name-override: peer1.orgschool.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgschool.rjxy.cn/tlsca/tlsca.orgschool.rjxy.cn-cert.pem
peer0.orgenterprise.rjxy.cn:
url: localhost:9051
eventUrl: localhost:9053
grpcOptions:
ssl-target-name-override: peer0.orgenterprise.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgenterprise.rjxy.cn/tlsca/tlsca.orgenterprise.rjxy.cn-cert.pem
peer1.orgenterprise.rjxy.cn:
url: localhost:10051
eventUrl: localhost:10053
grpcOptions:
ssl-target-name-override: peer1.orgenterprise.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgenterprise.rjxy.cn/tlsca/tlsca.orgenterprise.rjxy.cn-cert.pem
peer0.orgcredit.rjxy.cn:
url: localhost:11051
eventUrl: localhost:11053
grpcOptions:
ssl-target-name-override: peer0.orgcredit.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgcredit.rjxy.cn/tlsca/tlsca.orgcredit.rjxy.cn-cert.pem
peer1.orgcredit.rjxy.cn:
url: localhost:12051
eventUrl: localhost:12053
grpcOptions:
ssl-target-name-override: peer1.orgcredit.rjxy.cn
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: false
tlsCACerts:
path: /home/surface/workspace/src/school-credit/conf/crypto-config/peerOrganizations/orgcredit.rjxy.cn/tlsca/tlsca.orgcredit.rjxy.cn-cert.pem
model层代码编写
我们在models层操作需要用到 Fabric_SDK ,所以我们首先要做的就是先拿到 SDK 句柄,创建channel,添加节点,安装链代码,实例化链代码之手,才能进行数据的操作。首先我们定义一个结构体,该结构体里面存放一些与创建Fabric-SDK 有关的配置信息,格式如下:
package models
import (
"github.com/astaxie/beego"
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
"github.com/hyperledger/fabric-sdk-go/pkg/client/event"
"github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"
"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
)
//定义存放与Fabric-SDK有关配置值信息的结构体
type FabricSetup struct {
ConfigFile string // 初始化 SDK 对应的配置文件
OrgID string //组织节点ID
OrdererID string // orderer节点
ChannelID string // ChannelID
ChainCodeID string // ChainCodeID
ChannelConfig string // 与channel相关的配置文件
ChaincodeGoPath string // GOPATH 环境变量
ChaincodePath string // ChainCode 存放路径
OrgAdmin string // Admin 用户名
OrgName string // 组织名称
UserName string // 普通用户名
eventID string // eventID
initialized bool //是否已经初始化
client *channel.Client //pkg/client/channel支持访问Fabric网络上的通道。channel客户端实例提供与指定通道上的Peer节点进行交互的处理函数。channel客户端可以在指定通道上查询链码,执行链码以及注册或注销链码事件。如果应用程序需要与Fabric网络的多条通道进行交互,需要为每条通道创建一个单独的通道客户端实例。
resMgmtClient *resmgmt.Client //resmgmt支持在Fabric网络上创建和更新资源。resmgmt允许管理员创建、更新通道,并允许Peer节点加入通道。管理员还可以在Peer节点上执行与链码相关的操作,例如安装,实例化和升级链码。
sdk *fabsdk.FabricSDK //fabsdk是Fabric SDK的主要包,fabsdk支持客户端使用Hyperledger Fabric区块链网络。fabsdk基于配置创建上下文环境,上下文环境会在client包使用。
event *event.Client //event包支持访问Fabric网络上的通道事件。事件客户端可以接收区块事件,过滤区块事件,链码事件和交易状态事件。
}
type Application struct {
beego.Controller
FabricSetup *FabricSetup
}
// 学生基本信息
type StuInfo struct {
StuName string `json:"stuname"` // 学生姓名
StuSex string `json:"stusex"` // 学生性别
NativePlace string `json:"nativeplace"` //籍贯
Birthday string `json:"birthday"` // 出生日期
Nation string `json:"nation"` // 民族
IDNum string `json:"idnum"` // 身份证号
AdmissionDate string `json:"admissiondate"` // 入学日期
GraduationTime string `json:"graduationtime"` // 毕业日期
SchName string `json:"schname"` // 学校名称
SubName string `json:"subname"` // 专业名称
}
// 学校记录信息
type SchInfo struct {
StuGPA string `json:"stugpa"` // 学生绩点
ExcellentRecord string `json:"excellentrecord"` // 优良记录
BadRecord string `json:"badrecord"` //不良记录
StuLoan string `json:"stuloan"` // 助学贷款记录
GraduationComment string `json:"graduationcomment"` // 毕业评价
}
// 征信机构信息
type CreInfo struct {
BankOverdraft string `json:"bankoverdraft"` // 信用卡透支记录
AntCreditPayOverDue string `json:"antcreditpayoverdue"` // 花呗逾期记录
DidiTaxiArrears string `json:"diditaxiarrears"` //滴滴欠款记录
SesameCredit string `json:"sesamecredit"` // 芝麻信用积分
}
// 学生征信档案
type StuCreInfo struct {
StuInfo StuInfo `json:"stuinfo"` // 学生基本信息
SchInfo SchInfo `json:"schinfo"` // 学校记录信息
CreInfo CreInfo `json:"creinfo"` //征信机构信息
}
下一步我们新建setup.go文件,在里面利用fabric-sdk-go设计FabricSetup结构体的方法,包括SDK的初始化,链码的安装和实例化以及资源的释放。代码如下:
package models
import (
"fmt"
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
"github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"
"github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry"
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
"github.com/hyperledger/fabric-sdk-go/pkg/fab/ccpackager/gopackager"
"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/common/cauthdsl"
"github.com/pkg/errors"
)
//初始化配置文件以及客户端方法
func (setup *FabricSetup) Initialize() error {
//判断是否已经初始化
if setup.initialized == true {
return errors.New("sdk already initialized")
}
//通过配置文件初始化SDK
sdk, err := fabsdk.New(config.FromFile(setup.ConfigFile))
if err != nil {
return errors.WithMessage(err, "failed to create sdk")
}
setup.sdk = sdk
fmt.Println("SDK created")
//通过 SDK 实例,基于用户和组织创建上下文[为 创建 resMgmtCli 做准备]
resourceManagerClientcontext := setup.sdk.Context(fabsdk.WithUser(setup.OrgAdmin), fabsdk.WithOrg(setup.OrgName))
// 创建资源管理客户端 - resMgmtCli
resMgmtClient, err := resmgmt.New(resourceManagerClientcontext)
if err != nil {
return errors.WithMessage(err, "failed to create channel management client from Admin identity")
}
setup.resMgmtClient = resMgmtClient
fmt.Println("ResourceManager client created")
//利用资源管理客户端,创建 channel
req := resmgmt.SaveChannelRequest{
ChannelID: setup.ChannelID,
ChannelConfigPath: setup.ChannelConfig,
}
txID, err := setup.resMgmtClient.SaveChannel(req)
if err != nil || txID.TransactionID == ""{
return errors.WithMessage(err, "failed to save channel")
}
fmt.Println("Channel created")
// 把组织添加到 channel 中的时候,一般制定一些重试的策略,和指定 orderer节点的网络位置
err = setup.resMgmtClient.JoinChannel(setup.ChannelID, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint(setup.OrdererID))
if err != nil {
return errors.WithMessage(err,"Failed to join channel!")
}
fmt.Println("Joined channel!")
fmt.Println("Initialize Successful!")
return nil
}
//安装链代码以及实例化方法
func (setup *FabricSetup) InstallAndInstantiate() error {
//准备安装链代码的参数
ccPkg, err := gopackager.NewCCPackage(setup.ChaincodePath, setup.ChaincodeGoPath)
if err != nil {
return errors.WithMessage(err, "Chaincode packager error")
}
installRequest := resmgmt.InstallCCRequest{
Name: setup.ChainCodeID,
Path: "chaincode",
Version: "1.0",
Package: ccPkg,
}
//安装链代码
_, err = setup.resMgmtClient.InstallCC(installRequest)
if err != nil {
fmt.Println("chaincode install error")
}
fmt.Println("chanincode install success")
//实例化链代码,实例化之前需要制定一个背书策略
ccpolity := cauthdsl.SignedByAnyMember([]string{"OrgSchoolMSP"})
txID, err := setup.resMgmtClient.InstantiateCC(
setup.ChannelID,
resmgmt.InstantiateCCRequest{
Name: setup.ChainCodeID,
Path: setup.ChaincodeGoPath,
Version: "1.0",
Args: nil,
Policy: ccpolity,
},
)
if err != nil || txID.TransactionID == "" {
fmt.Println("Instantiate chaincode error!")
return err
}
fmt.Println("chaincode instantiate success!")
//实例化完成之后就可以创建一个客户端client来操作账本数据了
clientContext := setup.sdk.ChannelContext(setup.ChannelID, fabsdk.WithUser(setup.UserName))
setup.client, err = channel.New(clientContext)
if err != nil {
return errors.WithMessage(err, "failed to create new channel client")
}
fmt.Println("Channel client created")
fmt.Println("Chaincode Installation & Instantiation Successful")
return nil
}
//资源释放方法
func (setup *FabricSetup) CloseSDK() {
setup.sdk.Close()
}
再接下来我们创建fabricinit.go文件,完成关于FabricSetup结构体的初始化工作,代码如下:
package models
import (
"fmt"
"os"
)
var App Application
//init一旦加载,所有与创建sdk相关的一些配置就被初始化
func init(){
fSetup := FabricSetup{
ConfigFile: "conf/schoolconfig.yaml",
OrgAdmin: "Admin",
UserName: "User1",
OrgName: "OrgSchool",
ChainCodeID: "unionchaincode",
ChaincodePath: "school-credit/chaincode/union",
ChaincodeGoPath: os.Getenv("GOPATH"),
ChannelID: "unionchannel",
ChannelConfig: "/home/surface/workspace/src/school-credit/conf/channel-artifacts/unionchannel.tx",
OrdererID: "OrgSchoolMSP",
}
//调用方法初始化sdk,创建通道,加入通道
err := fSetup.Initialize()
if err != nil {
fmt.Printf("Unable Initizlize SDK,%v!\n",err)
return
}
//调用方法安装链码以及实例化链码
err = fSetup.InstallAndInstantiate()
if err != nil {
fmt.Printf("Unable InstallAndInstantiate,%v!\n",err)
}
defer fSetup.CloseSDK()
App = Application{
FabricSetup : &fSetup,
}
}
现在我们已经可以在model层利用SDK在其所对应的channel中调用链码的方法了,我们以添加学生信息为例,创建addStuInfo.go文件,其中代码如下:
package models
import "github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
func (this *Application) AddStuInfo(args []string) (string, error) {
//定义二维byte数组转换字符数组类型作为channel.Request的fcn参数
var tempArgs [][]byte
for i :=1; i < len(args); i++ {
tempArgs = append(tempArgs, []byte(args[i]))
}
request := channel.Request{ChaincodeID: this.FabricSetup.ChainCodeID, Fcn: args[0], Args: tempArgs}
response, err := this.FabricSetup.client.Execute(request)
if err != nil {
//添加失败
return"", err
}
//添加成功
return string(response.TransactionID), nil
}
完成model层方法的编写,我们则可以在controller层调用它,完成数据的添加和查询操作。此时我们完善controller层的代码,如下:
func (this *MainController) AddStuInfo() {
//获取前端传入的数据
StuName := this.GetString("StuName")
StuSex := this.GetString("StuSex")
NativePlace := this.GetString("NativePlace")
Birthday := this.GetString("Birthday")
Nation := this.GetString("Nation")
IDNum := this.GetString("IDNum")
//判断ID是否为空
if IDNum == "" {
return
}
AdmissionDate := this.GetString("AdmissionDate")
GraduationTime := this.GetString("GraduationTime")
SchName := this.GetString("SchName")
SubName := this.GetString("SubName")
//将参数放入字符串数组
var args []string
args = append(args, "addStuInfo")
args = append(args, StuName)
args = append(args, StuSex)
args = append(args, NativePlace)
args = append(args, Birthday)
args = append(args, Nation)
args = append(args, IDNum)
args = append(args, AdmissionDate)
args = append(args, GraduationTime)
args = append(args, SchName)
args = append(args, SubName)
//TODO 调用model层函数实现数据上链
ret, err := models.App.AddStuInfo(args)
if err != nil {
fmt.Println("AddStuInfo error...")
}
fmt.Println("<--- 添加学生个人信息结果 --->", ret)
}
至此我们的后端代码已经编写完成,可以将其部署在服务器上通过web端访问,查看是否跑通,后续我们将会利用vue进行简单的前端页面设计。