Podpisywanie wiadomości i uwierzytelnianie użytkowników

Podpisanie wiadomości dowodzi, że użytkownik kontroluje portfel – bez transakcji, bez opłaty i bez wywołań sieciowych. Aplikacje używają podpisanych wiadomości do logowania użytkowników, zapisywania faktu zaakceptowania regulaminu, powiązania portfela z istniejącym kontem lub autoryzowania działania wykonywanego off-chain, np. złożenia zamówienia.

Portfele Solana podpisują wiadomości na dwa sposoby:

  • Wiadomości off-chain (solana:signOffchainMessage) opakowują Twój tekst w standardowy format, zdefiniowany w specyfikacji podpisywania wiadomości off-chain (OCMS). Format ten nigdy nie może zostać pomylony z transakcją, wskazuje konta, które muszą go podpisać, i jest formatem podpisywanym przez portfele sprzętowe.
  • Surowe wiadomości (solana:signMessage) podpisują dokładnie te bajty, które przekaże aplikacja, bez prefiksu i bez zadeklarowanych podpisujących. Obsługuje je większość portfeli programowych, ale nic w tych bajtach nie wskazuje, że to wiadomość, a nie transakcja.

Używaj wiadomości off-chain tam, gdzie portfel je obsługuje, a w pozostałych przypadkach wracaj do surowych wiadomości. W dalszej części przewodnika jako przykład posłuży logowanie, ale te same kroki podpisywania i weryfikacji dotyczą każdej wiadomości.

Wybierz format wiadomości

Wiadomość off-chainSurowa wiadomośćTransakcja
Funkcja portfelasignOffchainMessagesignMessagesignTransaction
Obsługa portfeli sprzętowychZależy od urządzeniaNieTak
Czytelna dla użytkownikaTakTylko w portfelach programowychZależy od urządzenia
Deklaruje wymaganych podpisującychTakNieTak
Wywołania RPC i opłatyBrakBrakBlockhash, wysyłka, opłata, potwierdzenie

Dlaczego nie transakcja?

Portfele sprzętowe kiedyś nie miały możliwości podpisania czytelnej wiadomości. Wiele aplikacji prosiło więc takich użytkowników o podpisanie transakcji, która niczego nie robi (no-op). Działa to, ale zwiększa opóźnienia, koszty i ryzyko:

  • Opłaty sieciowe. Transakcja wiąże się z opłatą, nawet jeśli nic nie robi.
  • Wiele wywołań RPC. Aplikacja pobiera blockhash, symuluje, wysyła, a następnie odpytuje o potwierdzenie.
  • Dodatkowe opóźnienie. Każde z tych wywołań to osobna runda komunikacji, zanim użytkownik zostanie zalogowany.
  • Nic czytelnego. Użytkownik zatwierdza bajty transakcji, a nie oświadczenie o tym, na co się zgadza.
  • Ryzyko behawioralne. Proszenie użytkowników o podpisanie transakcji tylko po to, by się zalogować, ułatwia nakłonienie ich do podpisania szkodliwej.

Wiadomość off-chain pomija to wszystko. Jedyna runda komunikacji odbywa się z Twoim własnym serwerem.

A sign-in transaction fetches a blockhash, signs, simulates, sends, and polls for confirmation before the server can verify it. An off-chain message goes from a server challenge to a wallet signature to a server-side check, with no RPC calls and no fee.

Co podpisuje portfel

Wiadomość off-chain zaczyna się od stałej domeny podpisu, \xffsolana offchain. Żadna prawidłowa transakcja nie zaczyna się od tych bajtów, więc podpisana wiadomość nigdy nie może zostać użyta jako transakcja. Wersja 1 formatu zawiera następnie listę wymaganych podpisujących i przenosi wiadomość jako tekst UTF-8, bez limitu długości.

Portfel buduje te bajty, podpisuje je i zwraca wraz z podpisem. Specyfikacja opisuje pełny format, w tym starszą wersję 0.

A version 1 off-chain message is a 16-byte signing domain, a version byte, a signer count, each signer's 32-byte public key in sorted order, and the UTF-8 content. The envelope prefixes it with a signature count and one 64-byte signature per signer.

Wybierz metodę logowania

Nie każdy portfel obsługuje już wiadomości off-chain. Przed poproszeniem o podpis sprawdź funkcje połączonego konta, a gdy brakuje tej funkcji, użyj rozwiązania zastępczego:

  1. Jeśli konto obsługuje solana:signOffchainMessage z wiadomością w wersji 1, podpisz wiadomość off-chain.
  2. W przeciwnym razie podpisz to samo wyzwanie za pomocą solana:signMessage. Obsługuje je większość portfeli programowych.
  3. Użytkownikom portfeli sprzętowych bez obsługi wiadomości daj jawną opcję włączenia, np. przełącznik „Używam portfela sprzętowego”, który zamiast tego podpisuje transakcję logowania. Nie kieruj ich na tę ścieżkę domyślnie.
