使用 SIWE 實現 Ethereum 登入教學|React + Go 實作範例解析
本文教你如何使用 Sign-In with Ethereum(SIWE) 實作去中心化登入機制,包含前端 React 範例與後端 Go 驗證流程。透過 nonce、signature、JWT token 完成 Web3 登入驗證,適合想整合以太坊錢包登入功能的開發者。
CChia1104
筆記4 分鐘閱讀
siwe Ethereum 登入
前端先去跟後端拿一個隨機數(nonce)

前端再用這個 nonce 去取得簽名(signature)跟訊息(message)

message 格式
example.com wants you to sign in with your Ethereum account:
12345678
wallet_sign_statement
URI: https://example.com
Version: 1
Chain ID: 1
Nonce: 22ppxlq5
Issued At: 2025-02-19T03:57:48.796Z
Expiration Time: 2025-03-21T03:57:48.796ZReact 範例
import * as React from 'react'
import { useAccount, useNetwork, useSignMessage } from 'wagmi'
import { SiweMessage } from 'siwe'
function SignInButton({
onSuccess,
onError,
}: {
onSuccess: (args: { address: string }) => void
onError: (args: { error: Error }) => void
}) {
const [state, setState] = React.useState<{
loading?: boolean
nonce?: string
}>({})
const fetchNonce = async () => {
try {
const nonceRes = await fetch('/api/nonce')
const nonce = await nonceRes.text()
setState((x) => ({ ...x, nonce }))
} catch (error) {
setState((x) => ({ ...x, error: error as Error }))
}
}
// Pre-fetch random nonce when button is rendered
// to ensure deep linking works for WalletConnect
// users on iOS when signing the SIWE message
React.useEffect(() => {
fetchNonce()
}, [])
const { address } = useAccount()
const { chain } = useNetwork()
const { signMessageAsync } = useSignMessage()
const signIn = async () => {
try {
const chainId = chain?.id
if (!address || !chainId) return
setState((x) => ({ ...x, loading: true }))
// Create SIWE message with pre-fetched nonce and sign with wallet
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in with Ethereum to the app.',
uri: window.location.origin,
version: '1',
chainId,
nonce: state.nonce,
})
const signature = await signMessageAsync({
message: message.prepareMessage(),
})
// Verify signature
const verifyRes = await fetch('/api/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, signature }),
})
if (!verifyRes.ok) throw new Error('Error verifying message')
setState((x) => ({ ...x, loading: false }))
onSuccess({ address })
} catch (error) {
setState((x) => ({ ...x, loading: false, nonce: undefined }))
onError({ error: error as Error })
fetchNonce()
}
}
return (
<button disabled={!state.nonce || state.loading} onClick={signIn}>
Sign-In with Ethereum
</button>
)
}
export function Profile() {
const { isConnected } = useAccount()
const [state, setState] = React.useState<{
address?: string
error?: Error
loading?: boolean
}>({})
// Fetch user when:
React.useEffect(() => {
const handler = async () => {
try {
const res = await fetch('/api/me')
const json = await res.json()
setState((x) => ({ ...x, address: json.address }))
} catch (_error) {}
}
// 1. page loads
handler()
// 2. window is focused (in case user logs out of another window)
window.addEventListener('focus', handler)
return () => window.removeEventListener('focus', handler)
}, [])
if (isConnected) {
return (
<div>
{/* Account content goes here */}
{state.address ? (
<div>
<div>Signed in as {state.address}</div>
<button
onClick={async () => {
await fetch('/api/logout')
setState({})
}}
>
Sign Out
</button>
</div>
) : (
<SignInButton
onSuccess={({ address }) => setState((x) => ({ ...x, address }))}
onError={({ error }) => setState((x) => ({ ...x, error }))}
/>
)}
</div>
)
}
return <div>{/* Connect wallet content goes here */}</div>
}後端
後端需要透過這兩個資訊去解,判斷 nonce 是否一樣,並回傳 token

// This is a simple example of how to implement a web3 login endpoint using the siwe-go library.
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/golang-jwt/jwt/v5"
"github.com/spruceid/siwe-go"
)
const port = ":8080"
func main() {
http.HandleFunc("/api/v1/auth/web3:login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
err := Web3Login(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
err := http.ListenAndServe(port, nil)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
// verifyWalletSignature verifies the signature of a wallet address
func verifyWalletSignature(messageStr string, sig string) (jwt.MapClaims, error) {
message, err := siwe.ParseMessage(messageStr)
if err != nil {
err = fmt.Errorf("parse message err: %v", err)
return nil, err
}
verify, err := message.ValidNow()
if err != nil {
err = fmt.Errorf("verify message err: %v", err)
return nil, err
}
if !verify {
err = fmt.Errorf("verify message fail: %v", err)
return nil, err
}
publicKey, err := message.VerifyEIP191(sig)
if err != nil {
err = fmt.Errorf("verifyEIP191 err: %v", err)
return nil, err
}
pubBytes := crypto.FromECDSAPub(publicKey)
publicKeyString := hexutil.Encode(pubBytes)
// Return the verified claims
claims := jwt.MapClaims{
"web3_pub_key": publicKeyString,
}
return claims, nil
}
// LoginRequest represents the login request body
type LoginRequest struct {
Message string `json:"message"`
Signature string `json:"signature"`
}
func Web3Login(w http.ResponseWriter, r *http.Request) error {
// Parse the login request body
req := new(LoginRequest)
// 解析 JSON 請求體
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(req)
if err != nil {
return errors.New("invalid request body")
}
// Verify the signature
claims, err := verifyWalletSignature(req.Message, req.Signature)
if err != nil {
return errors.New(err.Error())
}
// Generate a JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return errors.New("error creating JWT token")
}
// Set the JWT token in the response header
http.SetCookie(w, &http.Cookie{
Name: "jwt_token",
Value: tokenString,
Expires: time.Now().Add(time.Hour * 24),
})
// Return a success response
w.WriteHeader(http.StatusOK)
return nil
}Written by: Chia1104 CC BY-NC-SA 4.0