이 글의 소개는 EIP-4361 이더리움 로그인 규칙을 따릅니다.
SIWE(Sign-In with Ethereum)는 이더리움에서 사용자 신원을 확인하는 방법으로, 지갑이 거래를 시작하는 것과 유사하며 사용자가 지갑을 제어할 수 있음을 나타냅니다.
현재 인증 방법은 매우 간단합니다. 지갑 플러그인에 있는 정보만 서명하면 됩니다. 일반 지갑 플러그인에서는 이미 지원하고 있습니다.
이 글에서 고려한 서명 시나리오는 이더리움에 관한 것이며 Solana, SUI 등과 같은 다른 시나리오는 이 글의 범위를 벗어납니다.
귀하의 프로젝트에 SIWE가 필요합니까?
SIWE는 지갑 주소의 인증 문제를 해결하기 위한 것이므로 다음과 같은 요구 사항이 있는 경우 SWIE 사용을 고려할 수 있습니다.
- 귀하의 Dapp에는 자체 사용자 시스템이 있습니다.
- 쿼리할 정보는 사용자 개인정보 보호와 관련되어 있습니다.
하지만 귀하의 Dapp이 etherscan과 같은 애플리케이션과 같은 쿼리 기반 기능인 경우 SIWE가 없어도 괜찮습니다.
궁금한 점이 있을 수 있습니다. Dapp에서 지갑을 통해 연결하면 지갑의 소유권을 갖게 되는 것 아닌가요?
예, 하지만 완전히 옳지는 않습니다. 프론트엔드의 경우 지갑을 통해 접속한 후 신원을 명시하는 것이 사실이나, 백엔드 지원이 필요한 일부 인터페이스 호출의 경우 단순히 신원을 전달할 방법이 없습니다. 인터페이스 주소가 있으면 누구나 귀하의 신원을 "빌릴" 수 있습니다. 결국 주소는 공개 정보입니다.
SIWE 원칙 및 프로세스
SIWE의 프로세스는 지갑 연결 - 서명 - 신원 획득의 세 단계로 요약될 수 있습니다. 이 세 단계를 자세히 소개합니다.
지갑 연결
지갑 연결은 일반적인 WEB3 작업입니다. 지갑 플러그인을 통해 Dapp에서 지갑을 연결할 수 있습니다.
징후
SIWE에서 서명 단계에는 Nonce 값 획득, 지갑 서명 및 백엔드 서명 확인이 포함됩니다.
Nonce 값을 얻는 것은 ETH 트랜잭션의 Nonce 값 설계를 참조해야 하며 이를 얻으려면 백엔드 인터페이스를 호출해야 합니다. 요청을 받은 후 백엔드는 임의의 Nonce 값을 생성하고 이를 현재 주소와 연결하여 후속 서명을 준비합니다.
프런트 엔드는 Nonce 값을 획득한 후 서명 콘텐츠를 구성해야 합니다. SIWE가 디자인할 수 있는 서명 콘텐츠에는 획득한 Nonce 값, 도메인 이름, 체인 ID, 서명 콘텐츠 등이 포함됩니다. 우리는 일반적으로 에서 제공하는 서명 방법을 사용합니다. 콘텐츠 서명을 수행하는 지갑입니다.
서명이 생성된 후 최종적으로 서명이 백엔드로 전송됩니다.
getgetidentity
백엔드가 서명을 확인하고 전달한 후 해당 사용자 ID(JWT일 수 있음)를 반환합니다. 그런 다음 프런트엔드는 백엔드 요청을 보낼 때 해당 주소와 ID를 가져와 지갑의 소유권을 나타낼 수 있습니다.
연습해 보세요
개발자가 지갑 연결 및 SIWE에 빠르게 액세스할 수 있도록 지원하는 구성 요소와 라이브러리가 이미 많이 있습니다. 연습 목표는 Dapp이 사용자 신원 확인을 위해 JWT를 반환할 수 있도록 하는 것입니다.
본 DEMO는 SIWE의 기본 프로세스를 소개하는 용도로만 사용되며, 프로덕션 환경에서 사용 시 보안 문제가 발생할 수 있습니다.
미리 준비하세요
이 글에서는 nextjs를 사용하여 애플리케이션을 개발하므로 개발자는 nodejs 환경을 준비해야 합니다. nextjs를 사용하면 프론트엔드와 백엔드 프로젝트를 나누지 않고도 풀스택 프로젝트를 직접 개발할 수 있다는 장점이 있습니다.
종속성 설치
먼저 프로젝트 디렉터리에 다음 명령줄을 입력하여 nextjs를 설치합니다.
npx create-next-app@14
프롬프트에 따라 nextjs를 설치하면 다음 내용을 볼 수 있습니다.
프로젝트 디렉토리에 들어가면 nextjs 스캐폴딩이 우리를 위해 많은 작업을 수행했음을 알 수 있습니다. 프로젝트 디렉터리에서 프로젝트를 실행할 수 있습니다.
npm run dev
그런 다음 터미널 프롬프트에 따라 localhost: 3000
입력하면 기본 nextjs 프로젝트가 실행되는 것을 확인할 수 있습니다.
SIWE 관련 종속성 설치
이전 소개에 따르면 SIWE는 로그인 시스템에 의존해야 하므로 프로젝트를 지갑에 연결해야 합니다. 여기서는 다음과 같은 이유로 Ant Design Web3( https://web3.ant.design/ )을 사용합니다.
- 완전히 무료이며 현재 적극적으로 유지관리되고 있습니다.
- WEB3 구성 요소 라이브러리로서 추가적인 정신적 부담 없이 일반 구성 요소 라이브러리와 사용 경험이 유사합니다.
- 그리고 SIWE를 지원합니다.
터미널에 다음을 입력해야 합니다.
npm install antd @ant-design/web3 @ant-design/web3-wagmi wagmi viem @tanstack/react-query --save
와그미를 소개합니다
Ant Design Web3의 SIWE는 Wagmi 라이브러리를 사용하여 구현하므로 프로젝트에 관련 구성 요소를 도입해야 합니다. 전체 프로젝트에서 Wagmi가 제공하는 Hooks를 사용할 수 있도록 해당 Provider를 layout.tsx
에 도입합니다.
먼저 WagmiProvider의 구성을 정의합니다. 코드는 다음과 같습니다.
"use client"; import { getNonce, verifyMessage } from "@/app/api"; import { Mainnet, MetaMask, OkxWallet, TokenPocket, WagmiWeb3ConfigProvider, WalletConnect, } from "@ant-design/web3-wagmi"; import { QueryClient } from "@tanstack/react-query"; import React from "react"; import { createSiweMessage } from "viem/siwe"; import { http } from "wagmi"; import { JwtProvider } from "./JwtProvider"; const YOUR_WALLET_CONNECT_PROJECT_ID = "c07c0051c2055890eade3556618e38a6"; const queryClient = new QueryClient(); const WagmiProvider: React.FC = ({ children }) => { const [jwt, setJwt] = React.useState(null); return ( (await getNonce(address)).data, createMessage: (props) => { return createSiweMessage({ ...props, statement: "Ant Design Web3" }); }, verifyMessage: async (message, signature) => { const jwt = (await verifyMessage(message, signature)).data; setJwt(jwt); return !!jwt; }, }} chains={[Mainnet]} transports={{ [Mainnet.id]: http(), }} walletConnect={{ projectId: YOUR_WALLET_CONNECT_PROJECT_ID, }} wallets={[ MetaMask(), WalletConnect(), TokenPocket({ group: "Popular", }), OkxWallet(), ]} queryClient={queryClient} > {children} ); }; export default WagmiProvider;
Ant Design Web3에서 제공하는 Provider를 사용하고 SIWE의 일부 인터페이스를 정의했습니다. 구체적인 인터페이스의 구현은 나중에 소개하겠습니다.
추후에는 지갑 연결 버튼을 소개하여 프런트엔드에 연결 입구를 추가할 수 있도록 하겠습니다.
이 시점에서 이미 SIWE에 연결했더라도 단계는 매우 간단합니다.
그 후 지갑과 서명을 연결하기 위한 연결 버튼을 정의해야 합니다. 코드는 다음과 같습니다.
"use client"; import type { Account } from "@ant-design/web3"; import { ConnectButton, Connector } from "@ant-design/web3"; import { Flex, Space } from "antd"; import React from "react"; import { JwtProvider } from "./JwtProvider"; export default function App() { const jwt = React.useContext(JwtProvider); const renderSignBtnText = ( defaultDom: React.ReactNode, account?: Account ) => { const { address } = account ?? {}; const ellipsisAddress = address ? `${address.slice(0, 6)}...${address.slice(-6)}` : ""; return `Sign in as ${ellipsisAddress}`; }; return ( <>
{jwt}
); }
이러한 방식으로 우리는 가장 간단한 SIWE 로그인 프레임워크를 구현했습니다.
인터페이스 구현
위의 소개에 따르면 SIWE에는 백엔드가 사용자의 신원을 확인하는 데 도움이 되는 몇 가지 인터페이스가 필요합니다. 이제 간단하게 구현해 보겠습니다.
목하
Nonce의 목적은 지갑에서 생성된 서명 내용이 서명될 때마다 변경되도록 하여 서명의 신뢰성을 높이는 것입니다. 이 Nonce 생성은 검증의 정확성을 높이기 위해 사용자가 전달한 주소와 연결되어야 합니다.
Nonce의 구현은 매우 간단합니다. 먼저 문자와 숫자로 생성된 임의의 문자열을 생성한 다음 Nonce를 주소에 연결합니다.
import { randomBytes } from "crypto"; import { addressMap } from "../cache"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const address = searchParams.get("address"); if (!address) { throw new Error("Invalid address"); } const nonce = randomBytes(16).toString("hex"); addressMap.set(address, nonce); return Response.json({ data: nonce, }); }
로그인메시지
signMessage의 기능은 콘텐츠에 서명하는 것입니다. 이 부분은 일반적으로 지갑 플러그인을 통해 완료됩니다. 일반적으로 구성할 필요는 없으며 이 데모에서는 Wagmi의 서명 방법만 지정하면 됩니다. 사용된.
확인 메시지
사용자가 콘텐츠에 서명한 후 서명 전 콘텐츠와 서명을 백엔드로 전송하여 확인해야 합니다. 백엔드는 비교를 위해 서명에서 해당 콘텐츠를 구문 분석하여 일치하면 확인을 통과합니다.
또한 서명된 콘텐츠의 Nonce 값이 사용자에게 보내는 Nonce 값과 일치하는지 여부 등 서명된 콘텐츠에 대해 일부 보안 확인을 수행해야 합니다. 검증이 통과된 후 후속 권한 검증을 위해 해당 사용자 JWT를 반환해야 합니다. 샘플 코드는 다음과 같습니다.
import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; import jwt from "jsonwebtoken"; import { parseSiweMessage } from "viem/siwe"; import { addressMap } from "../cache"; const JWT_SECRET = "your-secret-key"; // 请使用更安全的密钥,并添加对应的过期校验等const publicClient = createPublicClient({ chain: mainnet, transport: http(), }); export async function POST(request: Request) { const { signature, message } = await request.json(); const { nonce, address = "0x" } = parseSiweMessage(message); console.log("nonce", nonce, address, addressMap); // 校验nonce 值是否一致if (!nonce || nonce !== addressMap.get(address)) { throw new Error("Invalid nonce"); } // 校验签名内容const valid = await publicClient.verifySiweMessage({ message, address, signature, }); if (!valid) { throw new Error("Invalid signature"); } // 生成jwt 并返回const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }); return Response.json({ data: token, }); }
현재 SIWE 로그인을 기본적으로 구현하는 Dapp이 개발되었습니다.
일부 최적화 항목
이제 SIWE에 로그인할 때 기본 RPC 노드를 사용하면 확인 프로세스가 거의 30초가 걸리므로 인터페이스의 응답 시간을 향상시키기 위해 전용 노드 서비스를 사용하는 것이 좋습니다 . 이 문서에서는 ZAN의 노드 서비스( https://zan.top/home/node-service?chInfo=ch_WZ )를 사용합니다. ZAN 노드 서비스 콘솔로 이동하여 해당 RPC 연결을 얻을 수 있습니다.
Ethereum 메인 네트워크에 대한 HTTPS RPC 연결을 얻은 후 코드에서 publicClient
의 기본 RPC를 바꿉니다.
const publicClient = createPublicClient({ chain: mainnet, transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //获取到的ZAN 节点服务RPC });
교체 후 검증 시간이 크게 줄어들고 인터페이스 속도가 크게 빨라집니다.