Ký tin nhắn & Xác thực người dùng

Ký một tin nhắn chứng minh rằng người dùng kiểm soát một ví, mà không cần giao dịch, không mất phí và không có lệnh gọi mạng. Các ứng dụng dùng tin nhắn đã ký để đăng nhập cho người dùng, ghi nhận rằng người dùng đã chấp nhận điều khoản, liên kết ví với tài khoản hiện có, hoặc ủy quyền một hành động diễn ra off-chain, chẳng hạn như đặt lệnh.

Ví Solana ký tin nhắn theo hai cách:

  • Tin nhắn off-chain (solana:signOffchainMessage) bao bọc văn bản của bạn trong một định dạng chuẩn, được xác định bởi đặc tả ký tin nhắn off-chain (OCMS). Định dạng này không bao giờ bị nhầm lẫn với giao dịch, nêu rõ các tài khoản phải ký, và là định dạng mà ví cứng ký.
  • Tin nhắn thô (solana:signMessage) ký chính xác các byte mà ứng dụng của bạn truyền vào, không có tiền tố và không khai báo người ký. Hầu hết các ví phần mềm đều hỗ trợ loại này, nhưng không có gì trong các byte cho biết đó là tin nhắn chứ không phải giao dịch.

Hãy dùng tin nhắn off-chain khi ví hỗ trợ, và dự phòng bằng tin nhắn thô khi ví không hỗ trợ. Phần còn lại của hướng dẫn này dùng đăng nhập làm ví dụ minh họa, nhưng các bước ký và xác minh tương tự áp dụng cho mọi tin nhắn.

Chọn định dạng tin nhắn

Tin nhắn off-chainTin nhắn thôGiao dịch
Tính năng của vísignOffchainMessagesignMessagesignTransaction
Hỗ trợ ví cứngTùy thiết bịKhôngCó
Người dùng đọc đượcCóChỉ ví phần mềmTùy thiết bị
Khai báo người ký bắt buộcCóKhôngCó
Lệnh gọi RPC và phíKhông cóKhông cóBlockhash, gửi, phí, xác nhận

Tại sao không dùng giao dịch?

Trước đây, ví cứng không có cách nào để ký một tin nhắn đọc được. Nhiều ứng dụng đã chuyển sang yêu cầu những người dùng này ký một giao dịch no-op thay thế. Cách đó vẫn hoạt động, nhưng nó làm tăng độ trễ, chi phí và rủi ro:

  • Phí mạng. Một giao dịch phải trả phí, ngay cả khi nó không làm gì cả.
  • Nhiều lệnh gọi RPC. Ứng dụng của bạn lấy blockhash, mô phỏng, gửi, và sau đó thăm dò để chờ xác nhận.
  • Độ trễ tăng thêm. Mỗi lệnh gọi đó là một lượt đi về trước khi người dùng được đăng nhập.
  • Không có nội dung đọc được. Người dùng phê duyệt các byte của giao dịch, chứ không phải một tuyên bố về những gì họ đang đồng ý.
  • Rủi ro hành vi. Yêu cầu người dùng ký giao dịch chỉ để đăng nhập khiến họ dễ bị lừa ký một giao dịch độc hại hơn.

Tin nhắn off-chain loại bỏ tất cả những điều đó. Lượt đi về duy nhất là tới máy chủ của chính bạn.

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.

Ví ký những gì

Tin nhắn off-chain bắt đầu bằng một miền ký cố định, \xffsolana offchain. Không giao dịch hợp lệ nào bắt đầu bằng các byte đó, nên một tin nhắn đã ký không bao giờ có thể được dùng như giao dịch. Phiên bản 1 của định dạng sau đó liệt kê các người ký bắt buộc và mang tin nhắn dưới dạng văn bản UTF-8, không giới hạn độ dài.

Ví tạo các byte này, ký chúng, và trả về cùng với chữ ký. Đặc tả bao quát toàn bộ định dạng, bao gồm cả phiên bản 0 cũ.

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.

Chọn phương thức đăng nhập

Không phải ví nào cũng đã hỗ trợ tin nhắn off-chain. Hãy kiểm tra các tính năng của tài khoản đang kết nối trước khi yêu cầu chữ ký, và dự phòng khi tính năng bị thiếu:

  1. Nếu tài khoản hỗ trợ solana:signOffchainMessage với phiên bản tin nhắn 1, hãy ký một tin nhắn off-chain.
  2. Nếu không, hãy ký cùng challenge đó bằng solana:signMessage. Hầu hết các ví phần mềm đều hỗ trợ.
  3. Hãy cho người dùng ví cứng không hỗ trợ tin nhắn một lựa chọn chủ động rõ ràng, chẳng hạn như công tắc "Tôi đang dùng ví cứng", để ký một giao dịch đăng nhập thay thế. Đừng mặc định đưa họ vào lối này.
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.

Ký tin nhắn trong trình duyệt

Cài đặt các gói Wallet Standard cùng với Kit:

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

Các hàm hỗ trợ này nhận một UiWalletAccount, chẳng hạn như account được trả về bởi useConnectedWallet trong hướng dẫn 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 trả về signedOffchainMessage, tức toàn bộ các byte mà ví đã ký, và signature. Hãy mã hóa cả hai bằng Base64 và gửi chúng đến máy chủ của bạn cùng với địa chỉ ví.