When a wallet connects, check whether its account supports solana:signOffchainMessage version 1. If it does, sign an off-chain message. If not, sign a raw message with solana:signMessage, and let hardware wallet users opt into signing a sign-in transaction.

Podpisywanie wiadomości w przeglądarce

Zainstaluj pakiety Wallet Standard razem z Kit:

Terminal
$
npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry

Te funkcje pomocnicze przyjmują UiWalletAccount, takie jak account zwracane przez useConnectedWallet w przewodniku po React:

offchain-message.ts
import {
SolanaSignOffchainMessage,
type SolanaSignOffchainMessageFeature
} from "@solana/wallet-standard-features";
import type { UiWalletAccount } from "@wallet-standard/ui";
import { getWalletAccountFeature } from "@wallet-standard/ui-features";
import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";
type OffchainMessageFeature =
SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];
export function supportsOffchainMessages(account: UiWalletAccount) {
if (!account.features.includes(SolanaSignOffchainMessage)) return false;
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
return feature.supportedMessageVersions.includes(1);
}
export async function signOffchainMessage(
account: UiWalletAccount,
message: string
) {
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
const [output] = await feature.signOffchainMessage({
messageVersion: 1,
account: getWalletAccountForUiWalletAccount(account),
message,
requiredSigners: [account.publicKey]
});
return output;
}

signOffchainMessage zwraca signedOffchainMessage – pełne bajty podpisane przez portfel – oraz signature. Zakoduj oba w Base64 i wyślij na serwer razem z adresem portfela.

Napisz wyzwanie tak, aby dobrze wyglądało na małym ekranie. Podaj nazwę aplikacji i domenę, wyjaśnij, do czego służy podpis, oraz dołącz jednorazowy nonce i czas wystawienia z Twojego serwera:

swap.app wants you to sign in with your Solana account.
Signing in costs nothing and does not submit a transaction.
Nonce: 8f3a1c9e
Issued at: 2026-09-29T17:04:00Z

Weryfikacja podpisu na serwerze

Weryfikacja to lokalne sprawdzenie podpisu, które nie wymaga połączenia RPC. Zdekoduj bajty podpisane przez portfel, potwierdź, że odpowiadają wystawionemu przez Ciebie wyzwaniu, a następnie sprawdź podpis:

verify-sign-in.ts
import {
address,
assertOffchainMessageV1Equal,
getBase64Encoder,
getOffchainMessageDecoder,
verifyOffchainMessageEnvelope,
type OffchainMessageBytes,
type SignatureBytes
} from "@solana/kit";
export async function verifySignIn(input: {
walletAddress: string;
expectedMessage: string;
signedOffchainMessage: string;
signature: string;
}) {
const signer = address(input.walletAddress);
const base64 = getBase64Encoder();
const content = base64.encode(
input.signedOffchainMessage
) as OffchainMessageBytes;
const signature = base64.encode(input.signature) as SignatureBytes;
const received = getOffchainMessageDecoder().decode(content);
if (received.version !== 1) {
throw new Error(`Expected a v1 message, got v${received.version}`);
}
assertOffchainMessageV1Equal(received, {
version: 1,
requiredSignatories: [{ address: signer }],
content: input.expectedMessage
});
await verifyOffchainMessageEnvelope({
content,
signatures: { [signer]: signature }
});
}

assertOffchainMessageV1Equal zgłasza błąd, jeśli treść lub podpisujący różnią się od oczekiwanych. verifyOffchainMessageEnvelope zgłasza błąd, jeśli podpisu brakuje lub jest nieprawidłowy. Wyszukuj expectedMessage według nonce na serwerze, zamiast ufać kopii odesłanej przez klienta. Zużyj nonce po pomyślnej weryfikacji, aby ten sam podpis nie mógł posłużyć do zalogowania dwa razy.

Weryfikacja surowej wiadomości

Podpis signMessage obejmuje bezpośrednio bajty wyzwania, więc klient musi wysłać tylko podpis i adres portfela. Sprawdź go względem wystawionego wyzwania:

