GM(Good Morning) SIWE(Gonna Make It)는 이더리움(ETH) 상에서 사용자 신원을 검증하는 방식으로, 지갑에서 거래를 발생시키는 것과 유사한 방식으로 사용자가 해당 지갑을 제어하고 있음을 증명합니다.
현재 신원 인증 방식은 매우 간단해졌으며, 지갑 플러그인에서 정보에 서명하는 것만으로 가능합니다. 대부분의 지갑 플러그인이 이를 지원하고 있습니다.
본 문서에서는 이더리움(ETH) 상에서의 서명 시나리오를 다루며, 솔라나(SOL), 수이(SUI) 등 다른 블록체인은 다루지 않습니다.
SIWE가 필요한 프로젝트
SIWE는 지갑 주소의 신원 인증 문제를 해결하기 위한 것이므로, 다음과 같은 요구사항이 있다면 SIWE를 고려해볼 수 있습니다:
- Dapp에 자체 사용자 시스템이 있는 경우;
- 사용자 프라이버시와 관련된 정보를 조회해야 하는 경우;
그러나 etherscan과 같이 주로 조회 기능을 제공하는 Dapp의 경우에는 SIWE가 필요하지 않습니다.
Dapp에서 지갑을 연결하면 지갑의 소유권을 나타내는 것 같지만, 그렇지 않습니다. 프론트엔드에서는 지갑 연결 작업을 통해 자신의 신원을 나타낼 수 있지만, 백엔드 API 호출 시에는 자신의 신원을 나타낼 수 없습니다. 단순히 주소를 전달하는 것으로는 누구나 "차용"할 수 있기 때문입니다.
SIWE의 원리와 프로세스
SIWE의 프로세스는 3단계로 요약할 수 있습니다: 지갑 연결 - 서명 - 신원 식별자 획득. 각 단계에 대해 자세히 살펴보겠습니다.
지갑 연결
지갑 연결은 일반적인 WEB3 작업으로, 지갑 플러그인을 통해 Dapp에서 사용자의 지갑을 연결할 수 있습니다.
서명
SIWE에서 서명 단계에는 Nonce 값 획득, 지갑 서명, 백엔드 서명 검증이 포함됩니다.
Nonce 값은 이더리움(ETH) 거래의 Nonce와 유사한 개념으로, 백엔드에서 생성되며 현재 주소와 연결됩니다. 이는 향후 서명 검증을 위해 사용됩니다.
프론트엔드에서 Nonce 값을 받은 후에는 서명 내용을 구성합니다. SIWE에서는 Nonce 값, 도메인, 체인 ID, 서명 내용 등을 포함할 수 있으며, 일반적으로 지갑에서 제공하는 서명 메서드를 사용하여 서명을 생성합니다.
서명이 완성되면 최종적으로 백엔드에 전송합니다.
신원 식별자 획득
백엔드에서 서명을 검증하고 통과하면 해당 사용자의 신원 식별자(예: JWT)를 반환합니다. 이후 프론트엔드에서 백엔드 요청 시 주소와 신원 식별자를 함께 전송하면 지갑 소유권을 나타낼 수 있습니다.
실습해보기
현재 많은 구성 요소와 라이브러리가 개발자들의 빠른 지갑 연결 및 SIWE 도입을 지원하고 있습니다. 실제로 구현해보면 Dapp에서 JWT를 반환하여 사용자 신원 검증을 할 수 있습니다.
이 데모는 SIWE의 기본 프로세스를 소개하는 것이며, 실제 운영 환경에서는 보안 문제가 발생할 수 있습니다.
사전 준비
본 문서에서는 nextjs를 사용하여 애플리케이션을 개발하므로, nodejs 환경이 필요합니다. nextjs를 사용하면 프론트엔드와 백엔드를 하나의 프로젝트에서 개발할 수 있는 장점이 있습니다.
의존성 설치
먼저 nextjs를 설치합니다. 프로젝트 디렉토리에서 다음 명령어를 실행하세요:
npx create-next-app@14
안내에 따라 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
Wagmi 도입
Ant Design Web3의 SIWE는 Wagmi 라이브러리를 기반으로 구현되므로, 프로젝트에 관련 구성 요소를 도입해야 합니다. layout.tsx
에서 Wagmi Provider를 추가하면 전체 프로젝트에서 Wagmi Hooks를 사용할 수 있습니다.
먼저 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는 사용자가 전달한 address와 연관되어 생성되어야 정확한 검증이 가능합니다.
Nonce 구현은 매우 간단합니다. 먼저 문자와 숫자로 이루어진 랜덤 문자열을 생성하고, 이를 address와 연결하면 됩니다. 코드는 다음과 같습니다:
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,});}
메시지 서명
메시지 서명의 역할은 내용에 서명하는 것이며, 이 기능은 일반적으로 지갑 플러그인을 통해 수행됩니다. 우리는 일반적으로 구성을 할 필요가 없으며, 메서드만 지정하면 됩니다. 이 데모에서는 Wagmi의 서명 메서드를 사용하고 있습니다.
메시지 확인
사용자가 내용에 서명한 후에는 서명된 내용과 서명을 함께 백엔드에 보내 확인해야 합니다. 백엔드에서는 서명에서 해당 내용을 추출하여 비교하며, 일치하면 인증이 완료된 것입니다.
또한 서명 내용에 대해 추가적인 보안 검사를 수행해야 합니다. 예를 들어 서명 내용의 논스 값이 우리가 사용자에게 발급한 것과 일치하는지 등을 확인해야 합니다. 인증이 완료되면 사용자의 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);// 논스 값 확인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 연결 정보를 얻을 수 있습니다.
이더리움 메인넷의 HTTPS RPC 연결을 얻은 후, 코드에서 publicClient
의 기본 RPC를 다음과 같이 대체할 수 있습니다:
const publicClient = createPublicClient({chain: mainnet,transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //ZAN 노드 서비스 RPC});
이렇게 대체하면 인증 시간을 크게 단축할 수 있으며, 인터페이스 속도가 크게 향상됩니다.