签名消息并验证用户身份

对消息签名可以证明用户控制着某个钱包,无需交易、无需手续费,也无需任何网络调用。应用可以使用已签名的消息让用户登录、记录用户已接受条款、将钱包关联到现有账户,或授权发生在链下的操作(例如下单)。

Solana 钱包通过两种方式对消息签名:

  • 链下消息(solana:signOffchainMessage)会将你的文本包装为标准格式,该格式由链下消息签名(OCMS)规范定义。这种格式绝不会被误认为交易,会指明必须签名的账户,并且是硬件钱包所签名的格式。
  • 原始消息(solana:signMessage)会对应用传入的字节原样签名,没有前缀,也不声明签名者。大多数软件钱包都支持这种方式,但这些字节本身并不能表明它是消息而不是交易。

在钱包支持链下消息时使用链下消息,不支持时回退到原始消息。本指南的其余部分以登录为例,但相同的签名和验证步骤适用于任何消息。

选择消息格式

链下消息原始消息交易
钱包功能signOffchainMessagesignMessagesignTransaction
硬件钱包支持取决于设备否是
用户可读是仅限软件钱包取决于设备
声明所需签名者是否是
RPC 调用和手续费无无获取 blockhash、发送、手续费、确认

为什么不用交易?

过去,硬件钱包无法对可读消息签名。许多应用转而要求这些用户签署一笔空操作交易。这种做法虽然可行,但会增加延迟、成本和风险:

  • 网络手续费。 交易需要支付手续费,即使它什么都不做。
  • 多次 RPC 调用。 你的应用需要获取 blockhash、模拟、发送,然后轮询确认结果。
  • 额外延迟。 在用户登录之前,每一次调用都是一次往返。
  • 内容不可读。 用户批准的是交易字节,而不是对其所同意内容的说明。
  • 行为风险。 仅为登录就要求用户签署交易,会让他们更容易被诱骗去签署有害交易。

链下消息可以省去这一切。唯一的往返只发生在你自己的服务器上。

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 开头。任何有效交易都不会以这些字节开头,因此已签名的消息绝不可能被用作交易。格式的版本 1 随后会列出所需的签名者,并以 UTF-8 文本承载消息,不限制长度。

钱包构建这些字节并对其签名,然后将它们与签名一起返回。规范涵盖了完整的格式,包括旧版本 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. 如果账户支持消息版本 1 的 solana:signOffchainMessage,则对链下消息签名。
  2. 否则,使用 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.

在浏览器中签名消息

在安装 Kit 的同时安装 Wallet Standard 软件包:

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

这些辅助函数接受 UiWalletAccount,例如 React 指南中 useConnectedWallet 返回的 account:

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(钱包所签名的完整字节)和 signature。将两者进行 Base64 编码,并与钱包地址一起发送到你的服务器。

撰写挑战消息时,要确保它在小屏幕上也易于阅读。写明你的应用名称和域名,说明签名的作用,并包含由服务器生成的一次性 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 连接。解码钱包所签名的字节,确认它们与你签发的挑战消息一致,然后检查签名:

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 会抛出错误。请在服务器上通过 nonce 查找 expectedMessage,而不要信任客户端回传的副本。验证通过后立即消耗该 nonce,以免同一签名被用于重复登录。

验证原始消息

signMessage 的签名直接覆盖挑战消息的字节,因此客户端只需发送签名和钱包地址。请对照你签发的挑战消息进行检查:

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 规则:通过 nonce 查找 expectedMessage,并在验证通过后消耗该 nonce。

@solana/kit 从 @solana/offchain-messages 重新导出了这些辅助函数。如果你的服务器不依赖 Kit,请使用独立的软件包。

使用 Solana 登录

使用 Solana 登录(SIWS)对挑战消息本身进行了标准化。你的应用将 domain、statement、nonce 和 issuedAt 等结构化字段传递给钱包的 solana:signIn 功能。钱包会将它们格式化为标准消息,在需要时建立连接,并一步完成签名。

solana:signIn 的 1.1.0 版本新增了 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,来自 @solana/wallet-standard-util 的 verifySignIn 只会检查纯 SIWS 输出。当 signedMessageFormat 报告为链下消息时,signedMessage 是内容为 SIWS 文本的链下消息。请使用 @solana/wallet-standard-util 中的 createSignInMessageText,根据你存储的输入和签名地址重建预期文本,然后像验证其他链下消息一样进行验证,详见在服务器上验证签名。@solana/offchain-messages README 介绍了在 Kit 中实现此流程的方法。

面向钱包

钱包通过实现 @solana/wallet-standard-features 中的 solana:signOffchainMessage Wallet Standard 功能来添加支持:

  • 报告 version: "1.0.0",并在 supportedMessageVersions 中列出你能够签名的消息版本。目前为 [1]。
  • 根据 requiredSigners 构建版本 1 的前导部分(preamble),需对其排序并检查是否有重复项。如果该账户不在必需签名者之列,则拒绝该请求。
  • 在签名前,向用户完整显示消息文本,硬件设备上也不例外。
  • 将您签名的确切字节作为 signedOffchainMessage 返回,同时返回 Ed25519 signature。
  • 若要支持链下 Sign In With Solana,请为 solana:signIn 报告 version: "1.1.0"。当输入包含 useOffchainMessage 时,请将 SIWS 文本作为版本 1 链下消息进行签名,将其字节作为 signedMessage 返回,并设置 signedMessageFormat。
  • 请继续支持 solana:signMessage。该方法没有变化,应用仍将其用作后备方案。

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有