Υπογραφή μηνυμάτων & έλεγχος ταυτότητας χρηστών

Η υπογραφή ενός μηνύματος αποδεικνύει ότι ένας χρήστης ελέγχει ένα πορτοφόλι, χωρίς συναλλαγή, χωρίς κόστος και χωρίς κλήση δικτύου. Οι εφαρμογές χρησιμοποιούν υπογεγραμμένα μηνύματα για να συνδέσουν χρήστες, να καταγράψουν ότι ένας χρήστης αποδέχτηκε όρους, να συνδέσουν ένα πορτοφόλι με έναν υπάρχοντα λογαριασμό ή να εξουσιοδοτήσουν μια ενέργεια που γίνεται εκτός αλυσίδας, όπως η υποβολή μιας παραγγελίας.

Τα πορτοφόλια Solana υπογράφουν μηνύματα με δύο τρόπους:

  • Μηνύματα εκτός αλυσίδας (solana:signOffchainMessage): περιτυλίγουν το κείμενό σας σε μια τυπική μορφή, η οποία ορίζεται από την προδιαγραφή υπογραφής μηνυμάτων εκτός αλυσίδας (OCMS). Η μορφή αυτή δεν μπορεί ποτέ να εκληφθεί ως συναλλαγή, δηλώνει τους λογαριασμούς που πρέπει να την υπογράψουν και είναι η μορφή που υπογράφουν τα πορτοφόλια υλικού.
  • Ακατέργαστα μηνύματα (solana:signMessage): υπογράφουν ακριβώς τα bytes που περνά η εφαρμογή σας, χωρίς πρόθεμα και χωρίς δηλωμένους υπογράφοντες. Τα περισσότερα πορτοφόλια λογισμικού τα υποστηρίζουν, αλλά τίποτα στα bytes δεν τα χαρακτηρίζει ως μήνυμα αντί για συναλλαγή.

Χρησιμοποιήστε μηνύματα εκτός αλυσίδας όπου το πορτοφόλι τα υποστηρίζει και καταφύγετε σε ακατέργαστα μηνύματα όπου δεν τα υποστηρίζει. Ο υπόλοιπος οδηγός χρησιμοποιεί τη σύνδεση ως πρακτικό παράδειγμα, αλλά τα ίδια βήματα υπογραφής και επαλήθευσης ισχύουν για οποιοδήποτε μήνυμα.

Επιλέξτε μορφή μηνύματος

Μήνυμα εκτός αλυσίδαςΑκατέργαστο μήνυμαΣυναλλαγή
Δυνατότητα πορτοφολιούsignOffchainMessagesignMessagesignTransaction
Υποστήριξη πορτοφολιών υλικούΕξαρτάται από τη συσκευήΌχιΝαι
Αναγνώσιμο για τον χρήστηΝαιΜόνο λογισμικόΕξαρτάται από τη συσκευή
Δηλώνει τους απαιτούμενους υπογράφοντεςΝαιΌχιΝαι
Κλήσεις RPC και χρεώσειςΚαμίαΚαμίαBlockhash, αποστολή, χρέωση, επιβεβαίωση

Γιατί όχι συναλλαγή;

Παλαιότερα, τα πορτοφόλια υλικού δεν είχαν τρόπο να υπογράψουν ένα αναγνώσιμο μήνυμα. Πολλές εφαρμογές κατέφευγαν στο να ζητούν από αυτούς τους χρήστες να υπογράψουν μια συναλλαγή χωρίς ουσιαστική ενέργεια. Αυτό λειτουργεί, αλλά προσθέτει καθυστέρηση, κόστος και κίνδυνο:

  • Χρεώσεις δικτύου. Μια συναλλαγή επιβαρύνεται με χρέωση, ακόμη και όταν δεν κάνει τίποτα.
  • Πολλαπλές κλήσεις RPC. Η εφαρμογή σας ανακτά ένα blockhash, εκτελεί προσομοίωση, στέλνει και έπειτα ελέγχει διαρκώς για επιβεβαίωση.
  • Πρόσθετη καθυστέρηση. Κάθε μία από αυτές τις κλήσεις είναι μια μετάβαση μετ' επιστροφής πριν ο χρήστης συνδεθεί.
  • Τίποτα αναγνώσιμο. Ο χρήστης εγκρίνει bytes συναλλαγής και όχι μια δήλωση για το τι αποδέχεται.
  • Συμπεριφορικός κίνδυνος. Το να ζητάτε από τους χρήστες να υπογράψουν μια συναλλαγή μόνο για να συνδεθούν τους κάνει πιο εύκολους στο να εξαπατηθούν και να υπογράψουν μια επιζήμια συναλλαγή.

Ένα μήνυμα εκτός αλυσίδας τα παρακάμπτει όλα αυτά. Η μόνη μετάβαση μετ' επιστροφής γίνεται προς τον δικό σας διακομιστή.

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.

