用以太坊区块链保证Asp.Net Core的API安全(下)

上一篇用以太坊区块链保证Asp.Net Core的API安全(上)我们介绍了基本的解决方案,这一篇我们重点来看客户端。

正如我们所说,我们的DApp是一个简单的HTML/ES6客户端。我们将在Asp.Net Core 2之上构建客户端,以利用IIS Express和Visual Studio IDE。因此,Startup.cs类中的Configure方法将是:

1
2
3
4
5
6
7
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseDefaultFiles();
app.UseStaticFiles();

使DApp成为NPM项目并安装必备条件以使用ES6 Javascript标准。这不是强制性的,可以使用自己的堆栈构建DApp。

从项目文件夹运行Powershell并运行以下NPM命令:

1
2
3
4
5
6
7
8
npm init
npm install webpack
npm install babel-core babel-loader --save-dev
npm install babel-preset-es2015 --save-dev
npm install babel-preset-stage-0 --save-dev
npm install babel-polyfill --save
npm install babel-runtime --save
npm install babel-plugin-transform-runtime --save-dev

要配置webpack/babel,请使用以下配置创建webpack.config.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
var path = require("path");

module.exports = {
entry: [
"babel-polyfill",
"./src/main"
],
output: {
publicPath: "/js/",
path: path.join(__dirname, "/wwwroot/js/"),
filename: "main.build.js"
}
};

我们已设定webpacksrc/main.js文件构建到/www/js/main.build.js

安装以太坊扩展包:

1
2
npm install web3
npm install ethereumjs-util

Web3是一个javascript封装包,它简化了针对以太坊区块链的JSON RPC调用。Ethereumjs-util提供了一些以太坊特定的实用程序。让我们构建一个非常简单的HTML页面。我们需要一个登录按钮和另一个按钮来从我们的API层加载一些安全数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Ethereum Jwt Client</title>
</head>
<body>
<h1>Ethereum Jwt Client</h1>
<div id="login-view">
<label>Your account: </label> <span id="eth_account_span"></span>
<button type="submit" id="login_btn">Login</button>
</div>
<div id="data-view">
<button type="submit" id="load_data_btn">Request secured data</button>
<ul id="data_list">

</ul>
</div>
<script src="js/main.build.js"></script>
</body>
</html>

DApp逻辑将驻留在src/main.js文件中,正如我们在webpack.config.js文件中指定的那样。src/main.js文件将是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
let ethUtil = require('ethereumjs-util');
let Web3 = require('web3');

let coinbase = null;
let accessToken = null;

let init = () => {
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
web3.eth.getCoinbase(function (err, account) {
if (err === null && ethUtil.isValidAddress(account)) {
coinbase = account;
eth_account_span.innerHTML = coinbase;
} else {
eth_account_span.innerHTML = 'Please unlock your account and refresh the page';
console.error(err);
}
});

} else {
eth_account_span.innerHTML = 'Please install or unlock Metamask browser plugin or navigate this page with Mist or another web3 browser';
}
};

let request = obj => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(obj.method || "GET", obj.url);
if (obj.headers) {
Object.keys(obj.headers).forEach(key => {
xhr.setRequestHeader(key, obj.headers[key]);
});
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject(xhr.statusText);
xhr.send(obj.body);
});
};

login_btn.addEventListener('click', (e) => {
e.preventDefault();

login_btn.setAttribute('disabled', 'disabled');
login_btn.innerHTML = 'Please sign the message';

let plain = 'Hi, you request a login from client to Eth Jwt Api. Please sign this message. This is not a transaction, is completely free and 100% secure. We\'ll use your signature to prove your ownership over your private key server side.';
let msg = ethUtil.bufferToHex(new Buffer(plain, 'utf8'));
let hash = ethUtil.bufferToHex(ethUtil.keccak256("\x19Ethereum Signed Message:\n" + plain.length + plain));
let from = coinbase;

let params = [msg, from];
let method = 'personal_sign';

web3.currentProvider.sendAsync({
method,
params,
from,
}, function (err, result) {
if (err || result.error) {
login_btn.removeAttribute('disabled');
login_btn.innerHTML = 'Login';

console.error(err);
return console.error(result.error);
}
console.log({
'signature': result.result,
'msg': msg,
'hash': hash
});

login_btn.innerHTML = 'Requesting token...';
let loginData = {};
loginData.signer = from;
loginData.signature = result.result;
loginData.message = msg;
loginData.hash = hash;

request({
url: 'http://localhost:49443/api/token',
body: JSON.stringify(loginData),
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-type': 'application/json'
}
}).then(data => {
var json = JSON.parse(data);
accessToken = json.token;
console.log('access token: ' + accessToken);

login_btn.removeAttribute('disabled');
login_btn.innerHTML = 'Login';
}).catch(error => {
console.error(error);

login_btn.removeAttribute('disabled');
login_btn.innerHTML = 'Login';
});
});
});

