惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
O
OpenAI News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
Webroot Blog
Webroot Blog
GbyAI
GbyAI
S
SegmentFault 最新的问题
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
J
Java Code Geeks
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 【当耐特】
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
S
Security Affairs
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
Spread Privacy
Spread Privacy
T
Threatpost
Forbes - Security
Forbes - Security
C
Cisco Blogs
Scott Helme
Scott Helme
Attack and Defense Labs
Attack and Defense Labs
Simon Willison's Weblog
Simon Willison's Weblog
腾讯CDC
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
Last Week in AI
Last Week in AI
Recorded Future
Recorded Future
小众软件
小众软件
V
Vulnerabilities – Threatpost
美团技术团队
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
Martin Fowler
Martin Fowler
博客园 - 聂微东

BAIYUN'S BLOG

AI Coding 从小工到专家 - BAI YUN 电信 IPTV 内网融合教程:在局域网任意设备观看 IPTV - BAI YUN 美国住宅 IP 提供商体验对比 - BAI YUN Node.js 程序以 Keyless 模式从 1Password 读取密钥 - BAI YUN 高效优雅的观影指南 - BAI YUN 基于 Surge 的 DNS 优化指南 - BAI YUN RIME 鼠须管输入法简明使用教程 - BAI YUN 自动给不同 Git 项目设置不同的用户名邮箱 - BAI YUN Web3 新手如何选择加密货币钱包 - BAI YUN Web3 到底是什么? - BAI YUN 谨慎开启 Safari 的阻止跨站跟踪功能 - BAI YUN iOS Surge 开启后无法使用部分国产 APP 或网络出错的解决办法 - BAI YUN 屏蔽 Telegram 垃圾私信的有效办法 - BAI YUN 每个人都应该读一遍《1984》和《动物庄园》 - BAI YUN iOS 给常见机构号码增加联系人头像 - BAI YUN 在国内如何购买比特币和投资加密货币 - BAI YUN WEB 性能优化:如何让你的网站页面秒开(方法论) - BAI YUN 加密货币投资这件事 - BAI YUN 国内手机号注册推特教程 - BAI YUN 国内注册币安账号并通过 KYC 教程 - BAI YUN 使用一套助记词创建多个以太坊钱包账户 - BAI YUN Apple Watch 的几个用途 - BAI YUN 钱包安全指南:如何保护你的 NFT 和加密资产不被盗 - BAI YUN 我在 macOS 上高频使用的应用 - BAI YUN 2022 年了 Safari 无法渲染渐变文字的 BUG 依旧存在 - BAI YUN 境外手机卡:澳门电信大湾区预付卡使用体验 - BAI YUN 解决 iCloud Drive 上传文件卡住的问题 - BAI YUN Electron 应用如何获取系统代理配置 - BAI YUN 解决 iOS 上 Google 账号登录会卡在加载状态的问题 - BAI YUN 记录 Github Copilot 惊讶到我的时刻 - BAI YUN 解决 Xbox 在国内下载速度慢 - BAI YUN 开始使用 IndexNow - BAI YUN Surge iOS 和 AmpliFi 的兼容性问题 - BAI YUN 自建图床指南 - BAI YUN 使用 TypeScript 提升可维护性 - BAI YUN 在 JavaScript 应用中正确的实现时区切换 - BAI YUN 从 Loading 动画出发改进 React 应用的用户体验 - BAI YUN Node.js Tips - BAI YUN 使用 Dart Sass 代替 Node Sass - BAI YUN WSL 踩的坑和解决方法 - BAI YUN NPM & Yarn Tips - BAI YUN Git Tips - BAI YUN macOS 命令行 Tips - BAI YUN JavaScript 防劫持 - BAI YUN Dota2 - BAI YUN macOS 好用的系统维护工具 - BAI YUN 在 Linux 服务器上使用 Puppeteer 生成 PDF - BAI YUN The Golden Circle 黄金圆环法则 - BAI YUN CSS 写法进化史 - BAI YUN 一个 Web 开发者的 Windows10 开发环境设置心得 - BAI YUN Chrome DevTools Tips - BAI YUN React 设计模式和思想 - BAI YUN Linux & Unix 命令行环境的一些 Tips - BAI YUN 从传统 MVC 到同构:Web 发展历程 - BAI YUN Webpack 动态导入 - BAI YUN JavaScript 中的异步 - BAI YUN 关于我的博客 - BAI YUN
Web3 时代:如何使用 MetaMask 钱包实现一键登录 - BAI YUN
baiyun · 2022-04-30 · via BAIYUN'S BLOG