Τι υπογράφει το πορτοφόλι

Ένα μήνυμα εκτός αλυσίδας ξεκινά με έναν σταθερό τομέα υπογραφής, \xffsolana offchain. Καμία έγκυρη συναλλαγή δεν ξεκινά με αυτά τα bytes, επομένως ένα υπογεγραμμένο μήνυμα δεν μπορεί ποτέ να χρησιμοποιηθεί ως συναλλαγή. Η έκδοση 1 της μορφής στη συνέχεια απαριθμεί τους απαιτούμενους υπογράφοντες και μεταφέρει το μήνυμα ως κείμενο UTF-8, χωρίς όριο μήκους.

Το πορτοφόλι δημιουργεί αυτά τα bytes, τα υπογράφει και τα επιστρέφει μαζί με την υπογραφή. Η προδιαγραφή καλύπτει την πλήρη μορφή, συμπεριλαμβανομένης της παλαιότερης έκδοσης 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.

Επιλέξτε μέθοδο σύνδεσης

Δεν υποστηρίζουν όλα τα πορτοφόλια ακόμη μηνύματα εκτός αλυσίδας. Ελέγξτε τις δυνατότητες του συνδεδεμένου λογαριασμού πριν ζητήσετε υπογραφή και καταφύγετε σε εναλλακτική λύση όταν η δυνατότητα λείπει:

  1. Εάν ο λογαριασμός υποστηρίζει το solana:signOffchainMessage με έκδοση μηνύματος 1, υπογράψτε ένα μήνυμα εκτός αλυσίδας.
  2. Διαφορετικά, υπογράψτε το ίδιο challenge με το solana:signMessage. Τα περισσότερα πορτοφόλια λογισμικού το υποστηρίζουν.
  3. Δώστε στους χρήστες πορτοφολιών υλικού χωρίς υποστήριξη μηνυμάτων μια ρητή επιλογή συμμετοχής, όπως έναν διακόπτη «Χρησιμοποιώ πορτοφόλι υλικού», που υπογράφει αντ' αυτού μια συναλλαγή σύνδεσης. Μην τους κατευθύνετε σε αυτή τη διαδρομή από προεπιλογή.
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.

Υπογραφή μηνύματος στο πρόγραμμα περιήγησης

Εγκαταστήστε τα πακέτα Wallet Standard μαζί με το Kit:

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

Αυτές οι βοηθητικές συναρτήσεις δέχονται ένα UiWalletAccount, όπως το account που επιστρέφει το useConnectedWallet στον οδηγό 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 επιστρέφει το signedOffchainMessage, τα πλήρη bytes που υπέγραψε το πορτοφόλι, και το signature. Κωδικοποιήστε και τα δύο σε Base64 και στείλτε τα στον διακομιστή σας μαζί με τη διεύθυνση του πορτοφολιού.

Γράψτε το challenge έτσι ώστε να διαβάζεται εύκολα σε μικρή οθόνη. Αναφέρετε την εφαρμογή και τον τομέα σας, εξηγήστε τι κάνει η υπογραφή και συμπεριλάβετε ένα nonce μίας χρήσης και μια ώρα έκδοσης από τον διακομιστή σας:

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

Επαληθεύστε την υπογραφή στον διακομιστή σας

Η επαλήθευση είναι ένας τοπικός έλεγχος υπογραφής και δεν απαιτεί σύνδεση RPC. Αποκωδικοποιήστε τα bytes που υπέγραψε το πορτοφόλι, επιβεβαιώστε ότι ταιριάζουν με το challenge που εκδώσατε και έπειτα ελέγξτε την υπογραφή:

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 προκαλεί σφάλμα εάν το περιεχόμενο ή οι υπογράφοντες διαφέρουν από αυτά που περιμένατε. Το verifyOffchainMessageEnvelope προκαλεί σφάλμα εάν η υπογραφή λείπει ή δεν είναι έγκυρη. Αναζητήστε το expectedMessage με βάση το nonce στον διακομιστή σας αντί να εμπιστεύεστε το αντίγραφο που στέλνει πίσω ο πελάτης. Καταναλώστε το nonce μόλις επαληθευτεί, ώστε η ίδια υπογραφή να μην μπορεί να χρησιμοποιηθεί για σύνδεση δύο φορές.

Επαληθεύστε ένα ακατέργαστο μήνυμα

Μια υπογραφή signMessage καλύπτει απευθείας τα bytes του challenge, επομένως ο πελάτης χρειάζεται να στείλει μόνο την υπογραφή και τη διεύθυνση του πορτοφολιού. Ελέγξτε την σε σχέση με το challenge που εκδώσατε:

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");
}
}

Ισχύουν οι ίδιοι κανόνες για το nonce: αναζητήστε το expectedMessage με βάση το nonce και καταναλώστε το μόλις επαληθευτεί.

