如何实现MetaMask签名授权后DAPP一键登录功能?
网站太多,各种用户名/密码实在记不住。所以我们逐渐接受了BAT账号的授权登录功能。在以太坊DAPP应用中,也可以使用MetaMask实现授权后一键登录功能。MetaMask是去中心化钱包,授权信息不会如BAT中心一样存在被收集利用的问题。 本文从技术层面讲清楚原理,并结合代码说明如何实现。
我们往往被自己的密码难住,越来越抵制传统的电子邮件/密码注册流程。通过微信,QQ,支付宝,Facebook,Google或GitHub一键式社交登录功能可以省去记住密码或者密码泄露的而风险。当然,它也需要权衡利弊。
社交媒体登录集成的优点:
社交媒体登录集成的缺点:
加密猫( https://www.cryptokitties.co/ )游戏中,用户不需要输入用户名,密码就可以建立自己的账户体系,进行登录交易。
1.签名导入-cancel.png
本文介绍下这个方法的原理和代码实现,使用 MetaMask扩展的 一键式加密安全登录流程,所有数据都存储在我们自己的后端。我们称为“使用MetaMask登录”。
一张价值千言万语的图片,这里是我们要构建的登录流程的演示:
显示MetaMask登录演示的动画。
看起来不错?让我们开始吧!
一键式登录流程的基本思想是,通过使用私钥对一段数据进行签名,可以很容易地通过加密方式证明帐户的所有权。如果您设法签署由我们的后端生成的精确数据,那么后端将认为您是该钱包地址的所有者。因此,我们可以构建基于消息签名的身份验证机制,并将用户的钱包地址作为其标识符。
如果它看起来不太清楚,那就没问题了,因为我们会逐一解释它:
请注意,虽然我们将使用连接到 以太坊区块 链的工具(MetaMask,以太坊钱包地址),但此登录过程实际上并不需要区块链:它只需要其加密功能。话虽如此,随着MetaMask成为 如此受欢迎的扩展 ,现在似乎是介绍此登录流程的好时机。
如果您已经知道MetaMask是什么,请跳过本节。
MetaMask 是一个浏览器插件,可作为 MetaMask Chrome扩展 或 Firefox附加组件使用 。它的核心是它作为以太坊钱包:通过安装它,您将可以访问一个独特的以太坊钱包地址,您可以使用它开始发送和接收以太币或ERC20通证。
但MetaMask不仅仅是以太坊钱包。作为浏览器扩展,它可以与您正在浏览的当前网页进行交互。它通过在您访问的每个网页中注入一个名为 web3.js 的JavaScript库来实现。注入后, web3
将通过 window.web3
的JavaScript代码为你访问的每个网页提供一个对象。要查看此对象,只需在Chrome或Firefox DevTools控制台键入 window.web3
(如果已安装MetaMask),结果如下图。
web3.js
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, etc.), web3
立即给出响应。但是,某些功能(如 web3.eth.sendTransaction
和 web3.personal.sign
)需要当前帐户使用其私钥对某些数据进行签名。这些函数触发MetaMask显示确认弹窗,以仔细检查用户是否知道他或她正在签名的内容。
让我们看看如何使用MetaMask。要进行简单测试,请在DevTools控制台中粘贴以下行:
web3.personal.sign(web3.fromUtf8("你好,我是辉哥!!"), web3.eth.coinbase, console.log);
此命令表示:使用coinbase帐户(即当前帐户)将我的消息(从utf8转换为十六进制)进行签名,并以打印作为回调函数打印出签名。输入回车后,将出现MetaMask弹窗,如果点击签名按钮,将打印签名的消息。
MetaMask确认弹出窗口
我们将 web3.personal.sign
在登录流程中使用。
关于这一部分的最后一点说明:MetaMask将web3.js注入到您当前的浏览器中,但实际上还有其他独立的浏览器也会注入web3.js,例如 Mist 。但是,在我看来,MetaMask为普通用户提供了探索dapps的最佳用户体验和最简单的转换。
这是如何做到的呢?这部分内容讲说服你,证明这种方式是安全的。所以为什么部分的介绍就比较短了。
如前面所述,我们将忘记区块链。我们有一个传统的Web 2.0客户端 - 服务器RESTful架构。我们将做出一个假设:访问我们的前端网页的所有用户都安装了MetaMask。有了这个假设,我们将展示无密码加密安全登录流程的工作原理。
首先,我们的 User
模型需要有两个新的必填字段: publicAddress
和 nonce
。此外, publicAddress
需要具有唯一性。你可以保持平常 username
, email
和 password
字段,特别是如果你想平行实现您MetaMask登录电子邮件/密码登录,但它们是可选的。
如果用户希望使用MetaMask登录,则注册过程也会略有不同,因为注册时 publicAddress
将是必填字段。不过请放心,用户永远不需要手动输入 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
,传递一个带有 signature
和 publicAddress
的消息体。
当后端收到 POST /api/authentication
请求时,它首先根据请求消息体中 publicAddress
获取数据库中的对应用户,特别是它相关的随机数 nonce
。
具有随机数,钱包地址和签名后,后端可以 加密地验证 用户已正确签署了随机数。如果确认是这种情况,那么用户已经证明了拥有钱包地址的所有权,我们可以考虑对她或他进行身份验证。然后可以将JWT或会话标识符返回到前端。
为了防止用户使用相同的签名再次登录(如果它被泄露),我们确保下次同一用户想要登录时,她或他需要签署一个新的nonce。这是通过 nonce
为该用户生成另一个随机数并将其持久保存到数据库来实现的。
这就是我们管理nonce签名无密码登录流程的方法。
根据定义,身份验证实际上只是帐户所有权的证明。如果您使用钱包地址唯一地标识您的帐户,那么证明您加密方式拥有该帐户就非常简单。
为了防止黑客获取某个特定邮件及其签名(但不是您的实际私钥),我们会强制需要签名的消息满足以下条件:
在我们的demo样例中,每次成功登录后我们都改变了它,但也可以设想基于时间戳的机制。
MetaMask登录流程的六个步骤概述。
在本节中,我将逐一完成上述六个步骤。我将展示一些代码片段,以便我们如何从头开始构建此登录流,或者将其集成到现有的后端,而不需要太多努力。
为了本文的目的,我创建了一个小型演示应用程序。我正在使用的堆栈如下:
我尝试使用尽可能少的库。我希望代码足够简单,以便您可以轻松地将其移植到其他技术堆栈。
访问 https://login-with-metamask.firebaseapp.com/ 可以获得一个演示,也可以参考步骤搭建自己的本地工程。
欢迎加入辉哥的知识星球,从中下载本案例代码工程,也可加专门微信群交流技术问题。
需要两个字段: publicAddress
和 nonce
。我们初始化 nonce
为随机大数。每次成功登录后都应更改此号码。我还在 username
这里添加了一个可选字段,用户可以更改。
.\backend\src\models\user.model.js
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
字段设置为小写。更严格的检查地址是否是有效的以太坊地址的方法参考链接: https://ethereum.stackexchange.com/questions/1374/how-can-i-check-if-an-ethereum-address-is-valid )。
这是在 defaultValue()
上面的模型定义中的函数中完成的。
下一步是在后端添加一些样板代码来处理 User
模型上的CRUD方法,我们在这里不做。
切换到前端代码,当用户单击登录按钮时,我们的 handleClick
处理程序执行以下操作:
.\frontend\src\Login\Login.js
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
。此外,我决定签署一个更加用户友好的句子,而不是仅签署nonce,因为它将显示在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
包含一个 publicAddress
和一个路由上接收请求签名 signature
,并且需要验证钱包地址 publicAddress
是否已签名正确的随机数 nonce
。
第一步是从数据库中检索用户所说的 publicAddress
; 只有一个因为我们 publicAddress
在数据库中定义为唯一字段。然后,我们将消息设置 msg
为“I am signing my one-time nonce...”,与步骤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并将其发送回客户端。这是一种经典的身份验证方案,所以我不会在这里放置代码。
出于安全原因,最后一步是更改nonce。在成功验证后的某处,添加以下代码:
// --snip--
.then(user => {
user.nonce = Math.floor(Math.random() * 1000000);
return user.save();
})
// --snip--
虽然区块链可能有其缺陷并且仍处于早期阶段,但我无法强调如何在今天的任何现有网站上实施此登录流程的重要性。以下是为什么此登录流程优先于电子邮件/密码 和 社交登录的参数列表:
当然,MetaMask登录流程可以很好地与其他传统登录方法并行使用。需要在每个帐户与其拥有的钱包地址之间进行映射。
但是这个登录流程并不适合所有人:
web3
的浏览器,此登录流程显然无效。如果您的受众对加密货币不感兴趣,他们甚至会考虑安装MetaMask。随着最近的通证热潮,让我们希望我们正在走向 Web 3.0互联网 。正如我们所见,这 web3
是此登录流程的先决条件。在桌面浏览器上,MetaMask会注入它。但是,移动浏览器没有扩展程序,因此此登录流程无法在移动版Safari,Chrome或Firefox上开箱即用。有一些独立的移动浏览器注入了 web3
基于MetaMask的浏览器。在撰写本文时,它们还处于早期阶段,但如果您有兴趣,请查看 Cipher , Status 和 Toshi 。“使用MetaMask登录”适用于这些移动浏览器。
关于移动应用程序,答案是肯定的,登录流程有效,但需要有很多准备工作的作为基础。作为基本准备工作,您需要自己重建一个简单的以太坊钱包。这包括钱包地址生成,种子文字恢复和安全私钥存储,以及 web3.personal.sign
确认弹出窗口。幸运的是,有library可以帮助您。人们关心的关键信息是安全的,因为应用程序本身拥有私钥。在桌面浏览器上,我们将此任务委托给MetaMask。
所以我认为答案是否定的,这个登录流程今天不适用于移动设备。但它正朝着这个方向努力,今天简单的解决方案仍然是移动用户的并行传统登录方法。 【辉哥备注】虽然主流的移动端浏览器APP还不支持MetaMask插件,但是包括TrustWallet, AlphaWallet等钱包自带的DAPP浏览器支持支付功能,也就不需要MetaMask钱包用于支付了。手机端的一键登录问题转换为别的实现方案的问题。
辉哥采用Windows 环境下搭建Ubuntu Linux环境的方式,在Windows环境访问目标测试程序,所以需要修改前后端调用的IP地址为本地地址。就是http://192.168.0.103为Ubuntu服务器的IP地址,如果调用前端也在linux下运行则可使用http://127.0.0.1地址。
.\login-with-metamask-demo\frontend.env.development
REACT_APP_BACKEND_URL=http://192.168.0.103:8000/api
.\login-with-metamask-demo\frontend\src\registerServiceWorker.js
window.location.hostname === 'http://192.168.0.103' ||
修改后,然后把完整的login-with-metamask-demo工程上传至linux工作目录下。
在新的命令窗口运行以下命令,完成安装和服务器运行:
npm install -g yarn yarn yarn dev
安装运行成功的输出内容:
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ yarn dev
yarn run v1.10.1
$ nodemon --exec babel-node src/
[nodemon] 1.17.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `babel-node src/`
sequelize deprecated String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html##operators node_modules/sequelize/lib/sequelize.js:242:13
Express app listening on localhost:8000
^C
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ ls
Dockerfile node_modules package.json src yarn.lock
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ rm -r -f node_modules
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ ls
Dockerfile package.json src yarn.lock
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ npm install -g yarn
/home/duncanwang/.nvm/versions/node/v8.11.4/bin/yarn -> /home/duncanwang/.nvm/versions/node/v8.11.4/lib/node_modules/yarn/bin/yarn.js
/home/duncanwang/.nvm/versions/node/v8.11.4/bin/yarnpkg -> /home/duncanwang/.nvm/versions/node/v8.11.4/lib/node_modules/yarn/bin/yarn.js
+ yarn@1.10.1
updated 1 package in 4.201s
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ yarn
yarn install v1.10.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 173.70s.
duncanwang@ubuntu:~/work/login-with-metamask-demo/backend$ yarn dev
yarn run v1.10.1
$ nodemon --exec babel-node src/
[nodemon] 1.17.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `babel-node src/`
sequelize deprecated String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html##operators node_modules/sequelize/lib/sequelize.js:242:13
Express app listening on localhost:8000
在前端程序根目录下
yarn yarn start 安装运行成功的输出内容:
duncanwang@ubuntu:~/work/login-with-metamask-demo/frontend$ yarn
yarn install v1.10.1
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] Resolving packages...
[2/4] Fetching packages...
info There appears to be trouble with your network connection. Retrying...
info There appears to be trouble with your network connection. Retrying...
info fsevents@1.1.3: The platform "linux" is incompatible with this module.
info "fsevents@1.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 266.85s.
duncanwang@ubuntu:~/work/login-with-metamask-demo/frontend$ yarn start
yarn run v1.10.1
$ react-scripts start
Starting the development server...
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000/
On Your Network: http://192.168.0.103:3000/
Note that the development build is not optimized.
To create a production build, use yarn build.
在Windows浏览器运行客户端程序,点击完成SIGN签名授权:
登录后,更新用户的名字。
不需要输入密码,完成了duncanwang和钱包地址0xD1F7922e8b78cBEB182250753ade8379d1E09949的关联和一键登录功能。
我们在本文中介绍了一键式,加密安全的登录流程,没有涉及第三方,称为“使用MetaMask登录”。我们解释了后端生成的随机数的数字签名如何证明帐户的所有权,从而提供身份验证。我们还探讨了这种登录机制与传统电子邮件/密码或社交登录相比的权衡,无论是在桌面还是在移动设备上。
即使今天这样的登录流程的目标受众仍然很小,我真诚地希望你们中的一些人感到鼓舞,在你自己的网络应用程序中提供与MetaMask一起登录,与传统登录流程并行。
本文由辉哥翻译自 《One-click Login with Blockchain: A MetaMask Tutorial》 ,并做适当的修改以便中国用户可以更好的理解。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!