Hãy viết challenge sao cho dễ đọc trên màn hình nhỏ. Nêu tên ứng dụng và miền của bạn, cho biết việc ký làm gì, và đưa vào một nonce dùng một lần cùng thời điểm phát hành từ máy chủ của bạn:

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

Xác minh chữ ký trên máy chủ của bạn

Việc xác minh là một bước kiểm tra chữ ký cục bộ và không cần kết nối RPC. Giải mã các byte mà ví đã ký, xác nhận chúng khớp với challenge bạn đã phát hành, sau đó kiểm tra chữ ký:

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 sẽ ném lỗi nếu nội dung hoặc những người ký khác với những gì bạn mong đợi. verifyOffchainMessageEnvelope sẽ ném lỗi nếu chữ ký bị thiếu hoặc không hợp lệ. Hãy tra cứu expectedMessage theo nonce trên máy chủ của bạn thay vì tin vào bản sao mà client gửi lại. Hãy tiêu thụ nonce ngay khi xác minh thành công, để cùng một chữ ký không thể đăng nhập hai lần.

Xác minh tin nhắn thô

Chữ ký signMessage bao phủ trực tiếp các byte của challenge, nên client chỉ cần gửi chữ ký và địa chỉ ví. Hãy kiểm tra nó so với challenge bạn đã phát hành:

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

Áp dụng các quy tắc nonce tương tự: tra cứu expectedMessage theo nonce và tiêu thụ nó ngay khi xác minh thành công.

@solana/kit xuất lại các hàm hỗ trợ này từ @solana/offchain-messages. Hãy dùng gói độc lập nếu máy chủ của bạn không phụ thuộc vào Kit.

Sign In With Solana

Sign In With Solana (SIWS) chuẩn hóa chính challenge. Ứng dụng của bạn truyền các trường có cấu trúc, chẳng hạn như domain, statement, nonce và issuedAt, tới tính năng solana:signIn của ví. Ví định dạng chúng thành một tin nhắn chuẩn, kết nối nếu cần, và ký chỉ trong một bước.

Phiên bản 1.1.0 của solana:signIn bổ sung tùy chọn useOffchainMessage. Khi bạn truyền useOffchainMessage: { messageVersion: 1 }, ví sẽ bao bọc văn bản SIWS trong một tin nhắn off-chain phiên bản 1 với tài khoản ký là người ký bắt buộc duy nhất. Ví cứng có thể hiển thị và ký nó, và đầu ra báo cáo signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Các ví quảng bá phiên bản 1.0.0 không biết tùy chọn này, vì vậy hãy kiểm tra phiên bản tính năng trước và bỏ qua tùy chọn nếu không hỗ trợ.

solana:signIn là tính năng của ví, không phải tính năng của tài khoản, nên các hàm hỗ trợ này nhận một 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;
}

Hãy tạo đầu vào trên máy chủ của bạn, với nonce dùng một lần và thời điểm issuedAt, và lưu một bản sao theo khóa là nonce. Hãy tiêu thụ nonce ngay khi đăng nhập được xác minh.

Nếu không dùng Wallet Adapter, verifySignIn từ @solana/wallet-standard-util chỉ kiểm tra đầu ra SIWS thuần. Khi signedMessageFormat báo cáo một tin nhắn off-chain, signedMessage là một tin nhắn off-chain có nội dung là văn bản SIWS. Hãy dựng lại văn bản mong đợi từ đầu vào đã lưu của bạn và địa chỉ ký bằng createSignInMessageText từ @solana/wallet-standard-util, sau đó xác minh nó giống như mọi tin nhắn off-chain khác, như trong Xác minh chữ ký trên máy chủ của bạn. Tài liệu README của @solana/offchain-messages bao quát quy trình này trong Kit.

Dành cho ví

Ví bổ sung hỗ trợ bằng cách triển khai tính năng Wallet Standard solana:signOffchainMessage từ @solana/wallet-standard-features:

  • Báo cáo version: "1.0.0" và liệt kê các phiên bản tin nhắn mà bạn có thể ký trong supportedMessageVersions. Hiện nay, đó là [1].
  • Tạo phần mở đầu (preamble) phiên bản 1 từ requiredSigners, sau đó sắp xếp và kiểm tra các giá trị trùng lặp. Từ chối yêu cầu nếu tài khoản không nằm trong danh sách người ký bắt buộc.
  • Hiển thị đầy đủ nội dung thông điệp cho người dùng trước khi ký, kể cả trên các thiết bị phần cứng.
  • Trả về chính xác các byte bạn đã ký dưới dạng signedOffchainMessage, cùng với signature Ed25519.
  • Để hỗ trợ Sign In With Solana ngoài chuỗi (off-chain), hãy báo cáo version: "1.1.0" cho solana:signIn. Khi đầu vào có useOffchainMessage, hãy ký văn bản SIWS dưới dạng thông điệp ngoài chuỗi phiên bản 1, trả về các byte của nó dưới dạng signedMessage, và thiết lập signedMessageFormat.
  • Tiếp tục hỗ trợ solana:signMessage. Phương thức này không thay đổi, và các ứng dụng vẫn dùng nó như một giải pháp dự phòng.

Is this page helpful?

Mục lục

Chỉnh sửa trang