前言

在 web2 时代,几乎所有的网站和应用都需要自行保存用户的账户信息,且每家的账号不通用,用户需要记住所有的这些账号信息,为了管理这些账号密码信息,业界甚至诞生了很多类似 1Password 这样的密码管理器帮助用户管理繁多的账号信息。即使后来大多数网站都接入了基于 OAuth2 的社交媒体账号登录功能,但本质上依然存在上述问题,且最重要的是这些账号和账号产生的数据"可以不属于"用户所有。

到了目前的 web3 时代就明显不一样了,如果你经常使用 web3 相关的产品,可能已经发现这些 web3 的网站和应用基本都不提供"注册"功能,且登录的时候都是通过连接加密钱包来完成的,用户不需要输入用户名密码。下图是一个典型的 web2 应用和 web3 应用的登录页面对比图。

web2时代的登录页面对比web3时代的登录页面

以太坊钱包登录的原理

现如今的加密钱包帐户主要是基于公私钥加密机制,用户的帐户地址是公开的,这相当于公钥,钱包可以使用用户的私钥对一串数据进行签名从而拿到一串签名数据,之后其他用户可以在没有你的私钥的情况下通过密码学方式验证某一串数据是否和指定的签名相匹配。如果匹配则表示该用户是帐户的持有者。

我们用 Ethers.js 做个简单的代码示例:

本文使用的 ethers 版本是 ^5

import { ethers } from 'ethers'

// 这里省略了 web3Modal 的配置相关代码
const provider = await web3Modal.connect()
await provider.enable() // 连接到钱包,这一步钱包会弹出对话框向用户询问是否连接到当前站点

// The "any" network will allow spontaneous network changes
const web3Provider = new ethers.providers.Web3Provider(provider, 'any')

// The MetaMask plugin also allows signing transactions to
// send ether and pay to change state within the blockchain.
// For this, you need the account signer...
const signer = web3Provider.getSigner()

const message = `请签名证明你是钱包账户的拥有者\n\nNonce\n${Date.now()}`

// signMessage 方法会让钱包弹出对话框询问用户是否同意签名,用户同意后我们就可以拿到签名
const signature = await signer.signMessage(message)

// 调用后端登录 API,把当前用户的钱包地址以及上面的 message 和 signature 传给后端
loginApi({ signature, message, address })

有了签名后,我们在服务端使用 verifyMessage 方法验证 message 是否和签名匹配,如果签名是匹配的,则 verifyMessage 方法返回的地址肯定会和用户的钱包地址一致(比较过程需要忽略大小写)

// ethers 可以在 Node.js 和浏览器环境运行
import { ethers } from 'ethers'

function loaginHandler(req, res, next) {
  const { message, signature, address } = req.body

  const recoveredAddress = ethers.utils.verifyMessage(message, signature)
  if (recoveredAddress.toLowerCase() === address.toLoawerCase()) {
    res.json({ message: '登录成功了' })
  }
}

以上代码我们通过 Ethers.js 提供的 signer.signMessage()ethers.utils.verifyMessage() 这两个方法验证了用户是否为某个地址的持有者。对于验证通过的用户,我们就可以用 JWT Token 之类的方法给用户颁发鉴权信息,从而实现登录功能。

更详细的流程如下:

  1. 连接钱包
  2. 前端从服务端获取 nonce,或者自己生成
  3. 前端生成要进行签名的 message,这个 message 里要包含上一步生成的 nonce
  4. 前端调用 signMessage(message) 方法把 message 传进去让用户签名
  5. 前端拿到签名后,把签名、message、钱包地址这三项提交给登录 API
  6. 后端先检查 message 里面 nonce 是否有效
  7. nonce 检查通过后,后端调用 verifyMessage(message, signature) 方法检查此方法返回的地址是否和用户提供的一致
  8. 如果地址一致,则说明用户是此地址的持有者,此时就登录成功了,给前端返回鉴权 token

在生产环境下调用 signMessage 时你应该在 message 中包含一个一次性的 Nonce 进去,并在服务端检查 Nonce 是否被使用,从而避免重放攻击。
当你比较两个钱包地址是否相等或者将钱包地址保存在数据库之前,不要忘记将地址全部转为小写,避免大小写不一致导致意外情况。

使用 web3Modal 和 WalletConnect 兼容更多钱包

