>[info] 本文作者:http://www.kancloud.cn/@runningday
> 文档地址:http://www.kancloud.cn/runningday/fabric
**默认的 POST /registrar接口**
官方REST接口当中,有一个`POST /registrar`接口,对应的实现为
~~~
// fabric/core/rest/rest_api.go
func Register()
~~~
实际上,这个接口的作用是login,而不是注册新用户,有必要自己写一个接口,提供注册新用户的功能。
**REST接口路由**
所有的REST接口,都定义在`fabric/core/rest/rest_api.go`文件中,通过如下方式添加路由:
~~~
// fabric/core/rest/rest_api.go
router.Post("/registrar", (*ServerOpenchainREST).Register)
~~~
因此,可以仿照此,添加一个新的接口:
~~~
// fabric/core/rest/rest_api.go
router.Post("/users", (*ServerOpenchainREST).CreateUser)
~~~
即`POST /users`接口提供注册功能
**RegisterUser核心接口**
由于membersrvc提供了CA相关的服务,查阅官方文档[CA API](http://hyperledger-fabric.readthedocs.io/en/v0.6/API/MemberServicesAPI/),了解到ECAA提供了注册用户的核心接口:
~~~
/fabric/membersrvc/ca/ecaa.go
func (ecaa *ECAA) RegisterUser(ctx context.Context, in *pb.RegisterUserReq) (*pb.Token, error)
~~~
**RegisterUserReq结构体介绍**
首先看请求参数`in *pb.RegisterUserReq`:
~~~
// ca.pb.go
type RegisterUserReq struct {
Id *Identity
Role Role
Attributes []*Attribute
Affiliation string
Registrar *Registrar
Sig *Signature
}
~~~
* Id:即新用户的enrollID,由注册时用户提供
* Role:新用户的类型,分为几个限定类型,按位掩码规则
* Attributes:额外属性
* Affiliation:新用户所属机构,也是几个限定值,必须是`AffiliationGroups`数据库表中已存在的值
* Registrar:本身也是一个结构体:
~~~
// ca.pb.go
type Registrar struct {
Id *Identity
Roles []string
DelegateRoles []string
}
~~~
它定义了为新用户A提供注册代理的注册者B,这个有必要说一下,在fabric中,注册用户不是用户A向系统发出单纯注册请求即可,而是在请求中需要包含注册者B信息,有点类似于网站的强制邀请注册。注册者B必须有注册该类型用户A的权限,否则系统会拒绝这次请求,并提示:
~~~
member <registrar> is not a registrar
~~~
其中Registrar.Roles定义了新用户A,以后可注册哪几个类型新用户;而Registrar.DelegateRoles则存储了以后可以升级为Registrar.Roles的值,换句话说,就是该用户A注册的新用户C,以后可注册哪几个类型新用户。例如:
~~~
userReq := RegisterUserReq{
Id: Identity{Id: "Jim"},
Role: Role(1),
Affiliation: "institution_a",
Registrar: Registrar{
Id: Identity{Id: "admin"},
Roles: ["client", "peer", "validator"],
DelegateRoles: ["client"],
},
Sig: nil,
}
~~~
这个是注册新用户“Jim”的请求,enrollID(即用户名)为“Jim”;用户类型为1,即“client”;机构隶属为“institution_a”;Attributes留空;注册者是"admin",它是membersrvc默认的管理员账户,拥有注册各种类型用户的权限,其定义在membersrvc.yaml文件中,
>[warning] 需要说明一点的就是:admin虽然在配置文件中已定义了很高的权限,但是在注册新用户之前,首先需要把admin用户enroll到系统中,为的是将admin证书写入到数据库Certificates表中,这个操作一次即可。
继续余下的字段,Jim若注册成功,他将可以作为["client", "peer", "validator"]这3种角色的注册者;但是由Jim注册的新成员,最多只能作为["client"]的注册者。
* Sig:上例中Sig留空,这个字段存储了userReq请求的签名,一个请求在发送之前,必须要通过注册者私钥进行签名,如果留空则后续处理会报空指针错误;如果签名错误,系统也会提示:
~~~
Signature verification failed.
~~~
**给请求签名**
注册前的最后一步就是给注册请求签名了,即给userReq.Sig赋予正确的值,签名的私钥用的是注册者的enrollmentKey(或者叫做privateKey),可以通过读取文件`viper.GetString("peer.fileSystemPath") + "/crypto/client/admin/ks/raw/enrollment.key"`得到raw数据,然后使用`primitives.PEMtoPrivateKey(raw, adminEnrollPwd)`解密得到enrollmentKey:
~~~
var path = viper.GetString("peer.fileSystemPath") + "/crypto/client/admin/ks/raw/enrollment.key"
raw, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println("failed loading registrar private key: " + err.Error())
return nil, err
}
privateKey, err := primitives.PEMtoPrivateKey(raw, []byte("Xurw3yU9zI0l"))
if err != nil {
fmt.Println("failed parsing private key: " + err.Error())
return nil, err
}
var registrarPrivateKey = privateKey.(*ecdsa.PrivateKey)
~~~
但这并不是一个好的方法,最好能找到已经封装好的接口,通过查看`fabric/core/crypto/node_impl.go`源码,可以看到在`nodeImpl`结构体中,包含privateKey字段:
~~~
// fabric/core/crypto/node_impl.go
type nodeImpl struct {
enrollPrivKey *ecdsa.PrivateKey
}
~~~
但这个字段并没有暴露出来,所以还需要找找别的方式。
后来在`fabric/core/crypto/crypto.go`文件中发现:
~~~
// fabric/core/crypto/crypto.go
type CertificateHandler interface {
// Sign signs msg using the signing key corresponding to the certificate
Sign(msg []byte) ([]byte, error)
}
~~~
这里介绍说使用和证书对应的签名秘钥,对数据进行签名,因为只有私钥才会用于签名,而公钥常常用来加密,所以这个应该正是我所需要的,再看Sign()的实现:
~~~
// fabric/core/crypto/client_ecert_handler.go
// Sign signs msg using the signing key corresponding to this ECert
func (handler *eCertHandlerImpl) Sign(msg []byte) ([]byte, error) {
return handler.client.signWithEnrollmentKey(msg)
}
// fabric/core/crypto/node_sign.go
func (node *nodeImpl) signWithEnrollmentKey(msg []byte) ([]byte, error) {
return primitives.ECDSASign(node.enrollPrivKey, msg)
}
~~~
这里赫然写着node.enrollPrivKey,正是私钥。所以使用CertificateHandler接口中的Sign()方法,是一个更好的途径。
余下的流程就简单了:
~~~
// sign the registration request with Registrar's private key
// 获取"admin"注册者客户端实例
adminClient, err := crypto.InitClient("admin", nil)
// 获取客户端的CertificateHandler接口
handler, err := adminClient.GetEnrollmentCertificateHandler()
// 将请求数据序列化为[]byte
raw, _ := proto.Marshal(registerUserReq)
// 使用admin的私钥对请求签名
result, err := handler.Sign(raw)
// 签名结果是序列化处理的[]byte,需要将其反序列化,以构建RegisterUserReq.Sig结构体
var ecdsaSignature = new(primitives.ECDSASignature)
asn1.Unmarshal(result, ecdsaSignature)
R, _ := ecdsaSignature.R.MarshalText()
S, _ := ecdsaSignature.S.MarshalText()
registerUserReq.Sig = &pbb.Signature{Type: pbb.CryptoType_ECDSA, R: R, S: S}
~~~
**发送注册请求**
注册者指定完成并已经enroll进CA系统中,请求已被正确签名,这时就可以发送注册请求了:
~~~
var address = "localhost:7054"
conn, err := grpc.Dial(address, grpc.WithInsecure()) // 连接address中指定的grpc端口
defer conn.Close()
ecaa := protos.NewECAAClient(conn)
token, err := ecaa.RegisterUser(context.Background(), registerUserReq)
~~~
**注册成功返回值**
一旦注册成功,token即可接收到方法返回值,token是个结构体:
~~~
// ca.pb.go
type Token struct {
Tok []byte `protobuf:"bytes,1,opt,name=tok,proto3" json:"tok,omitempty"`
}
~~~
token.Tok即为新用户的enrollPwd。
