>[info] 本文作者:http://www.hmoore.net/@runningday
> 文檔地址:http://www.hmoore.net/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。