SIWE: вход на сайт с Ethereum-кошельком
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 2 из 30.
SIWE задаёт узнаваемый формат сообщения входа Ethereum-аккаунта на веб-сайт. Он соединяет домен, адрес, назначение, сеть и одноразовый nonce. В этом уроке подготовим сообщение на серверной стороне и сохраним его точные байты. Результат — кошелёк подписывает конкретный challenge, который сервер сможет сопоставить с собственной записью.
Предполагается модель входа предыдущего урока. Учебный сервер использует Node22.14.0 и viem2.37.9; здесь не запускается HTTP-приложение, кошелёк или проверка подписи. Домен library.example является учебным обозначением, не адресом production-сервиса ProfessorWeb. Его значения позже заменяются только контролируемой конфигурацией приложения.
Сообщение относится к конкретному сайту
Домен отвечает на вопрос, кто предлагает вход. URI определяет ресурс назначения. Chain ID связывает вход с сетью, особенно когда адрес является контрактным аккаунтом. Версия формата SIWE равна1. Nonce является случайной серверной строкой, а время выдачи и истечения ограничивают пригодность сообщения.
Не следует получать домен только из произвольного заголовка Host без настройки доверенных proxy и допустимых origin. Злоумышленник мог бы сформировать вход на неожиданное имя и заставить сервер принять его как собственный. Для локальной модели выбираем фиксированную конфигурацию AUTH_DOMAIN и AUTH_URI; настоящая инфраструктура отдельно определяет допустимые варианты.
Человекочитаемое statement в стандартном формате имеет ограничения, включая отсутствие перевода строки. Для учебного примера используем короткое ASCII-описание назначения. Русский интерфейс перед окном кошелька объясняет тот же смысл, но не меняет подписываемые байты самовольно. Точное сообщение остаётся частью серверного challenge.
Построение challenge
Создадим make-login-challenge.js. Функция получает уже проверенный адрес аккаунта. Хранилище challenge вводится следующим уроком; пока выход представляет полную запись для него.
import { randomBytes } from 'node:crypto';
import { getAddress } from 'viem';
import { createSiweMessage } from 'viem/siwe';
export function makeLoginChallenge(address, now = new Date()) {
const nonce = randomBytes(16).toString('hex');
const expiresAt = new Date(now.getTime() + 5 * 60_000);
const message = createSiweMessage({
address: getAddress(address),
domain: 'library.example',
uri: 'https://library.example/account/login',
chainId: 31337,
version: '1',
statement: 'Sign in to the Library account.',
nonce,
issuedAt: now,
expirationTime: expiresAt,
});
return { nonce, address: getAddress(address), message,
issuedAt: now.toISOString(), expiresAt: expiresAt.toISOString() };
}
Случайные16 байтов превращаются в32 шестнадцатеричных символа, которые соответствуют алфавитно-цифровой форме nonce. Это одна выбранная учебная политика генерации, а не требование использовать именно такую длину в каждом проекте. Главное — достаточная непредсказуемость и одноразовое серверное назначение.
Пять минут являются сроком нашего примера. Он не рекомендуется автоматически всем сайтам: удобство, риск повторного использования и поведение пользователей различаются. Сервер проверяет собственную запись и время, а не доверяет сроку, который клиент прислал в изменённом сообщении. Изменение любой строки требует нового challenge.
Точные байты
Браузер получает message и передаёт его в walletClient.signMessage как строку без повторного форматирования. Не добавляет приветствие, не переводит statement и не переставляет строки. Подписывается конкретное содержание, поэтому «тот же смысл» после изменения пробелов не является тем же сообщением.
Сервер может сохранить сообщение целиком и при проверке потребовать совпадение с выданным. Дополнительно сообщение разбирается по стандартной схеме и проверяется относительно ожидаемых полей. Сопоставление точных байтов удобно для связывания challenge, но не отменяет проверку назначения, срока и подписи.
Если человек меняет аккаунт до подписи, создаётся новое сообщение для нового адреса. Нельзя заменить только поле address в сохранённом тексте и оставить прежнюю запись challenge. Сервер должен знать, для какого адреса и потока входа сформирован каждый экземпляр. Старый challenge остаётся неиспользованным до истечения или явного отзыва.
Связь с браузерным потоком
Запрос challenge связывается с конкретной попыткой входа, например предварительным серверным идентификатором в cookie. Такая связь помогает не выдать сессию браузеру, который просто принёс чужую подпись. Детали cookie и защиты origin рассматриваются отдельно; формат SIWE сам по себе не заменяет защиту HTTP-маршрута.
Сервер ограничивает размер входного адреса и частоту выдачи challenge. Иначе endpoint может стать источником неограниченных записей и нагрузить хранилище. Истёкшие записи удаляются по отдельной политике. Очистка не делает неиспользованное старое сообщение пригодным снова: оно просто больше не принимается.
Подпись не требует транзакции
Вход использует подпись сообщения, а не запись publish. Он не меняет состояние CourseCatalog и не требует сетевой комиссии как обычная транзакция. Кошелёк всё равно должен показать запрос и получить согласие. Отсутствие газа не означает отсутствие значимости: сообщение предоставляет доступ к кабинету и требует понятного назначения.
Подпись нельзя включать в URL страницы. URL попадает в историю, журналы сервера и другие места. Сервер получает её через защищённый маршрут с ограниченной обработкой, а диагностика не записывает полное значение. Nonce тоже не должен превращаться в вечный публичный токен доступа.
Изменение назначения
Если сайт позже разрешит сохранить прогресс или подтвердить отдельную редакционную операцию, это не означает, что прежняя подпись входа автоматически разрешает оба действия. Серверная сессия имеет собственную политику, а подписанное разрешение действия — отдельный формат. Стандартный statement не используется как скрытый договор неограниченных полномочий.
Для упражнения измените в сообщении домен, адрес, сеть и срок по одному полю. Объясните, почему сервер обязан сопоставить каждое изменение с собственной ожидаемой записью, а не проверять только криптографическую подпись. Ожидаемый итог — сообщение является проверяемым предложением входа, а не произвольной строкой. Формат приведён в ERC-4361, построение — в createSiweMessage.
Выдача и отображение
Браузер показывает точное сообщение перед запросом и отдельно объясняет его назначение по-русски. Человек не должен подписывать непрозрачный набор байтов только потому, что сайт назвал кнопку «Войти». Если расширение отображает формат иначе, сайт сохраняет собственный предварительный просмотр и не меняет данные ради удобного внешнего вида.
При повторной выдаче challenge прежняя попытка либо остаётся пригодной до своего срока, либо явно отзывается по политике потока. Сервер не должен случайно хранить несколько одинаково активных nonce без понимания, какая подпись ожидается в текущем браузере. Новая запись имеет собственный идентификатор, а поздний ответ старой страницы не подменяет текущий маршрут входа.
Точное сообщение в разных интерфейсах
Если кошелёк переносит длинную строку на экране, это изменение отображения, а не повод менять подписываемые байты. Сервер хранит исходное сообщение целиком. Повторная сборка из отдельных полей после получения подписи может дать другой текст, даже когда видимые значения кажутся одинаковыми.