MetaMask 是目前用户最多的钱包软件,不过像 Coinbase Wallet、Trust Wallet、imToken、Zerion 也有不小的用户群体。总之目前的钱包软件非常的多。作为开发者我们不需要一个个去适配这些钱包。大部分钱包都实现了 WalletConnect 协议,我们只要兼容 WalletConnect 就可以变相的兼容大部分钱包。再配合 web3Modal 这个 NPM 包,简单的配置一下参数就能兼容市面上几乎所有的钱包。

接入 WalletConnect 必须要提供一个 infuraId,在 infura.io 免费创建一个项目即可拿到 infuraId,免费版本每天限制调用次数不超过10万次,超出需要付费,用户量大的话接入 WalletConnect 会多一些额外成本。如果用户选择用 MetaMask 连接就不消耗你的 infura 额度了。

import { useEffect } from 'react'
import { ethers } from 'ethers'
import Web3Modal, { CHAIN_DATA_LIST, ChainData } from 'web3modal'
import WalletConnect from '@walletconnect/web3-provider'
import CoinbaseWalletSDK from '@coinbase/wallet-sdk'
import { CoinbaseWalletSDKOptions } from '@coinbase/wallet-sdk/dist/CoinbaseWalletSDK'

// 只有 walletconnect 和 coinbase wallet 需要提供 infuraId
const infuraId = '替换为你自己的 infuraId'

const providerOptions = {
  walletconnect: {
    package: WalletConnect,
    options: {
      infuraId,
      rpc: {
        56: 'https://bscrpc.com', // Binance Smart Chain
        97: 'https://data-seed-prebsc-2-s3.binance.org:8545', // Binance Smart Chain Testnet
        137: 'https://rpc-mainnet.matic.network', // Polygon
        42161: 'https://arb1.arbitrum.io/rpc', // Arbitrum One
        421611: 'https://rinkeby.arbitrum.io/rpc', // Arbitrum Rinkeby
        43114: 'https://api.avax.network/ext/bc/C/rpc', // Avalanche C-Chain
      },
    },
  },
  // 这个是 Coinbase Wallet
  walletlink: {
    package: CoinbaseWalletSDK,
    options: {
      appName: '你的应用名称',
      appLogoUrl: '',
      infuraId,
    } as CoinbaseWalletSDKOptions,
  },
}

function App() {
  const signerRef = useRef<ethers.Signer | null>(null)
  const web3ModalRef = useRef<Web3Modal | null>(null)

  // 初始化 Web3Modal
  useEffect(() => {
    const web3Modal = new Web3Modal({
      network: '', // optional
      cacheProvider: true, // optional
      providerOptions, // required
    })
    web3ModalRef.current = web3Modal
    if (web3Modal.cachedProvider) {
      // 如果有上次使用的钱包,自动尝试连接钱包
      onConnectWallet()
    } else {
      // 如果没有上次使用的钱包,则让用户手动连接钱包
    }
  }, [])

  // 连接到钱包,拿到用户钱包地址和当前选择的网络
  async function connectWallet() {
    const web3Modal = web3ModalRef.current!
    if (!web3Modal) {
      // web3Modal 不存在
      return
    }

    const provider = await web3Modal.connect()
    await provider.enable() // 调用此方法后会唤起钱包弹框询问用户是否要连接到当前站点
    
    const web3Provider = new ethers.providers.Web3Provider(provider, 'any')
    const signer = web3Provider.getSigner()
    signerRef.current = signer

    const [publicAddress, chainId] = await Promise.all([signer.getAddress(), signer.getChainId()])

    return { publicAddress, chainId }
  }

  function onConnectWallet() {
    return connectWallet()
      .then((meta) => {
        if (meta) {
          console.log('拿到的钱包信息', meta)
        } else {
          // 没有获取到钱包信息
        }
      })
      .catch((err) => {
        localStorage.removeItem('walletconnect')
        web3ModalRef.current?.clearCachedProvider()
        console.error('连接钱包出错', err)
      })
  }

  return (
    <div>
      <button onClick={onConnectWallet}>连接到钱包</button>
    </div>
  )
}

总结

基于钱包登录的方案,用户的帐户完全由用户自己掌控,一个账户可以在多个平台使用。用户不用再担心自己的密码是否被网站泄露,帐户安全完全由自己掌控,且注册和登录过程变得异常简单。需要注意的是,自动化批量创建钱包帐户的成本非常低,必要时网站可以在登录时配合 reCAPTCHA 之类的服务过滤机器人用户,或者要求用户绑定 Gmail 之类的邮箱,以此降低垃圾账户的数量。

参考

本文原载于:baiyun.me

原文链接:https://baiyun.me/sign-in-with-ethereum