Το @solana/kit επανεξάγει αυτές τις βοηθητικές συναρτήσεις από το @solana/offchain-messages. Χρησιμοποιήστε το αυτόνομο πακέτο εάν ο διακομιστής σας δεν εξαρτάται από το Kit.

Sign In With Solana

Το Sign In With Solana (SIWS) τυποποιεί το ίδιο το challenge. Η εφαρμογή σας περνά δομημένα πεδία, όπως domain, statement, nonce και issuedAt, στη δυνατότητα solana:signIn του πορτοφολιού. Το πορτοφόλι τα μορφοποιεί σε ένα τυπικό μήνυμα, συνδέεται εάν χρειάζεται και το υπογράφει σε ένα βήμα.

Η έκδοση 1.1.0 του solana:signIn προσθέτει την επιλογή useOffchainMessage. Όταν περάσετε useOffchainMessage: { messageVersion: 1 }, το πορτοφόλι περιτυλίγει το κείμενο SIWS σε ένα μήνυμα εκτός αλυσίδας έκδοσης 1 με τον λογαριασμό υπογραφής ως τον μοναδικό απαιτούμενο υπογράφοντα. Τα πορτοφόλια υλικού μπορούν να το εμφανίσουν και να το υπογράψουν, και η έξοδος αναφέρει signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Τα πορτοφόλια που δηλώνουν την έκδοση 1.0.0 δεν γνωρίζουν την επιλογή, γι' αυτό ελέγξτε πρώτα την έκδοση της δυνατότητας και παραλείψτε την σε αυτή την περίπτωση.

Το solana:signIn είναι δυνατότητα πορτοφολιού και όχι λογαριασμού, επομένως αυτές οι βοηθητικές συναρτήσεις δέχονται ένα 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;
}

Δημιουργήστε την είσοδο στον διακομιστή σας, με ένα nonce μίας χρήσης και μια ώρα issuedAt, και κρατήστε ένα αντίγραφο με κλειδί το nonce. Καταναλώστε το nonce μόλις η σύνδεση επαληθευτεί.

Χωρίς το Wallet Adapter, το verifySignIn από το @solana/wallet-standard-util ελέγχει μόνο απλή έξοδο SIWS. Όταν το signedMessageFormat αναφέρει μήνυμα εκτός αλυσίδας, το signedMessage είναι ένα μήνυμα εκτός αλυσίδας με περιεχόμενο το κείμενο SIWS. Ανακατασκευάστε το αναμενόμενο κείμενο από την αποθηκευμένη είσοδο και τη διεύθυνση υπογραφής με το createSignInMessageText από το @solana/wallet-standard-util και έπειτα επαληθεύστε το όπως οποιοδήποτε άλλο μήνυμα εκτός αλυσίδας, όπως στην ενότητα Επαληθεύστε την υπογραφή στον διακομιστή σας. Το README του @solana/offchain-messages καλύπτει αυτή τη ροή στο Kit.

Για πορτοφόλια

Τα πορτοφόλια προσθέτουν υποστήριξη υλοποιώντας τη δυνατότητα solana:signOffchainMessage του Wallet Standard από το @solana/wallet-standard-features:

  • Αναφέρετε version: "1.0.0" και απαριθμήστε τις εκδόσεις μηνυμάτων που μπορείτε να υπογράψετε στο supportedMessageVersions. Σήμερα, αυτή είναι η [1].
  • Δημιουργήστε το preamble έκδοσης 1 από το requiredSigners, το οποίο ταξινομείτε και ελέγχετε για διπλότυπα. Απορρίψτε το αίτημα, αν ο λογαριασμός δεν είναι ένας από τους απαιτούμενους υπογράφοντες.
  • Εμφανίστε στον χρήστη ολόκληρο το κείμενο του μηνύματος πριν από την υπογραφή, ακόμη και σε συσκευές hardware.
  • Επιστρέψτε τα ακριβή byte που υπογράψατε ως signedOffchainMessage, μαζί με την υπογραφή signature Ed25519.
  • Για να υποστηρίξετε το off-chain Sign In With Solana, δηλώστε version: "1.1.0" για το solana:signIn. Όταν η είσοδος περιλαμβάνει το useOffchainMessage, υπογράψτε το κείμενο SIWS ως off-chain μήνυμα έκδοσης 1, επιστρέψτε τα byte του ως signedMessage και ορίστε το signedMessageFormat.
  • Συνεχίστε να υποστηρίζετε το solana:signMessage. Παραμένει αμετάβλητο και οι εφαρμογές εξακολουθούν να το χρησιμοποιούν ως εφεδρική λύση.

Is this page helpful?

© 2026 Ίδρυμα Solana. Με επιφύλαξη παντός δικαιώματος.