Ký thông điệp & Xác thực người dùng

Ký một thông điệp chứng minh 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 phát sinh lệnh gọi mạng. Các ứng dụng dùng thông điệp đã ký để đăng nhập người dùng, ghi nhận việc 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ý thông điệp theo hai cách:

  • Thông điệp off-chain (solana:signOffchainMessage) đóng gói văn bản của bạn theo một định dạng chuẩn, được định nghĩa bởi đặc tả ký thông điệp off-chain (OCMS). Định dạng này không bao giờ bị nhầm với giao dịch, nêu rõ các tài khoản phải ký, và là định dạng mà ví phần cứng ký.
  • Thông điệp 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 ví phần mềm đều hỗ trợ, nhưng không có gì trong các byte đánh dấu đó là thông điệp thay vì giao dịch.

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

Chọn định dạng thông điệp

Thông điệp off-chainThông điệp thôGiao dịch
Tính năng của vísignOffchainMessagesignMessagesignTransaction
Hỗ trợ ví phần 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í phần cứng không có cách nào để ký một thông điệp có thể đọc được. Nhiều ứng dụng đành yêu cầu những người dùng này ký một giao dịch không làm gì (no-op) thay thế. Cách này dùng được, nhưng làm tăng độ trễ, chi phí và rủi ro:

  • Phí mạng. Giao dịch phải trả phí, ngay cả khi không làm gì.
  • Nhiều lệnh gọi RPC. Ứng dụng của bạn lấy blockhash, mô phỏng, gửi, rồi thăm dò để chờ xác nhận.
  • Độ trễ tăng thêm. Mỗi lệnh gọi đó là một vòng đi-về trước khi người dùng được đăng nhập.
  • Không đọc được. Người dùng phê duyệt các byte giao dịch, chứ không phải một tuyên bố về nội dung họ đồ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.

Thông điệp off-chain bỏ qua tất cả những điều đó. Vòng đ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ì

Thông điệp off-chain bắt đầu bằng một miền ký (signing domain) cố định, \xffsolana offchain. Không giao dịch hợp lệ nào bắt đầu bằng các byte đó, nên thông điệp đã ký không bao giờ có thể bị dùng như một 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 thông điệp 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 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

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

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

Hãy viết thử thách sao cho dễ đọc trên màn hình nhỏ. Nêu tên ứng dụng và tên 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à 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 thử thách bạn đã phát hành, rồi 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 người ký khác với những gì bạn mong đợi. verifyOffchainMessageEnvelope 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 thông điệp thô

Chữ ký signMessage bao phủ trực tiếp các byte của thử thách, nên client chỉ cần gửi chữ ký và địa chỉ ví. Hãy kiểm tra nó với thử thách 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ùng quy tắc nonce: 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 trợ giúp 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 thử thách. Ứng dụng của bạn truyền các trường có cấu trúc, như domain, statement, nonce và issuedAt, đến tính năng solana:signIn của ví. Ví định dạng chúng thành một thông điệp chuẩn, kết nối nếu cần, và ký 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ẽ đóng gói văn bản SIWS trong một thông điệp 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í phần 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í cô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 phải phiên bản 1.1.0.

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 trợ giúp 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;
}

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à giữ một bản sao được đánh khóa theo 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 là thông điệp off-chain, signedMessage là một thông điệp 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 và địa chỉ ký bằng createSignInMessageText từ @solana/wallet-standard-util, rồi xác minh như mọi thông điệp off-chain khác, như trong Xác minh chữ ký trên máy chủ của bạn. README của @solana/offchain-messages trình bày 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 thông điệp 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, đồng thời sắp xếp và kiểm tra các mục 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 off-chain phiên bản 1, trả về các byte của nó dưới dạng signedMessage, và đặt 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ó làm giải pháp dự phòng.

Is this page helpful?