Java sdk使用加载账户私钥调用合约
1.智能合约案例
1.2 智能合约的流程
1.2 智能合约详细代码
实现了一个简单的商店功能。它定义了三个结构体:用户、商家和商品,以及对应的映射关系。它提供了一些方法用于注册用户和商家,创建商品,更新用户余额,购买商品等等。它还包括一些修饰符,用于限制只有用户或商家可以调用特定的方法。用户购买商品主要涉及到的是,检测当前用户是否有用户的权限,商家生产出售商品检测当前的用户是否有商家的权限。
主要的三个结构体
User
Merchant
Commodity
三个结构体分别是用户、商家、商品。
具体业务关系如下:
- 用户注册和商家注册 该合约允许用户和商家进行注册,分别在
userMap
和merchantMap
中将对应的用户地址和信息映射起来。 - 创建商品 商家可以调用
createCommodity
方法创建一种新的商品,包括商品名称、价格和数量,并将此商品信息存储在commodityMap
和commoditys
数组中,同时将商品数量映射到相应的商家地址(即commodityToMerchantMap
)上。 - 更新用户余额 用户可以通过
updateBalance
方法更新其账户余额。 - 购买商品 用户可以通过
buyCommodity
方法购买已有的商品,其中需要传入商品ID和购买数量。如果用户余额足够且商品库存充足,则会购买成功,同时商家收到相应的款项。 - 分页查询商品 用户可通过
queryAllCommoditys
方法查看所有商品并按照分页方式展示。 - 查询用户信息和商家信息 用户和商家都可以通过
queryUserInfo
和queryMerchantInfo
方法查询自己的信息。
pragma solidity ^0.4.25;pragma experimental ABIEncoderV2;contract Shop {// 用户的结构体struct User {address userAddress;stringuserName;uint256 userBalance;Rolerole;}// 商家的结构体struct Merchant {address merchantAddress;uint256 merchantBalance;Rolerole;}// 商品的结构体struct Commodity {uint256 commodityId;stringcommodityName;uint256 commodityPrice;uint256 commodityQuantity;}// 资产IDuint256 public commodityCount;// 角色枚举enum Role { UserType,MerchantType }// 地址映射用户信息mapping(address => User) userMap;// 地址映射商家信息mapping(address => Merchant) merchantMap;// 商品ID映射商品信息mapping(uint256 => Commodity) commodityMap;// 商品ID映射售卖的商家mapping(uint256 => address) commodityToMerchantMap;// 存储所有商品Commodity[] commoditys;// 判断当前是不是用户modifier AuthUser(address _userAddress) {require(userMap[_userAddress].role == Role.UserType,"当前不是用户");_;}// 判断当前是不是商家modifier AuthMerchant(address _merchantAddress) {require(merchantMap[_merchantAddress].role == Role.MerchantType,"当前不是商家");_;}// 注册事件 event Registered(address indexed _Address);// 商家添加商品事件event CreateCommodity(address indexed _Address,string _name);// 用户更新余额事件event UpdateBalance(address indexed _Address,uint256 indexed _amount);// 用户购买商品事件event BuyCommodity(address indexed _Address,uint256 indexed _commodityId);// 企业注册function registerUser( address _userAddress, string _userName ) public {User memory user = User({userAddress: _userAddress,userName: _userName,userBalance: 0,role: Role.UserType});userMap[_userAddress] = user;emit Registered(_userAddress);}// 商家注册function registerMerchant( address _merchantAddress, uint256 _merchantBalance ) public {Merchant memory merchant = Merchant({merchantAddress: _merchantAddress,merchantBalance: _merchantBalance,role: Role.MerchantType}); merchantMap[_merchantAddress] = merchant;emit Registered(_merchantAddress);}// 创建资产 function createCommodity(string memory _name,uint256 _price,uint256 _quantity) public AuthMerchant(msg.sender) {Commodity memory commodity = Commodity({commodityId: commodityCount,commodityName: _name,commodityPrice: _price,commodityQuantity: _quantity});commodityMap[commodityCount] = commodity;commoditys.push(commodity);commodityToMerchantMap[commodityCount] = msg.sender;commodityCount++;emit CreateCommodity(msg.sender,_name);}// 更新余额function updateBalance(address _userAddress,uint256 _amount) public AuthUser(msg.sender) {userMap[_userAddress].userBalance += _amount;emit UpdateBalance(msg.sender,_amount);}// 用户购买一个物品function buyCommodity(uint256 _commodityId,uint256 _amount) public AuthUser(msg.sender) {require(userMap[msg.sender].userBalance >= _amount,"当前余额不足");require(commodityMap[_commodityId].commodityQuantity != 0,"当前的物品没有库存");userMap[msg.sender].userBalance -= _amount;commodityMap[_commodityId].commodityQuantity--;merchantMap[commodityToMerchantMap[_commodityId]].merchantBalance += _amount;for (uint i = 0; i 0, "页数不能为0");uint256 startIndex = (page - 1) * pageSize; // 计算起始索引uint256 endIndex = startIndex + pageSize > commoditys.length ? commoditys.length : startIndex + pageSize; // 计算结束索引Commodity[] memory CommodityArr = new Commodity[](endIndex - startIndex); // 创建每页大小的 Enterprise 数组for (uint i = startIndex; i < endIndex; i++){CommodityArr[i - startIndex] = commoditys[i];}return CommodityArr;}// 查询用户信息function queryUserInfo() public returns(address,string memory,uint256,Role) {User memory user = userMap[msg.sender];return (user.userAddress,user.userName,user.userBalance,user.role);}// 查询商家信息function queryMerchantInfo() public AuthMerchant(msg.sender) returns(address,uint256,Role){Merchant memory merchant = merchantMap[msg.sender];return (merchant.merchantAddress,merchant.merchantBalance,merchant.role);}}
1.3 使用WeBASE-Front导出
使用基于FISCO BCOS区块链平台,以及WeBASE-Front部署智能合约,导出Java项目。
使用IDEA查看当前的项目。
2.Java SDK加载私钥调用合约
使用场景
Webase-front部署智能合约时,私钥的切换通常用于以下场景:
- 部署智能合约:在部署智能合约时,需要使用部署账户的私钥对合约进行签名,确保合约被正确地部署到区块链网络中。
- 调用智能合约:在调用智能合约时,需要使用调用账户的私钥对调用进行签名,确保调用请求的安全性和可信度。
- 管理智能合约:在管理智能合约时,可能需要使用不同的账户私钥进行操作,例如更新合约、授权其他账户对合约进行操作等。
在这些场景下,私钥的切换可以确保智能合约的安全性和可信度,同时也可以保护账户的私密信息不被泄露。
私钥存储的方式
- Java SDK支持通过私钥字符串或者文件加载,所以账户的私钥可以存储在
数据库
中或者本地文件
。 - 本地文件支持两种存储格式,其中
PKCS12
加密存储,而PEM
格式明文存储。 - 开发业务时可以根据实际业务场景选择私钥的存储管理方式。
这里我选择将私钥加密存储到数据库中,通过用户的地址去查询该地址的私钥或者公钥的方式,用户需要权限的时候,通过用户的地址获取私钥,通过加载用户的私钥传入调用合约的方法中。脱离默认的合约调用者。
针对Java SDK调用合约,有基于ABI和基于BIN的方式调用合约。
- 官网链接-基于ABI和BIN的方式: https://fisco-bcos-documentation.readthedocs.io/zh_CN/v2.8.0/docs/sdk/java_sdk/assemble_transaction.html
- 官网链接-账户管理: https://fisco-bcos-documentation.readthedocs.io/zh_CN/v2.8.0/docs/sdk/java_sdk/key_tool.html
如下是完整的项目结构图。
2.1 创建数据库表存储公私钥
虽然把私钥和公钥存储在Mysql中,但是还是不算很安全,所以存储的时候需要加盐加密。
create database shop default character set utf8mb4;create table shop_userKey(user_address varchar(60) comment '用户地址',user_privateKey text comment '用户私钥',user_publicKey text comment '用户公钥') comment '用户的公私钥表';
2.2 Gradlew添加依赖
在dependencies中添加相对应的依赖。
- mybatis-plus
- mysql-connector-java
- fastjson
compile group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.5.0'implementation 'mysql:mysql-connector-java:8.0.27'implementation group: 'com.alibaba', name: 'fastjson', version: '2.0.24'implementation group: 'org.apache.directory.studio', name: 'org.apache.commons.codec', version: '1.8'
2.3 连接数据库
spring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=UTC&useSSL=falsemybatis.configuration.cache-enabled=truemybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImplmybatis-plus.mapper-locations=classpath:/mapper/*.xmlmybatis-plus.global-config.id-type=automybatis-plus.global-config.db-config.table-prefix=shop_
2.3 创建实体类对象
创建UserKey
对象,主要是用来操作数据库。在用户注册的时候,需要给用户返回地址和公私钥,私钥仅提供一次下载。
import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableName;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;@Data@AllArgsConstructor@NoArgsConstructor@ToString@TableName(value = "shop_userKey")public class UserKey {@TableField(value = "user_address")private String userAddress;@TableField(value = "user_privateKey")private String userPrivateKey;@TableField(value = "user_publicKey")private String userPublicKey;}
创建User对象,用于返回前端页面用户的数据。
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;@Data@AllArgsConstructor@NoArgsConstructor@ToStringpublic class User {// 用户地址private StringuserAddress;// 用户名称private StringuserName;// 用户余额private int userBalance;// 用户角色private int role;}
创建Merchant对象,用于返回前端页面商家的信息。
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;import java.math.BigInteger;@Data@AllArgsConstructor@NoArgsConstructor@ToStringpublic class Merchant {// 商家地址private StringmerchantAddress;// 商家余额private int merchantBalance;// 用户角色private int role;}
创建Commodity的对象,用于返回前端页面的商品信息或者商品集合
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;import java.math.BigInteger;@Data@AllArgsConstructor@NoArgsConstructor@ToStringpublic class Commodity {// 商品IDprivateint commodityId;// 商品名称privateStringcommodityName;// 商品价格privateint commodityPrice;// 商品数量privateintcommodityQuantity;}
2.4 数据持久层
创建UserKeyMapper的接口,继承Mybatis-plus的接口,实现对数据库的CRUD操作。
这里主要是查询和删除两个操作,后续测试暂时需要用到这两个操作,如果有具体的场景还需要详细的操作。
@Mapperpublic interface UserKeyMapper extends BaseMapper<UserKey> {}public interface UserKeyService extends IService<UserKey> {public UserKey selectByUserAddress(String address);public boolean deleteByUserAddress(String address);}@Servicepublic class UserKeyServiceImpl extends ServiceImpl<UserKeyMapper, UserKey> implements UserKeyService {@Autowiredprivate UserKeyMapper userKeyMapper;@Overridepublic UserKey selectByUserAddress(String address) {if (address.isEmpty()){return null;}LambdaQueryWrapper<UserKey> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(UserKey::getUserAddress, address);return userKeyMapper.selectOne(queryWrapper);}@Overridepublic boolean deleteByUserAddress(String address) {if (address.isEmpty()){return false;}LambdaQueryWrapper<UserKey> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(UserKey::getUserAddress, address);userKeyMapper.delete(queryWrapper);return true;}}
2.5 使用AES算法加密工具类
AES算法是一种对称密钥加密算法,用于保护数据的机密性。它使用相同的密钥进行加密和解密操作。在AES算法中,数据被分成固定长度的块,并通过多轮的密钥混合和替换运算来加密。解密则通过逆向的操作将密文转换为明文。AES算法以其高安全性、高效性和广泛应用而受到广泛认可。
package org.example.Shop.utils;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.nio.charset.StandardCharsets;import java.security.Key;import java.util.Base64;public class EncryptionUtils {private static final String ALGORITHM = "AES"; // 加密算法private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; // 加密模式private static final String KEY = "MySecretKey12345";/** * 加密数据 * * @param data 待加密的数据 * @param KEY 密钥 * @return 加密后的数据 */public static String encrypt(String data) throws Exception {Key secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);Cipher cipher = Cipher.getInstance(TRANSFORMATION);cipher.init(Cipher.ENCRYPT_MODE, secretKey);byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encryptedBytes);}/** * 解密数据 * * @param encryptedData 加密后的数据 * @param KEY 密钥 * @return 解密后的数据 */public static String decrypt(String encryptedData) throws Exception {Key secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);Cipher cipher = Cipher.getInstance(TRANSFORMATION);cipher.init(Cipher.DECRYPT_MODE, secretKey);byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);byte[] decryptedBytes = cipher.doFinal(decodedBytes);return new String(decryptedBytes, StandardCharsets.UTF_8);}}
2.6 业务层实现
导出Java项目之后,在service层中,有写好的业务方法,可以直接调用,但是有一些方法和业务并不满足需求,比如需要切换账户调用合约,需要加载用户的私钥再调用合约进行交易。
- 用户注册的业务默认
- 商家注册的业务默认
Java SDK的org.fisco.bcos.sdk.crypto.CryptoSuite
提供了账户生成功能。
// 创建非国密类型的CryptoSuiteCryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);// 随机生成非国密公私钥对CryptoKeyPair cryptoKeyPair = cryptoSuite.createKeyPair();// 获取账户地址String accountAddress = cryptoKeyPair.getAddress();// 获取账户私钥String hexPrivateKey = cryptoKeyPair.getHexPrivateKey();// 获取账户公钥String hexPublicKey = cryptoKeyPair.getHexPublicKey();
加载用户的私钥:
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);// 加载用户私钥CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
由于我这里是随机生产的地址,所以没用pem
和p12
的方式保存和获取。
如下图:
商家需要创建一个苹果,但是创建苹果的用户和地址必须是商家的,不能是其他未注册或者是购买商品的用户去创建的。
2.6.1 添加用户注册获取公私钥
在UserKeyServiceImpl
的实现类中,添加一个方法makeNewUserKey()
,用于生成用户新注册时候随机生成的地址和公私钥,以及加密公私钥返回给数据库。
@Servicepublic class UserKeyServiceImpl extends ServiceImpl<UserKeyMapper, UserKey> implements UserKeyService {@Autowiredprivate UserKeyMapper userKeyMapper;@SneakyThrows@Overridepublic UserKey makeNewUserKey() {// 创建非国密类型的CryptoSuiteCryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);// 随机生成非国密公私钥对CryptoKeyPair cryptoKeyPair = cryptoSuite.createKeyPair();// 获取账户地址String keyPairAddress = cryptoKeyPair.getAddress();// 获取账户私钥String hexPrivateKey = cryptoKeyPair.getHexPrivateKey();// 获取账户公钥String hexPublicKey = cryptoKeyPair.getHexPublicKey();UserKey userKey = new UserKey();userKey.setUserAddress(keyPairAddress);// 对数据进行AES算法加密userKey.setUserPrivateKey(EncryptionUtils.encrypt(hexPrivateKey));userKey.setUserPublicKey(EncryptionUtils.encrypt(hexPublicKey));// 加密后存储数据库userKeyMapper.insert(userKey);return userKey;}// 其他业务代码......}
2.6.2 添加加载账户私钥
如下的操作是在service
层的默认导出的ShopService
类中修改。
这是一个公共的方法,可以复用,用户和商家传递私钥对合约的调用。
/** * 加载用户账户切换业务 * @param cryptoKeyPair * @param params * @return * @throws Exception */private TransactionResponse getTransactionResponse(CryptoKeyPair cryptoKeyPair,String funcName,List<Object> params) throws Exception {// 过创建和使用AssembleTransactionProcessor对象来调用和查询等操作。不部署合约,那么就不需要复制binary文件AssembleTransactionProcessor transactionProcessor = TransactionProcessorFactory.createAssembleTransactionProcessor(client, cryptoKeyPair,"src/main/resources/abi/","");// 使用同步方式发送交易TransactionResponse transactionResponse = transactionProcessor.sendTransactionAndGetResponseByContractLoader("Shop",this.address,funcName, params);return transactionResponse;}
2.6.3 添加商家加载私钥
商家注册之后需要保存地址信息,调用合约的时候携带了商家的地址,通过商家的地址查询获取数据库中加密的私钥,然后该方法主要是商家传入私钥和合约方法,通过加载商家的私钥,即可调用创建商品的方法。
/** * 商家添加商品信息 * @param input * @param cryptoKeyPair * @param params * @return * @throws Exception */public boolean createCommodity(CryptoKeyPair cryptoKeyPair,ShopCreateCommodityInputBO input) throws Exception {TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"createCommodity", input.toArgs());return transactionResponse.getReceiptMessages().equals("Success");}/** * 查询商家的信息 * @param cryptoKeyPair * @param params * @return * @throws Exception */public Merchant queryMerchantInfo(CryptoKeyPair cryptoKeyPair, List<Object> params) throws Exception {TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"queryMerchantInfo", params);return Optional.of(transactionResponse).filter(response -> "Success".equals(response.getReceiptMessages())).map(response -> JSON.parseArray(response.getValues())).filter(objects -> objects.size() >= 3).map(objects -> {Merchant merchant = new Merchant();merchant.setMerchantAddress(objects.getString(0));merchant.setMerchantBalance(objects.getInteger(1));merchant.setRole(objects.getInteger(2));return merchant;}).orElse(null);}
2.6.4 添加用户加载私钥
需要用户加载私钥进行的操作的是:
- 更新用户的余额
- 用户购买商品
这里的操作和上面是一样的,都是需要验证用户加载私钥后,是否为当前私钥的用户地址。
/** * 用户更新账户余额 * @param cryptoKeyPair * @param input * @return * @throws Exception */public TransactionResponse updateBalance(CryptoKeyPair cryptoKeyPair,ShopUpdateBalanceInputBO input) throws Exception {return getTransactionResponse(cryptoKeyPair,"updateBalance", input.toArgs());}/** * 用户购买商品 * @param cryptoKeyPair * @param input * @return * @throws Exception */public TransactionResponse buyCommodity(CryptoKeyPair cryptoKeyPair,ShopBuyCommodityInputBO input) throws Exception {return getTransactionResponse(cryptoKeyPair,"buyCommodity", input.toArgs());}
2.6.5 完整的业务代码
@Service@NoArgsConstructor@Datapublic class ShopService {public static final String ABI = org.example.Shop.utils.IOUtil.readResourceAsString("abi/Shop.abi");public static final String BINARY = org.example.Shop.utils.IOUtil.readResourceAsString("bin/ecc/Shop.bin");public static final String SM_BINARY = org.example.Shop.utils.IOUtil.readResourceAsString("bin/sm/Shop.bin");@Value("${system.contract.shopAddress}")private String address;@Autowiredprivate Client client;AssembleTransactionProcessor txProcessor;@PostConstructpublic void init() throws Exception {this.txProcessor = TransactionProcessorFactory.createAssembleTransactionProcessor(this.client, this.client.getCryptoSuite().getCryptoKeyPair());}/** * 用户更新账户余额 * @param cryptoKeyPair * @param input * @return * @throws Exception */public TransactionResponse updateBalance(CryptoKeyPair cryptoKeyPair,ShopUpdateBalanceInputBO input) throws Exception {return getTransactionResponse(cryptoKeyPair,"updateBalance", input.toArgs());}/** * 用户购买商品 * @param cryptoKeyPair * @param input * @return * @throws Exception */public TransactionResponse buyCommodity(CryptoKeyPair cryptoKeyPair,ShopBuyCommodityInputBO input) throws Exception {return getTransactionResponse(cryptoKeyPair,"buyCommodity", input.toArgs());}public TransactionResponse registerUser(ShopRegisterUserInputBO input) throws Exception {return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "registerUser", input.toArgs());}/** * 查询商家的信息 * @param cryptoKeyPair * @param params * @return * @throws Exception */public Merchant queryMerchantInfo(CryptoKeyPair cryptoKeyPair, List<Object> params) throws Exception {TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"queryMerchantInfo", params);return Optional.of(transactionResponse).filter(response -> "Success".equals(response.getReceiptMessages())).map(response -> JSON.parseArray(response.getValues())).filter(objects -> objects.size() >= 3).map(objects -> {Merchant merchant = new Merchant();merchant.setMerchantAddress(objects.getString(0));merchant.setMerchantBalance(objects.getInteger(1));merchant.setRole(objects.getInteger(2));return merchant;}).orElse(null);}/** * 加载用户账户切换业务 * @param cryptoKeyPair * @param params * @return * @throws Exception */private TransactionResponse getTransactionResponse(CryptoKeyPair cryptoKeyPair,String funcName,List<Object> params) throws Exception {AssembleTransactionProcessor transactionProcessor = TransactionProcessorFactory.createAssembleTransactionProcessor(client, cryptoKeyPair,"src/main/resources/abi/","");TransactionResponse transactionResponse = transactionProcessor.sendTransactionAndGetResponseByContractLoader("Shop",this.address,funcName, params);return transactionResponse;}/** * 商家添加商品信息 * @param input * @param cryptoKeyPair * @param params * @return * @throws Exception */public boolean createCommodity(CryptoKeyPair cryptoKeyPair,ShopCreateCommodityInputBO input) throws Exception {TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"createCommodity", input.toArgs());return transactionResponse.getReceiptMessages().equals("Success");}public TransactionResponse queryAllCommoditys(ShopQueryAllCommoditysInputBO input) throws Exception {return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "queryAllCommoditys", input.toArgs());}public TransactionResponse queryUserInfo() throws Exception {return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "queryUserInfo", Arrays.asList());}public TransactionResponse registerMerchant(ShopRegisterMerchantInputBO input) throws Exception {return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "registerMerchant", input.toArgs());}public CallResponse commodityCount() throws Exception {return this.txProcessor.sendCall(this.client.getCryptoSuite().getCryptoKeyPair().getAddress(), this.address, ABI, "commodityCount", Arrays.asList());}}
2.7 控制层实现
这里主要是做测试,为了更加的安全隐患,实际开发模式中需要使用POST请求,不能暴露出来。
2.7.1 RegisterController
主要实现的是:
- 用户的注册接口
- 商家的注册接口
- 用户和商家创建新的地址和随机公私钥接口
API接口 | 请求类型 | 请求参数 |
---|---|---|
/register | GET | 无 |
/register/user | POST | ShopRegisterUserInputBO |
/register/merchant | POST | ShopRegisterMerchantInputBO |
用户的请求接口示例:
http://localhost:8088/register/user{"_userAddress": "0xaf9f11d639600dffa32ce7f1c3474d1e280cf791","_userName": "张三"}
商家的请求接口示例:
http://localhost:8088/register/merchant{"_merchantAddress": "0xaf9f11d639600dffa32ce7f1c3474d1e280cf791","_merchantBalance": 1000}
@RestController@RequestMapping("register")public class RegisterController {@Autowiredprivate UserKeyService userKeyService;@Autowiredprivate ShopService shopService;/** * 用户注册创建新的随机地址和公私钥 * @return */@SneakyThrows@GetMappingpublic CommonResponse getAddressAndPrivateKey(){return CommonResponse.ok("Success",userKeyService.makeNewUserKey());}/** * 用户注册 * @param registerUserInputBO * @return */@SneakyThrows@PostMapping("user")public CommonResponse registerUser(@RequestBody ShopRegisterUserInputBO registerUserInputBO){if (StringUtils.isBlank(registerUserInputBO.get_userAddress()) || StringUtils.isBlank(registerUserInputBO.get_userName())){return CommonResponse.fail("400",new RuntimeException("参数为空"));}TransactionResponse transactionResponse = shopService.registerUser(registerUserInputBO);return CommonResponse.ok("注册成功",transactionResponse.getReceiptMessages());}/** * 商家注册 * @param registerMerchantInputBO * @return */@SneakyThrows@PostMapping("merchant")public CommonResponse registerMerchant(@RequestBody ShopRegisterMerchantInputBO registerMerchantInputBO){if (StringUtils.isBlank(registerMerchantInputBO.get_merchantAddress())){return CommonResponse.fail("400",new RuntimeException("参数为空"));}TransactionResponse transactionResponse = shopService.registerMerchant(registerMerchantInputBO);return CommonResponse.ok("注册成功",transactionResponse.getReceiptMessages());}}
2.7.2 UserController
主要实现的是:
- 用户获取商品的列表接口
- 用户购买商品的接口
- 用户更新账户余额的接口
API接口 | 请求类型 | 请求参数 |
---|---|---|
/users/{address} | POST | ShopQueryAllCommoditysInputBO |
/users/{address} | POST | ShopBuyCommodityInputBO |
/users/{address} | PUT | BigInteger |
用户的接口示例:
// 查询商品的列表POST http://localhost:8088/users/{address}{"page": 1,"pageSize": 4}// 用户购买商品POST http://localhost:8088/users/{address}{"_commodityId": 1,"_amount": 100}// 更新用户的余额PUT http://localhost:8088/users/{address}?balance=1000
@RestController@RequestMapping("users")public class UserController {@Autowiredprivate Client client;@Autowiredprivate ShopService shopService;@Autowiredprivate UserKeyService userKeyService;@SneakyThrows@GetMapping("{address}")public CommonResponse getCommodityList(@PathVariable String address,@RequestBody ShopQueryAllCommoditysInputBO commoditysInputBO){if (address.isEmpty() || commoditysInputBO.toArgs().isEmpty()){return CommonResponse.fail("400",new RuntimeException("查询异常"));}TransactionResponse transactionResponse = shopService.queryAllCommoditys(commoditysInputBO);if (transactionResponse.getReceiptMessages().equals("Success")){String responseValues = transactionResponse.getValues();int pageSize = commoditysInputBO.getPageSize();// 使用 intStream.range() 方法生成 [0, pageSize-1] 的整数流List<Commodity> commodities = IntStream.range(0, pageSize)// 把每一个整数映射成一个 JSONArray 对象.mapToObj(i -> JSON.parseArray(responseValues).getJSONArray(0).getJSONArray(i))// 把每个 JSONArray 转换成 Commodity 对象.map(jsonArray -> {// 根据 jsonArray 创建一个 Commodity 对象Commodity commodity = new Commodity();commodity.setCommodityId((Integer) jsonArray.get(0));commodity.setCommodityName((String) jsonArray.get(1));commodity.setCommodityPrice((Integer) jsonArray.get(2));commodity.setCommodityQuantity((Integer) jsonArray.get(3));return commodity;})// 把所有 Commodity 对象收集到一个 List 中.collect(Collectors.toList());return CommonResponse.ok("查询成功",commodities);}return CommonResponse.fail("400",new RuntimeException("查询异常"));}@SneakyThrows@PostMapping("{address}")public CommonResponse buyCommodity(@PathVariable String address, @RequestBody ShopBuyCommodityInputBO commodityInputBO){if (StringUtils.isBlank(address)){return CommonResponse.fail("400", new RuntimeException("当前用户无效"));}// 获取当前用户的私钥,并使用其创建加密套件及密钥对UserKey userKey = userKeyService.selectByUserAddress(address);CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));// 购买商品操作TransactionResponse transactionResponse = shopService.buyCommodity(cryptoKeyPair, commodityInputBO);if (transactionResponse.getReceiptMessages().equals("Success")){return CommonResponse.ok("购买成功",address);}return CommonResponse.fail("400",new RuntimeException("购买失败"));}@SneakyThrows@PutMapping("{address}")public CommonResponse updateBalance(@PathVariable String address,@RequestParam BigInteger balance){if (StringUtils.isBlank(address)){return CommonResponse.fail("400", new RuntimeException("当前用户无效"));}// 获取当前用户的私钥,并使用其创建加密套件及密钥对UserKey userKey = userKeyService.selectByUserAddress(address);CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));ShopUpdateBalanceInputBO balanceInputBO = new ShopUpdateBalanceInputBO(address, balance);// 更新账户余额操作TransactionResponse transactionResponse = shopService.updateBalance(cryptoKeyPair,balanceInputBO);if (transactionResponse.getReceiptMessages().equals("Success")){return CommonResponse.ok("更新成功",balance);}return CommonResponse.fail("400",new RuntimeException("更新失败"));}}
2.7.3 MerchantController
主要实现的是:
- 查看商家内部信息的接口
- 商家创建商品的接口
API接口 | 请求类型 | 请求参数 |
---|---|---|
/merchants?address={address} | GET | address |
/merchants/address | POST | ShopCreateCommodityInputBO |
商家的接口请求示例:
GET http://localhost:8088/merchants/?address={address}POST http://localhost:8088/merchants/address{"_name": "苹果","_price": 100,"_quantity": 20}
@RestController@RequestMapping("merchants")public class MerchantController {@Autowiredprivate UserKeyService userKeyService;@Autowiredprivate ShopService shopService;/** * 查询商家信息接口 * * @param address 用户地址(商家地址) * @return响应结果对象 */@SneakyThrows@GetMappingpublic CommonResponse queryMerchant(@RequestParam String address){if (StringUtils.isEmpty(address)){return CommonResponse.fail("400",new RuntimeException("查询失败"));}// 获取商家用户的私钥,并使用其创建加密套件及密钥对UserKey userKey = userKeyService.selectByUserAddress(address);CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);CryptoKeyPair cryptoKeyPair = null;cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));List<Object> params = new ArrayList<>();// 调用查询商家信息 假设这是商家的内部信息 普通用户不可查询Merchant merchant = shopService.queryMerchantInfo(cryptoKeyPair, params);if (Objects.isNull(merchant)){return CommonResponse.fail("400",new RuntimeException("当前角色不是商家"));}return CommonResponse.ok("查询成功",merchant);}/** * 创建商品接口 * * @param address 用户地址(商家地址) * @param createCommodityInputBO 商品信息参数对象 * @return响应结果对象 */@SneakyThrows@PostMapping("{address}")public CommonResponse createCommodity(@PathVariable String address,@RequestBody ShopCreateCommodityInputBO createCommodityInputBO){if (StringUtils.isEmpty(createCommodityInputBO.get_name())){return CommonResponse.fail("400",new RuntimeException("商品的名称不能为空"));}// 获取商家用户的私钥,并使用其创建加密套件及密钥对UserKey userKey = userKeyService.selectByUserAddress(address);System.out.println(userKey.getUserPrivateKey());CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));// 商家调用创建商品boolean commodity = shopService.createCommodity(cryptoKeyPair, createCommodityInputBO);return CommonResponse.ok("创建成功",commodity);}}
2.8 API测试
2.8.1 测试注册用户
GET http://localhost:8088/registerPOST http://localhost:8088/register/user
1.发送GET请求获取如下的信息:
- 用户的地址:
0x17b1730bb77db57313ac59c20c0aaae2efbaeab6
- 用户加密的私钥:
dzuF553WpMInWPeLqjP/dyZ4s381sEPeQni4+mY/L7Y50P46JZkLXiV7RaMYXjMxz1AD+cq9ktjWH4ZCX2jOXAbb0lJO2pSgRxctIDarhYM=
2.使用上面的用户地址,去注册一个叫”张三“的用户。如下图是注册成功。
3.这里显示注册成功,然后我们通过AES算法的加密工具类解密如下:
得到解密后的私钥: c8c25e5a9bc3485e90a2cab239861eae9e51a2d57bbb60ea7eda9a4c83267eac
4.在WeBASE-Front中导入私钥,并且创建zhangsan
的测试用户。
成功导入之后,直接使用这个账号调用queryUserInfo
的合约。因为我写查看用户合约的时候我加了检测当前地址是否有权限。
可以发现如下是直接交易成功,并且我是能查看到张三
的信息的。
- zhangsan的用户地址:
0x17B1730bb77db57313aC59C20C0AAAe2EFBAeAB6
对比一下上面接口的测试获取的用户地址,用户地址一模一样。
2.8.2 测试注册商家
GET http://localhost:8088/registerPOST http://localhost:8088/register/merchant
1.发送GET请求获取如下的信息:
- 商家的地址:
0x4ab865d51fbf01e2a102a86e56fe9e7358382dce
- 商家加密的私钥:
SxWg6lFxmvmp/uyIA0GBmkE1iaCh6jioFigyk+VSbTP4OuyK4HGSbinGKFTzDmwft7dz5hOsiJyUgQ0lQXeuqwbb0lJO2pSgRxctIDarhYM=
2.使用新的用户地址注册商家,注册资金为1000元。
3.通过解密后得到商家的私钥
- 商家的私钥:
f143333a17243b5d804c4162adbb68bf28fc75dfb7e3db680d48a1f1e2dfcb48
在WeBASE-Front导入私钥商家的名称为merchant
,调用查询商家的方法。
调用当前的queryMerchantInfo
的函数,查看交易如下:
- merchant的用户地址:
0x4aB865D51fbf01E2A102A86e56Fe9E7358382DcE
当我使用zhangsan
的用户去查看企业的内部信息的时候,会交易失败。显示当前不是商家。
4.使用接口测试查询商家的信息,结果和上面一样。
2.8.3 测试商家的添加商品
POST http://localhost:8088/merchants/0x4ab865d51fbf01e2a102a86e56fe9e7358382dce
1.使用商家的地址0x4ab865d51fbf01e2a102a86e56fe9e7358382dce
,添加两个商品,一个是苹果,一个是香蕉。如下添加成功。
2.使用用户分页查询当前的所有数据。
2.8.4 测试用户购买商品
PUT http://localhost:8088/users/{address}?balance=1000POST http://localhost:8088/users/{address}
1.给用户的账户更新余额100元。然后查询当前用户的余额。
如下是使用WeBASE-Front查看的余额。
2.用户购买商品,确定只有注册的用户是可以购买成功的。
通过WeBASE-Front调用zhangsan的账户查看个人信息,成功扣款和购买商品。
查看当前的所有商品信息,香蕉已经扣款数量为1。
查看当前的商家余额,已经到账100元。
2.8.5 测试是否加载了私钥
1.我特意在合约中用户购买商品的业务写了触发事件,假设用户购买了该商品,会记录当前的地址是谁购买的,可以通过查看Event事件的msg.sender
的地址是不是该用户的。
如下是合约的详细部分:
- 有AuthUser修饰符函数确定当前是否具备用户的权限
- 有BuyCommodity的事件
2.在用户购买商品的部分,也就是用户购买商品时候访问的接口,添加获取当前的合约调用的用户地址。用log的方式查看。
如下是添加的测试代码,用于观察。
@SneakyThrows@PostMapping("{address}")public CommonResponse buyCommodity(@PathVariable String address, @RequestBody ShopBuyCommodityInputBO commodityInputBO){if (StringUtils.isBlank(address)){return CommonResponse.fail("400", new RuntimeException("当前用户无效"));}// 获取当前用户的私钥,并使用其创建加密套件及密钥对UserKey userKey = userKeyService.selectByUserAddress(address);CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));// 购买商品操作TransactionResponse transactionResponse = shopService.buyCommodity(cryptoKeyPair, commodityInputBO);if (transactionResponse.getReceiptMessages().equals("Success")){log.info("======================================================");log.info("++++++++++++当前的合约调用者:" + transactionResponse.getTransactionReceipt().getFrom());log.info("++++++++++++当前的合约调用者:" + transactionResponse.getTransactionReceipt().getLogs().get(0).getTopics().get(1));return CommonResponse.ok("购买成功",address);}return CommonResponse.fail("400",new RuntimeException("购买失败"));}
调用此接口继续测试。
3.查看当前的控制台输出情况。
4.使用WeBASE-Front的Event的事件查看。
从日志和Event事件查看,合约的调用者都是0x17b1730bb77db57313ac59c20c0aaae2efbaeab6
地址。
根据以上的案例,可以理解私钥加载的使用场景。