区块链系列教程(10)
Fisco Bcos 权限控制下的数据上链实操演练
哈希科技作为一家区块链技术服务商,有责任也有义务为推广区块链贡献一份力量。计划连续推出区块链教程,欢迎大家交流。
哈希科技拥有全面的区块链能力,支持Hyperledger Fabric、Ethereum、FISCO BCOS及腾讯Trust SQL等主流框架。
FISCO BCOS是微众银行、腾讯等联合推出的,安全可控、适用于金融行业且开源的区块链底层平台。哈希科技与微众区块链团队进行过深度对接合作,也得到了微众团队童鞋们的鼎力支持和帮助,在此一并感谢!
本教程作者为哈希科技CTO林滨,这是一位才华在线颜值担当的90后有为青年,推荐大家关注。
一、目的
前面已经完成fisco bcos 相关底层搭建、sdk使用、控制台、webase中间件平台等系列实战开发,
本次进行最后一个部分,体系化管理区块链底层,建立有序的底层控管制度,实现权限化管理。
完成:链管理、系统管理、数据上链操作等。
其中数据上链分为:合约版本上链、crudService 版本上链等操作。
二、准备工作
在进行之前,我们首先要了解一下,fisco bcos 的底层权限系统介绍。
https://mp.weixin.qq.com/s/QJNk71w4o_cGX2O-1aW29Q
三、设计理念差异
1、底层默认是可以部署合约,只有一旦操作 grantDeployAndCreateManager 命令,才开始限制用户部署合约权限
ps:一开始权限基本开放,而不是像常规系统设计那样,一开始权限为无,等到分配好权限才可以相应的的操作。
2、底层默认是写表操作,写表操作就有了CRUD等操作,常规的区块链体系是在不断区块打包过程中附加数据,Fisco Bcos 提供写表操作,实质上业务数据可以有修改的权限,
所以在链搭建好后,就必须限制Update等操作权限,并且在业务设计时候,需要多方去验证修改数据等过程,才可以防止区块链底层数据让高权限的人的篡改。
ps:与官方人员交谈,提供的例子
比如官方给出的存证的例子,一个证据X,需要A,B机构确认。先是证据X上链(一笔交易),然后机构A看到证据,
确认有效(又一笔交易),机构B演的证据,确认有效(又一笔交易)。三笔交易完成业务共识的逻辑。别人取证X的时候,读区块链,查看是否A,B都确认过,确认过了,证据X才有效。
四、场景构想搭建
我们在底层完成各种系统化的搭建,现在要使用多种方式进行权限管控,以及数据上链等操作。
1、我们首先要建立链管理员、系统管理员、普通用户。
2、其次我们要使用控制台 或sdk进行管理员等设定。
3、对合约的部署权限、以及数据权限更新操作做授权。
4、在sdk 中配置授权用户的pem、p12密钥的使用。
5、使用合约操作,进行合约部署,合约CRUD操作。
6、使用CRUDService 进行数据操作。
7、使用PermissionService 进行权限控制。
最后根据我们设定好的权限,完成多个不同身份用户进行的数据操作,这样我们就完成生产环境下的数据上链操作。
备注:以下操作,可以参考web3sdk单元测试。官方github web3sdk 地址:https://github.com/FISCO-BCOS/web3sdk
五、实操演练
(一)创建和管理用户
创建用户,用于后续的管理员设置,进行权限管理设置。
官方资料:https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/tutorial/account.html
fisco bcos 底层 支持控制台创建账户、sdk 创建账户
先采用控制台进行基础用户创建,以及基本全是设置
在console 目录下 使用bash get_account.sh、bash get_account.sh -p 创建不同的账户
Usage: ./get_account.sh
default generate account and store private key in PEM format file(默认生成pem格式用户)
-p generate account and store private key in PKCS12 format file(默认生成p12格式用户)
-k [FILE] calculate address of PEM format [FILE](通过pem 私钥文件生成账户地址)
-P [FILE] calculate address of PKCS12 format [FILE](通过p12 私钥文件生成账户地址)
-h Help
创建链管理员 ,pem 格式
ubuntu@VM-16-14-ubuntu:~/generator/console$ bash get_account.sh
[INFO] Account Address : 0x83a37766067ea59eea78135b20a4afc251246e88
[INFO] Private Key (pem) : accounts/0x83a37766067ea59eea78135b20a4afc251246e88.pem
创建三个用户
1、具有 发布合约权限的用户
ubuntu@VM-16-14-ubuntu:~/generator/console$ bash get_account.sh -p
Enter Export Password:123456
Verifying - Enter Export Password:123456
[INFO] Account Address : 0xb93bb9276d97f5f75ea574965beab99f72310e45
[INFO] Private Key (p12) : accounts/0xb93bb9276d97f5f75ea574965beab99f72310e45.p12
2、系统管理员
ubuntu@VM-16-14-ubuntu:~/generator/console$ bash get_account.sh -p
Enter Export Password:
Verifying - Enter Export Password:
[INFO] Account Address : 0xca96eb0e7c70c9117a2b5bda65cbcfc1b37a35c2
[INFO] Private Key (p12) : accounts/0xca96eb0e7c70c9117a2b5bda65cbcfc1b37a35c2.p12
3、普通用户
ubuntu@VM-16-14-ubuntu:~/generator/console$ bash get_account.sh -p
Enter Export Password:
Verifying - Enter Export Password:
[INFO] Account Address : 0x190b5d0a7ed4754c9226ee96c50cd125ec5720bf
[INFO] Private Key (p12) : accounts/0x190b5d0a7ed4754c9226ee96c50cd125ec5720bf.p12
pem登录方式:
bash ./start.sh 1 -pem accounts/0x83a37766067ea59eea78135b20a4afc251246e88.pem
p12 登录方式:(需要输入密码)
bash ./start.sh 1 -p12 accounts/0xb93bb9276d97f5f75ea574965beab99f72310e45.p12
(二)控制台基本权限设置
设定账户1为链管理员账户,账户2为系统管理员账户,账户3为普通账户。
1、链管理员账户拥有权限管理的权限,即能分配权限。
[group:1]> grantDeployAndCreateManager 0x83a37766067ea59eea78135b20a4afc251246e88
{
"code":0,
"msg":"success"
}
2、系统管理员账户可以管理系统相关功能的权限,每一种系统功能权限都需要单独分配,
具体包括部署合约和创建用户表的权限、管理节点的权限、利用CNS部署合约的权限以及修改系统参数的权限。链管理员账户可以授权其他账户为链管理员账户或系统管理员账户,
[group:1]> grantDeployAndCreateManager 0xb93bb9276d97f5f75ea574965beab99f72310e45
{
"code":0,
"msg":"success"
}
3、也可以授权指定账号可以写指定的用户表,即普通账户。
用某个用户登录后,使用命令赋予权限,前提是他拥有该权限操作
权限命令:
https://fisco-bcos-documentation.readthedocs.io/zh_CN/release-2.0/docs/manual/permission_control.html
(三)Sdk下配置
1、将控制台生成account 目录下的文件copy sdk中。
applicationContext.xml 文件配置修改,其中包括密钥文件的配置。
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
对应的节点ip:20201
对接节点ip:20200
127.0.0.1:20202
127.0.0.1:20203
如果要写单元测试
也要把相关配置copy 过去,详情见图
(四)单元测试 合约部署,CRUD操作
1、基础设置,包括具有部署合约权限用户设置
private Credentials credentials;
private static BigInteger gasPrice = new BigInteger("300000000");
private static BigInteger gasLimit = new BigInteger("300000000");
@Autowired
Web3j web3j;
protected String tempDirPath = new File("src/main/resources/").getAbsolutePath();
//这很重要,没有这个无法通过
@Before
public void setUp() throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext(
"classpath:applicationContext-keystore-sample.xml");
// test p12
P12Manager p12 = context.getBean(P12Manager.class);
ECKeyPair p12KeyPair = p12.getECKeyPair();
System.out.println("p12KeyPair.getPrivateKey() = " + p12KeyPair.getPrivateKey().toString(16));
System.out.println("p12KeyPair.getPublicKey() = " + p12KeyPair.getPublicKey().toString(16));
ECPublicKey publicKey = (ECPublicKey) p12.getPublicKey();
byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);
BigInteger publicKeyValue =
new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length));
System.out.println("publicKeyValue = " + publicKeyValue.toString(16));
credentials = Credentials.create(p12KeyPair);//这里将具有合约部署权限的用户设置进去,当前操作对象
System.out.println("credentials getAddress= " + credentials.getAddress());
}
@After
public void tearDown() {
}
2、部署合约
@Test
//1、部署合约
public void DeployTable() throws Exception {
// 部署合约
TableTemp tableTemp = TableTemp.deploy(web3j, credentials, new StaticGasProvider(gasPrice, gasLimit)).send();
if (tableTemp != null) {
System.out.println("TableTemp address is: " + tableTemp.getContractAddress());
}
}
3、创建表合约操作,此时,需要用到部署合约时候生成的地址
//2、创建表操作
@Test
public void CreateTableTest()throws Exception {
String contractAddress = "0xb4245b7b6cc33f8f65d8bf37f084dec3e31ca573";//合约部署时候的生成的地址
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
TransactionReceipt receipt = tableTemp.create().send();
System.out.println("AssetTest.AssetTransfer receipt="+receipt.toString());
}
4、插入表操作(合约地址改为自己测试的,笔者由于多次单元测试,合约地址不同)
//3.1、插入表操作 无返回值操作
@Test
public void InsertTableTest()throws Exception {
String contractAddress = "0x215ac9f7af5766ff45d80082091856b54fcf4308";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
String name = "wq";
int item_id = Integer.parseInt("2");
String item_name ="aaa";
String item_address ="ddd";
tableTemp.insert(name, BigInteger.valueOf(item_id), item_name,item_address).send();
}
//3.2、插入表操作
@Test
public void InsertTableByReturnTest()throws Exception {
String contractAddress = "0xb4245b7b6cc33f8f65d8bf37f084dec3e31ca573";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
String name = "ak";
int item_id = Integer.parseInt("1");
String item_name ="tempUser";
String item_address ="北京";
TransactionReceipt send = tableTemp.insert(name, BigInteger.valueOf(item_id), item_name, item_address).send();
System.out.println(" send= "+send.toString());
}
5、删除表操作
//4.1删除表数据操作
@Test
public void DeleteTableTest()throws Exception {
String contractAddress = "0x215ac9f7af5766ff45d80082091856b54fcf4308";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
TransactionReceipt send= tableTemp.remove("ak",BigInteger.valueOf(1)).send();
System.out.println("send.toString() = " + send.toString());
System.out.println("send.getContractAddress() = " + send.getContractAddress());
System.out.println("send.getBlockHash() = " + send.getBlockHash());
System.out.println("send.getBlockNumber() = " + send.getBlockNumber());
}
//4.2删除表数据操作
@Test
public void DeleteTableByReturnTest()throws Exception {
String contractAddress = "0xb4245b7b6cc33f8f65d8bf37f084dec3e31ca573";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
int item_id = Integer.parseInt("1");
String name="ak";
RemoteCall remove =
(RemoteCall) tableTemp.remove(name,BigInteger.valueOf(item_id));
TransactionReceipt transactionReceipt = remove.send();
List removeResultEvents =
tableTemp.getRemoveResultEvents(transactionReceipt);
if (removeResultEvents.size() > 0) {
TableTemp.RemoveResultEventResponse reomveResultEventResponse = removeResultEvents.get(0);
System.out.println(
"removeCount = " + reomveResultEventResponse.count.intValue());
} else {
System.out.println("tableTemp table does not exist.");
}
}
6、查询表操作,由于合约返回字段有限,最好不要超过三个,有可能会报错。如果使用返回strust是可以解决该问题的。
开头加上这个:pragma experimental ABIEncoderV2;
返回值可以用struct
//5、表查询
@Test
public void SelectTableTest() throws Exception{
String contractAddress = "0xa94c07af700bf2e435a3051b0a98f2f75eca0298";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
Tuple3, List, List> lists = tableTemp.select("ak").send();
//这个只是返回合约执行的结果,不会返回本身数据库表数据
// System.out.println("send.toString() = " + send.toString());
List value1 = lists.getValue1();
List value2 = lists.getValue2();
List value3 = lists.getValue3();
for (int i = 0; i < value1.size(); i++) {
String name = new String(value1.get(i));
System.out.println("name = " + name);
int item_id = value2.get(i).intValue();
System.out.println("item_id = " + item_id);
String item_name = new String(value3.get(i));
System.out.println("item_name = " + item_name);
}
}
7、更新表操作,当前笔者还没设置更新操作权限,所以该方法是可以修改表数据
//6、表更新
@Test
public void UpateTableTest() throws Exception{
String contractAddress = "0xb940c1966a6ce94484f0fdabd3bb8cb38edc9dfd";
// 加载合约地址
TableTemp tableTemp = TableTemp.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
TransactionReceipt ak= tableTemp.update("ak",BigInteger.valueOf(1),"121","wew").send();
System.out.println("ak.toString() = " + ak.toString());
}
ps:由于合约操作在实际中返回值等问题,可能存在许多不确定等坑,所以和官方聊之后,可以使用crudService进行表的操作。
(五) 采用CrudService 进行表数据操作
CrudService 本身就是对表的操作的服务封装。
这里的单元测试需要用到 基础设置TestBase,主要配置具有表操作权限的用户。
package customTest;
import java.math.BigInteger;
import java.util.Arrays;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.fisco.bcos.channel.client.P12Manager;
import org.fisco.bcos.channel.client.Service;
import org.fisco.bcos.web3j.crypto.Credentials;
import org.fisco.bcos.web3j.crypto.ECKeyPair;
import org.fisco.bcos.web3j.protocol.Web3j;
import org.fisco.bcos.web3j.protocol.channel.ChannelEthereumService;
import org.fisco.bcos.web3j.tx.gas.StaticGasProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestBase {
public static ApplicationContext context = null;
public static Credentials credentials;
protected static Web3j web3j;
protected static BigInteger gasPrice = new BigInteger("30000000");
protected static BigInteger gasLimit = new BigInteger("30000000");
protected static String address;
protected static BigInteger blockNumber;
protected static String blockHash;
protected static String txHash;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
Service service = context.getBean(Service.class);
service.run();
ChannelEthereumService channelEthereumService = new ChannelEthereumService();
channelEthereumService.setChannelService(service);
web3j = Web3j.build(channelEthereumService, service.getGroupId());
ApplicationContext context =
new ClassPathXmlApplicationContext(
"classpath:applicationContext-keystore-sample.xml");
// test p12
P12Manager p12 = context.getBean(P12Manager.class);
ECKeyPair p12KeyPair = p12.getECKeyPair();
System.out.println("p12KeyPair.getPrivateKey() = " + p12KeyPair.getPrivateKey().toString(16));
System.out.println("p12KeyPair.getPublicKey() = " + p12KeyPair.getPublicKey().toString(16));
ECPublicKey publicKey = (ECPublicKey) p12.getPublicKey();
byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);
BigInteger publicKeyValue =
new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length));
System.out.println("publicKeyValue = " + publicKeyValue.toString(16));
credentials = Credentials.create(p12KeyPair);
System.out.println("credentials getAddress= " + credentials.getAddress());
}
@AfterClass
public static void setUpAfterClass() throws Exception {
((ClassPathXmlApplicationContext) context).destroy();
}
}
以下是crudservice 的单元测试全过程操作,依法操作就可以了,关键地方已经有了注释。
package customTest;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.fisco.bcos.Application;
import org.fisco.bcos.channel.client.P12Manager;
import org.fisco.bcos.temp.TableTemp;
import org.fisco.bcos.web3j.crypto.Credentials;
import org.fisco.bcos.web3j.crypto.ECKeyPair;
import org.fisco.bcos.web3j.precompile.crud.CRUDService;
import org.fisco.bcos.web3j.precompile.crud.Condition;
import org.fisco.bcos.web3j.precompile.crud.Entry;
import org.fisco.bcos.web3j.precompile.crud.Table;
import org.fisco.bcos.web3j.protocol.Web3j;
import org.fisco.bcos.web3j.tx.gas.StaticGasProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*
* 1、继承 TestBase,配置文件在main的java的resources中applicationContext.xml
* */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class CRUDServiceTest extends TestBase{
//这里如果设置了权限,那么必须权限用户才可以使用,请看TestBase 中的改造,credentials
private CRUDService crudSerivce = new CRUDService(web3j, credentials);
@SuppressWarnings("unchecked")
@Test
public void CreateTest() throws Exception {
String tableName = "t_item2";
String key = "name";
String valueFields = "item_id, item_name,item_address,item_count";
Table table = new Table(tableName, key, valueFields);
// create table
int resultCreate = crudSerivce.createTable(table);
Assert.assertEquals(resultCreate, 0);
}
@SuppressWarnings("unchecked")
@Test
public void Insert()throws Exception{
String tableName = "t_item2";
String key = "name";
Table table = new Table(tableName, key);
int insertResult = 0;
int num = 5;
for(int i = 1; i <= num; i++)
{
Entry insertEntry = table.getEntry();
insertEntry.put("item_id", "q");
insertEntry.put("item_name", "q"+i);
insertEntry.put("item_address", "q"+i);
insertEntry.put( "item_count",BigInteger.valueOf(i).toString());
table.setKey("q");
insertResult += crudSerivce.insert(table, insertEntry);
}
Assert.assertEquals(insertResult, num);
}
@SuppressWarnings("unchecked")
@Test
public void Select()throws Exception{
String tableName = "t_item2";
String key = "name";
Table table = new Table(tableName, key);
// select records
Condition condition1 = table.getCondition();
condition1.EQ("name", "q");
condition1.EQ("item_id", "q");
// condition1.Limit(1);
table.setKey("q");//查询记录sql语句必须在where子句中提供表的主键字段值。