verify-raw-sign-in.ts
import {
address,
getBase64Encoder,
getPublicKeyFromAddress,
getUtf8Encoder,
signatureBytes,
verifySignature
} from "@solana/kit";
export async function verifyRawSignIn(input: {
walletAddress: string;
expectedMessage: string;
signature: string;
}) {
const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));
const signature = signatureBytes(getBase64Encoder().encode(input.signature));
const message = getUtf8Encoder().encode(input.expectedMessage);
if (!(await verifySignature(publicKey, signature, message))) {
throw new Error("Invalid signature");
}
}

Obowiązują te same zasady dotyczące nonce: wyszukaj expectedMessage według nonce i zużyj go po pomyślnej weryfikacji.

@solana/kit reeksportuje te funkcje pomocnicze z @solana/offchain-messages. Użyj samodzielnego pakietu, jeśli Twój serwer nie zależy od Kit.

Sign In With Solana

Sign In With Solana (SIWS) standaryzuje samo wyzwanie. Aplikacja przekazuje ustrukturyzowane pola, takie jak domain, statement, nonce i issuedAt, do funkcji solana:signIn portfela. Portfel formatuje je w standardową wiadomość, w razie potrzeby nawiązuje połączenie i podpisuje ją w jednym kroku.

Wersja 1.1.0 funkcji solana:signIn dodaje opcję useOffchainMessage. Gdy przekażesz useOffchainMessage: { messageVersion: 1 }, portfel opakowuje tekst SIWS w wiadomość off-chain w wersji 1, w której jedynym wymaganym podpisującym jest konto podpisujące. Portfele sprzętowe mogą ją wyświetlić i podpisać, a wynik zawiera signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Portfele deklarujące wersję 1.0.0 nie znają tej opcji, więc najpierw sprawdź wersję funkcji, a w przeciwnym razie pomiń opcję.

solana:signIn jest funkcją portfela, a nie konta, dlatego te funkcje pomocnicze przyjmują UiWallet:

sign-in.ts
import {
SolanaSignIn,
type SolanaSignInFeature,
type SolanaSignInInput
} from "@solana/wallet-standard-features";
import type { UiWallet } from "@wallet-standard/ui";
import { getWalletFeature } from "@wallet-standard/ui-features";
type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];
export function supportsOffchainSignIn(wallet: UiWallet) {
if (!wallet.features.includes(SolanaSignIn)) return false;
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
return feature.version === "1.1.0";
}
export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
const [output] = await feature.signIn({
...input,
...(supportsOffchainSignIn(wallet) && {
useOffchainMessage: { messageVersion: 1 }
})
});
return output;
}

Zbuduj dane wejściowe na serwerze, z jednorazowym nonce i czasem issuedAt, i zachowaj kopię indeksowaną według nonce. Zużyj nonce po pomyślnej weryfikacji logowania.

Bez Wallet Adapter funkcja verifySignIn z @solana/wallet-standard-util sprawdza wyłącznie zwykłe dane wyjściowe SIWS. Gdy signedMessageFormat wskazuje wiadomość off-chain, signedMessage jest wiadomością off-chain, której treścią jest tekst SIWS. Odbuduj oczekiwany tekst z zapisanych danych wejściowych i adresu podpisującego za pomocą createSignInMessageText z @solana/wallet-standard-util, a następnie zweryfikuj go jak każdą inną wiadomość off-chain, tak jak w sekcji Weryfikacja podpisu na serwerze. README @solana/offchain-messages opisuje ten przepływ w Kit.

Dla portfeli

Portfele dodają obsługę, implementując funkcję Wallet Standard solana:signOffchainMessage z @solana/wallet-standard-features:

  • Zgłoś version: "1.0.0" i wymień w supportedMessageVersions wersje wiadomości, które potrafisz podpisać. Obecnie jest to [1].
  • Zbuduj preambułę wersji 1 na podstawie requiredSigners, które sortujesz i sprawdzasz pod kątem duplikatów. Odrzuć żądanie, jeśli konto nie należy do wymaganych sygnatariuszy.
  • Przed podpisaniem wyświetl użytkownikowi pełny tekst wiadomości, również na urządzeniach sprzętowych.
  • Zwróć dokładne bajty, które podpisałeś, jako signedOffchainMessage, wraz z podpisem Ed25519 w polu signature.
  • Aby obsługiwać logowanie Sign In With Solana poza łańcuchem (off-chain), zgłaszaj version: "1.1.0" dla solana:signIn. Gdy dane wejściowe zawierają useOffchainMessage, podpisz tekst SIWS jako wiadomość off-chain w wersji 1, zwróć jej bajty jako signedMessage i ustaw signedMessageFormat.
  • Nadal obsługuj solana:signMessage. Ta metoda pozostaje bez zmian, a aplikacje wciąż używają jej jako rozwiązania zapasowego.

Is this page helpful?

Spis treści

Edytuj stronę