load_data_btn.addEventListener('click', (e) => {
e.preventDefault();

request({
url: 'http://localhost:49443/api/values',
headers: {
'Authorization': 'Bearer ' + accessToken
}
}).then(data => {
var json = JSON.parse(data);
for (let i = 0; i < json.length; i++) {
data_list.innerHTML += '<li>' + json[i] + '</li>';
}
}).catch(error => {
console.error(err);
});

});

window.addEventListener('load', init);
  • 1.coinbaseaccessToken是全局变量,分别存储用户以太坊帐户和JWT token。
  • 2.init函数从Metamask提供的提供程序初始化web3对象,然后它尝试检索用户的帐户(coinbase)。这需要解锁在Metamask中签名的帐户。
  • 3.require函数只是hxr对象的封装,可以轻松地向API层调用ajax。
  • 4.load_data_btn单击处理程序对API层安全端点进行ajax调用。这需要有效的accessToken才能工作,否则,API层将响应401 HTTP响应。
  • 5.login_btn单击是一个两步功能。首先,它要求用户签署任意消息。签名后,它会将帐户,签名,明文消息和带前缀的哈希发送到令牌端点。

请注意,web3.personal.sign将十六进制格式(0x …)的普通字符串的字节数组作为输入。

正如我们所说的,服务器端,我们将使用两种不同的方式从签名中恢复公钥:在一个中我们将使用JSON RPC 接口中的web3.personal.ecrecover(web3.personal.sign对应);在另一个中,我们将使用底层的ecrecover离线功能。根据文档,web3.personal.sign使用底层签名函数来签署hash和前缀消息,因此,为了使用底层ecrecover对应,我们还需要计算并将此hash发送到令牌端点。

运行两个应用程序并使用安装了Metamask插件的浏览器导航到客户端。请记住,为了将src/main.js文件构建到js/main.build.js,你需要从Powershell运行webpac命令。如果一切正常,客户端将检索coinbase,你将在页面上看到你的帐户:

如果你现在单击“请求数据”按钮,将获得HTTP响应401。如果单击“登录”按钮,Metamask将提示你签名:

签名后,处理程序将对令牌端点进行ajax调用。在此阶段,身份验证方法不会检查任何签名,因此端点将始终发出JWT令牌。一旦收到JWT令牌,客户端就能通过ajax调用安全端点。如果现在单击“请求数据”按钮,将收到HTTP响应200和数据负载:

从签名中检索以太坊帐户

到目前为止,EthereumJwtApi是一个简单的JWT Asp.Net核心示例,因为它不提供任何有效的身份验证方法。

TokenController的关键部分是两个Authenticate方法及其从签名中检索以太坊帐户的能力。为此,你需要安装Nethereum.Web3 NuGet包。Nethereum是以太坊的.Net实现。

Authenticate方法只是对web3.personal.ecrecover函数进行JSON RPC调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async Task<UserVM> Authenticate(LoginVM login)
{
UserVM user = null;

var client = new RpcClient(new Uri(_config["Nethereum:Geth"])); // Require the RPC endpoint of a Geth node as input eg: http://127.0.0.1:8545
var signer = await client.SendRequestAsync<string>(new RpcRequest(1, "personal_ecRecover", login.Message, login.Signature));

if (signer.ToLower().Equals(login.Signer.ToLower()))
{
// read user from DB or create a new one
// for now we fake a new user
user = new UserVM { Account = signer, Name = string.Empty, Email = string.Empty };
}

return user;
}

PRO:

web3.personal.signweb3.personal.sign的对应部分,因此你无需担心其底层实现。

缺点:

需要你自己的Geth节点。不支持Parity,Infura不允许JSON RPC调用web3.personal.*Authenticate2方法显示了另一种方法,它使用底层ecrecover功能的离线实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async Task<UserVM> Authenticate2(LoginVM login)
{
UserVM user = null;

var signer = new Nethereum.Signer.MessageSigner();
var account = signer.EcRecover(login.Hash.HexToByteArray(), login.Signature);

if (account.ToLower().Equals(login.Signer.ToLower()))
{
// read user from DB or create a new one
// for now we fake a new user
user = new UserVM { Account = account, Name = string.Empty, Email = string.Empty };
}

return user;
}

PRO:

不需要JSON RPC调用就能工作。MessageSigner.EcRecoverNethereum提供的离线功能。

缺点:

你需要处理web3.personal.sign实现才能正确恢复帐户。出于这个原因,在客户端,我们相应地计算了前缀消息哈希。

结论

现在你拥有基本的知识和一个项目的骨架,可以使用以太坊保护你的Asp.Net Core 2 API。只需几点说明:

web3 1.0.0处于测试阶段,web3.personal.sign实现可能会随着时间的推移而变化。请务必在你可以维护的代码库上使用这种身份验证方法。也许Infura某天决定允许web3.personal.ecrecover :-)

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。

汇智网原创翻译,转载请标明出处。这里是原文