前端js都能做区块链的相关内容了,推荐去看原文,理解起来应该不难。
原文:One-click Login with Blockchain: A MetaMask Tutorial 作者:AMAURY MARTINY
译文
在线用户对传统电子邮件/密码注册流程的抵抗力日益增强。通过Facebook,Google或GitHub的一键式社交登录功能被证明是更理想的选择。然而,它存在一种权衡。
社交媒体登录整合的优点:
社交媒体登录整合的缺点:
本文介绍一种新的登录方法:使用MetaMask扩展的一键式加密安全登录流程,所有数据都存储在我们自己的后端。我们称之为:“使用MetaMask登录”。
一张图片胜过千言万语,下面是我们将要构建的登录流程演示:
看起来不错?让我们开始吧!
其基本思想是通过使用私钥签署一段数据来加密证明易于证明帐户的所有权。如果您设法签署由我们的后端生成的精确数据,那么后端将认为您是该公共地址的所有者。因此,我们可以构建一个基于消息签名的身份验证机制,并将用户的公共地址作为标识符。
如果它看起来不太清楚,那是对的,因为我们会逐点解释它:
请注意,尽管我们将使用连接到以太坊区块链(MetaMask,Ethereum公共地址)的工具,但此登录过程实际上并不需要区块链:它只需要其密码功能。话虽如此,随着MetaMask成为如此流行的扩展,现在似乎是引入此登录流程的好时机。
如果您已经知道MetaMask是什么,请随意跳过本节。
MetaMask是一个浏览器插件,可用作MetaMask Chrome扩展或Firefox插件。它的核心是它作为一个以太坊钱包:通过安装它,您将可以访问一个独特的以太坊公开地址,您可以通过它开始发送和接收以太币或令牌。
但MetaMask比Ethereum钱包做得更多。作为浏览器扩展,它可以与您正在浏览的当前网页进行交互。它通过在您访问的每个网页中注入一个名为web3.js的JavaScript库来实现。一旦注入,一个web3
对象将被通过本网站的JavaScript代码的window.web3
提供。要查看此对象的外观,只需在Chrome或Firefox DevTools控制台(如果已安装MetaMask)键入window.web3
即可。
Web3.js是以太坊区块链的JavaScript界面。有以下功能:
web3.eth.getBlockNumber
)web3.eth.coinbase
)上的当前活动帐户web3.eth.getBalance
)web3.eth.sendTransaction
)web3.personal.sign
)当安装MetaMask时,任何前端代码都可以访问所有这些功能,并与区块链交互。他们被称为dapps或DApps(分散式的应用程序,有时甚至是病急乱投医“ĐApps”)。
与DApp开发相关: 时间锁定钱包:以太坊智能合同简介
web3.js中的大多数函数都是读函数(get block,get balance等),并且web3
会立即给出响应。但是,某些功能(如web3.eth.sendTransaction
和web3.personal.sign
)需要当前帐户使用其私钥签署一些数据。这些功能触发MetaMask显示确认屏幕,以检查用户是否知道他或她正在签名。
我们来看看如何使用MetaMask。要进行简单测试,请将以下行粘贴到DevTools控制台中:
web3.personal.sign(web3.fromUtf8("Hello from Toptal!"), web3.eth.coinbase, console.log);
这个命令意味着:使用coinbase帐户(即当前帐户)签署我的消息,从utf8转换为十六进制,并作为回叫,打印签名。将出现一个MetaMask弹出窗口,如果您签名,则会打印签名消息。
我们将在我们的登录流程中使用web3.personal.sign
。
关于本部分的最后一点注意事项:MetaMask将web3.js注入到当前的浏览器中,但实际上还有其他独立浏览器,它们也会注入web3.js,例如Mist。然而,在我看来,MetaMask今天为普通用户提供了最好的用户体验和最简单的转换,以探索dapps。
我们先来看看how。How将有望说服你,这是安全的,所以我会保持why 部分短。
正如概述中所述,我们会忘记区块链。我们有一个传统的Web 2.0客户机 - 服务器RESTful体系结构。我们将做出一个假设:访问我们前端网页的所有用户都安装了MetaMask。有了这个假设,我们将展示无密码加密安全登录流程如何工作。
首先,我们的User
模型需要有两个新的必需字段:publicAddress
和nonce
。另外,publicAddress
需要是独一无二的。你可以保留通常的username
,email
和password
字段 - 特别是如果你想实现你的MetaMask登录并行到电子邮件/密码登录 - 但它们是可选的。
publicAddress
如果用户希望使用MetaMask登录,注册过程也会略有不同,如注册时所需的字段。放心,用户将永远不需要publicAddress
手动输入,因为它可以通过web3.eth.coinbase
。
对于数据库中的每个用户,在该nonce
字段中生成一个随机字符串。例如,nonce
可以是一个大的随机整数。
在我们的前端JavaScript代码中,假设MetaMask存在,我们可以访问window.web3
。因此,我们可以致电web3.eth.coinbase
获取当前MetaMask帐户的公开地址。
当用户点击登录按钮时,我们会向后端发起API调用以检索与其公共地址相关的随机数。类似于具有过滤器参数的路由GET /api/users?publicAddress=${publicAddress}
应该这样做。当然,由于这是未经过身份验证的API调用,因此后端应配置为仅显示nonce
此路由上的公共信息(包括)。
如果前一个请求没有返回任何结果,则表示当前的公共地址尚未注册。我们需要先通过POST /users
传递publicAddress
请求主体来创建一个新帐户。另一方面,如果有结果,那么我们将其nonce
存储。
一旦前端收到nonce
前一个API调用的响应,它将运行以下代码:
web3.personal.sign(nonce, web3.eth.coinbase, callback);
这将提示MetaMask显示签名消息的确认弹出窗口。该随机数将显示在此弹出框中,以便用户知道她或他没有签署某些恶意数据。
当她或他接受它时,将使用签名消息(调用signature
)作为参数调用回调函数。然后,前端进行另一个API调用POST /api/authentication
,将一个body与both signature
和publicAddress
。
当后端接收到POST /api/authentication
请求时,它首先在数据库publicAddress
中根据请求体中的给定内容提取用户。特别是它提取相关的随机数。
具有随机数,公共地址和签名后,后端可以用密码验证用户已经正确签署了随机数。如果是这种情况,那么用户证明了公共地址的所有权,我们可以考虑她或他的身份验证。然后可以将JWT或会话标识符返回到前端。
为了防止用户再次使用相同的签名登录(以防被盗用),我们确保下次同一用户想要登录时,她或他需要签署新的随机数。这是通过nonce
为该用户生成另一个随机数并将其保存到数据库来实现的。
Etvoilà!这就是我们管理无签名无密码登录流程的方式。
根据定义,身份验证实际上只是帐户所有权的证明。如果您使用公开地址唯一标识您的帐户,那么它的密码不重要,以证明您拥有它。
为了防止黑客掌握一个特定的消息和您的签名(但不是您的实际私钥),我们强制该消息签名为:
我们在解释每次成功登录后都对其进行了更改,但基于时间戳的机制也是可以想象的。
在本节中,我将逐一完成上述六个步骤。我将展示一些关于如何从零开始构建登录流的代码片段,或者将它集成到现有的后端,而不需要太多的努力。
为了本文的目的,我创建了一个小型演示应用程序。我使用的堆栈如下:
我尝试尽可能少地使用图书馆。我希望代码很简单,以便您可以轻松地将其移植到其他技术堆栈。
整个项目可以在这个GitHub仓库中看到。演示托管在这里。
需要两个字段:publicAddress
和nonce
。我们初始化nonce
为一个随机大数。这个数字应该在每次成功登录后进行更改。我还在username
这里添加了一个可选字段,用户可以更改。
const User = sequelize.define('User', {
nonce: {
allowNull: false,
type: Sequelize.INTEGER.UNSIGNED,
defaultValue: () => Math.floor(Math.random() * 1000000) // Initialize with a random nonce
},
publicAddress: {
allowNull: false,
type: Sequelize.STRING,
unique: true,
validate: { isLowercase: true }
},
username: {
type: Sequelize.STRING,
unique: true
}
});
为了简单起见,我将publicAddress
字段设置为小写字母。一个更严格的实现会添加一个验证函数来检查这里的所有地址都是有效的以太坊地址。
这是在defaultValue()
上面模型定义中的函数中完成的。
下一步是在后端添加一些样板代码来处理User
模型上的CRUD方法,这在这里我们不会做。
切换到前端代码时,当用户单击登录按钮时,我们的handleClick
处理程序会执行以下操作:
class Login extends Component {
handleClick = () => {
// --snip--
const publicAddress = web3.eth.coinbase.toLowerCase();
// Check if user with current publicAddress is already present on back end
fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`)
.then(response => response.json())
// If yes, retrieve it. If no, create it.
.then(
users => (users.length ? users[0] : this.handleSignup(publicAddress))
)
// --snip--
};
handleSignup = publicAddress =>
fetch(`${process.env.REACT_APP_BACKEND_URL}/users`, {
body: JSON.stringify({ publicAddress }),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}).then(response => response.json());
}
在这里,我们正在检索MetaMask活动帐户web3.eth.coinbase
。然后我们检查这publicAddress
是否已经存在或不在后端。我们要么检索它,如果用户已经存在,或者如果不存在,我们在handleSignup
方法中创建一个新帐户。
让我们继续前进我们的handleClick
方法。我们现在拥有一个由后端提供的用户(不管是检索还是新创建的)。特别是,我们有他们nonce
和publicAddress
。所以我们准备publicAddress
使用与此关联的私钥对nonce进行签名web3.personal.sign
。这在handleSignMessage
功能中完成。
请注意,web3.personal.sign
将字符串的十六进制表示作为其第一个参数。我们需要使用UTF-8编码的字符串转换为十六进制格式web3.fromUtf8
。此外,我不是只签署随机数,而是决定签署更友好的句子,因为它将显示在MetaMask确认弹出窗口中:I am signing my once-time nonce: ${nonce}
。
class Login extends Component {
handleClick = () => {
// --snip--
fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`)
.then(response => response.json())
// If yes, retrieve it. If no, create it.
.then(
users => (users.length ? users[0] : this.handleSignup(publicAddress))
)
// Popup MetaMask confirmation modal to sign message
.then(this.handleSignMessage)
// Send signature to back end on the /auth route
.then(this.handleAuthenticate)
// --snip--
};
handleSignMessage = ({ publicAddress, nonce }) => {
return new Promise((resolve, reject) =>
web3.personal.sign(
web3.fromUtf8(`I am signing my one-time nonce: ${nonce}`),
publicAddress,
(err, signature) => {
if (err) return reject(err);
return resolve({ publicAddress, signature });
}
)
);
};
handleAuthenticate = ({ publicAddress, signature }) =>
fetch(`${process.env.REACT_APP_BACKEND_URL}/auth`, {
body: JSON.stringify({ publicAddress, signature }),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}).then(response => response.json());
}
当用户成功签署消息时,我们转到该handleAuthenticate
方法。我们只是发送请求到/auth
后端的路由,发送我们publicAddress
以及signature
用户刚签名的消息。
这是稍微复杂一点的部分。后端在/auth
包含a publicAddress
和a 的路由上接收请求signature
,并且需要验证这publicAddress
是否签署了正确的请求nonce
。
第一步是从数据库中检索用户说的publicAddress
; 只有一个,因为我们将其定义publicAddress
为数据库中的唯一字段。然后,我们将该消息设置msg
为“我正在签署我的...”,就像在步骤4的前端中一样,此用户的随机数。
下一个块是验证本身。有一些密码学涉及。如果您觉得冒险,我建议您阅读更多关于椭圆曲线签名的内容。
总结这个块,它所做的是,由于我们的msg
(包含nonce
)和我们signature
的ecrecover
函数输出用于签名的公共地址msg
。如果它与publicAddress
请求主体中的我们相匹配,那么成功请求请求的用户证明了他们的所有权publicAddress
。我们认为他们是认证的。
User.findOne({ where: { publicAddress } })
// --snip--
.then(user => {
const msg = `I am signing my one-time nonce: ${user.nonce}`;
// We now are in possession of msg, publicAddress and signature. We
// can perform an elliptic curve signature verification with ecrecover
const msgBuffer = ethUtil.toBuffer(msg);
const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
const signatureBuffer = ethUtil.toBuffer(signature);
const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
const publicKey = ethUtil.ecrecover(
msgHash,
signatureParams.v,
signatureParams.r,
signatureParams.s
);
const addressBuffer = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBuffer);
// The signature verification is successful if the address found with
// ecrecover matches the initial publicAddress
if (address.toLowerCase() === publicAddress.toLowerCase()) {
return user;
} else {
return res
.status(401)
.send({ error: 'Signature verification failed' });
}
})
成功验证后,后端生成一个JWT并将其发送回客户端。这是一个经典的认证方案,所以我不会把代码放在这里。
出于安全原因,最后一步是更改随机数。在成功认证后的某个地方,添加以下代码:
// --snip--
.then(user => {
user.nonce = Math.floor(Math.random() * 1000000);
return user.save();
})
// --snip--
这并不难,是吗?再次,如果你想看看整个应用程序是如何连接的(JWT代,CRUD路线,localStorage等),请随时查看GitHub仓库。
尽管区块链可能存在缺陷并且仍处于幼年阶段,但我无法强调如何在现有的任何网站上实现此登录流程。以下是为什么此登录流程优于电子邮件/密码和社交登录的一系列参数:
当然,MetaMask登录流程可以很好地与其他传统登录方法并行使用。需要在每个帐户和它拥有的公共地址之间进行映射。
但是这个登录流程并不适合所有人:
web3
启用浏览器,此登录流程显然不起作用。如果你的观众对加密货币不感兴趣,他们甚至有可能考虑安装MetaMask。随着最近的密码繁荣,让我们希望我们正在走向一个Web 3.0互联网。正如我们所看到的,web3
这是登录流程的先决条件。在桌面浏览器上,MetaMask将其注入。但是,移动浏览器上没有扩展功能,因此此移动版Safari浏览器,Chrome或Firefox无法使用此登录流程。有一些独立的移动浏览器,web3
基本上在浏览器中注入了MetaMask。在撰写本文时,它们相当早,但如果您有兴趣,请查看Cipher,Status和Toshi。“使用MetaMask登录”适用于这些移动浏览器。
关于移动应用程序,答案是肯定的,登录流程正常,但准备工作有很多基础。基本上,你需要自己重建一个简单的以太坊钱包。这包括公共地址生成,种子字恢复和安全私钥存储,以及web3.personal.sign
确认弹出窗口。幸运的是,有些图书馆可以帮助您。关键的关键领域是自然安全,因为应用程序本身就拥有私钥。在桌面浏览器上,我们将此任务委派给MetaMask。
所以我会争辩说,简短的答案是否定的,这个登录流程今天不适用于移动设备。正在朝这个方向努力,但今天的简单解决方案仍然是移动用户的并行传统登录方法。
我们在本文中介绍了一键加密安全的登录流程,没有涉及第三方,称为“使用MetaMask登录”。我们解释了后端生成的随机随机数的数字签名如何证明账户的所有权,从而提供身份验证。我们还探讨了与桌面和移动设备上的传统电子邮件/密码或社交登录相比,此登录机制的权衡。
尽管今天这种登录流量的目标受众仍然很少,但我真诚地希望你们中的一些人能够感受到启发,在您自己的网络应用程序中提供与MetaMask的登录,与传统的登录流程并行 - 我很想听听它如果你这样做。如果您有任何问题,请随时联系下面的评论。
Amaury拥有五年以上构建全栈网络和移动应用程序(Node.js,React,React Native)的经验。他目前在Parity Technologies担任区块链应用程序开发人员。他对创业真正感兴趣,并曾为各种规模的初创公司工作。在业余时间,他阅读机器学习,并在他的旅行博客上撰写关于搭便车,文化和哲学的文章。