跳到主要内容

签名与验证

本页描述了一种让后台确保用户真正拥有所声明地址的方法。 请注意,并非所有 DApp 都需要用户验证。

请注意,并非所有DApps都需要 ton_proof 验证。 这对于后端的授权是必要的,以确保用户确实拥有声明的地址,因此可以推断出用户有权访问其在后端的数据。

它是如何工作的?

  • 用户启动登录程序。
  • 后台生成一个 ton_proof 实体并发送给前台。
  • 前端使用 ton_proof 登录钱包,并接收已签名的 ton_proof 返回。
  • 前台将已签名的 ton_proof 发送到后台进行验证。


ton_proof 的结构

我们将使用在连接器中实现的 TonProof 实体。

type TonProofItemReply = TonProofItemReplySuccess | TonProofItemReplyError;

type TonProofItemReplySuccess = {
name: "ton_proof";
proof: {
timestamp: string; // 64-bit unix epoch time of the signing operation (seconds)
domain: {
lengthBytes: number; // AppDomain Length
value: string; // app domain name (as url part, without encoding)
};
signature: string; // base64-encoded signature
payload: string; // payload from the request
}
}

在服务器端检查 ton_proof

  1. 读取用户的 TonProofItemReply 信息。
  2. 验证接收的域是否与应用程序的域一致。
  3. 检查 TonProofItemReply.payload 是否为原始服务器所允许,是否仍处于活动状态。
  4. 检查 timestamp 是否为当前实际值。
  5. 根据 信息方案 组装信息。
  6. 从应用程序接口 (a) 或通过后端逻辑 (b) 获取 public_key
  • 从用户处检索 TonProofItemReply
    • 使用 TON API 方法 POST /v2/tonconnect/stateinitwalletStateInit 获取 {public_key, address}
    • 检查从 walletStateInit 提取的 address 是否与用户声明的钱包 address 一致。
  • 验证接收到的域是否对应于应用程序的域。
    • 通过钱包合约 get method 获取钱包的 public_key
    • 如果合约未激活,或者缺少旧版本钱包(v1-v3)中的 get_method,则无法通过这种方式获取密钥。相反,您需要解析前端提供的 walletStateInit。确保 TonAddressItemReply.walletStateInit.hash() 等于 TonAddressItemReply.address.hash(),表明是 BoC 哈希值。
  1. 6a:

React 示例

  1. 将 token 提供者添加到应用的根目录:
function App() {
const [token, setToken] = useState<string | null>(null);

return (
<BackendTokenContext.Provider value={{token, setToken}}>
{ /* Your app */ }
</BackendTokenContext.Provider>
)
}
  1. 通过后台集成在前端实施身份验证:
示例
import {useContext, useEffect, useRef} from "react";
import {BackendTokenContext} from "./BackendTokenContext";
import {useIsConnectionRestored, useTonConnectUI, useTonWallet} from "@tonconnect/ui-react";
import {backendAuth} from "./backend-auth";

const localStorageKey = 'my-dapp-auth-token';
const payloadTTLMS = 1000 * 60 * 20;

export function useBackendAuth() {
const { setToken } = useContext(BackendTokenContext);
const isConnectionRestored = useIsConnectionRestored();
const wallet = useTonWallet();
const [tonConnectUI] = useTonConnectUI();
const interval = useRef<ReturnType<typeof setInterval> | undefined>();

useEffect(() => {
if (!isConnectionRestored || !setToken) {
return;
}

clearInterval(interval.current);

if (!wallet) {
localStorage.removeItem(localStorageKey);
setToken(null);

const refreshPayload = async () => {
tonConnectUI.setConnectRequestParameters({ state: 'loading' });

const value = await backendAuth.generatePayload();
if (!value) {
tonConnectUI.setConnectRequestParameters(null);
} else {
tonConnectUI.setConnectRequestParameters({state: 'ready', value});
}
}

refreshPayload();
setInterval(refreshPayload, payloadTTLMS);
return;
}

const token = localStorage.getItem(localStorageKey);
if (token) {
setToken(token);
return;
}

if (wallet.connectItems?.tonProof && !('error' in wallet.connectItems.tonProof)) {
backendAuth.checkProof(wallet.connectItems.tonProof.proof, wallet.account).then(result => {
if (result) {
setToken(result);
localStorage.setItem(localStorageKey, result);
} else {
alert('Please try another wallet');
tonConnectUI.disconnect();
}
})
} else {
alert('Please try another wallet');
tonConnectUI.disconnect();
}

}, [wallet, isConnectionRestored, setToken])
}

后台示例

检查证明是否有效(Next.js)
'use server'
import {Address, Cell, contractAddress, loadStateInit, TonClient4} from '@ton/ton'


export async function isValid(proof, account) {
const payload = {
address: account.address,
public_key: account.publicKey,
proof: {
...proof,
state_init: account.walletStateInit
}
}
const stateInit = loadStateInit(Cell.fromBase64(payload.proof.state_init).beginParse())
const client = new TonClient4({
endpoint: 'https://mainnet-v4.tonhubapi.com'
})
const masterAt = await client.getLastBlock()
const result = await client.runMethod(masterAt.last.seqno, Address.parse(payload.address), 'get_public_key', [])
const publicKey = Buffer.from(result.reader.readBigNumber().toString(16).padStart(64, '0'), 'hex')
if (!publicKey) {
return false
}
const wantedPublicKey = Buffer.from(payload.public_key, 'hex')
if (!publicKey.equals(wantedPublicKey)) {
return false
}
const wantedAddress = Address.parse(payload.address)
const address = contractAddress(wantedAddress.workChain, stateInit)
if (!address.equals(wantedAddress)) {
return false
}
const now = Math.floor(Date.now() / 1000)
if (now - (60 * 15) > payload.proof.timestamp) {
return false
}
return true
}

您可以查看我们展示主要方法的 示例

概念解释

如果请求 TonProofItem,钱包会证明所选账户密钥的所有权。签名信息绑定到

  • Timestamp 是签名操作的64位unix时代时间
  • Payload 是一个可变长度的二进制字符串。
  • 应用域名
  • 签署时间戳
  • 应用程序的自定义有效载荷(服务器可在其中放置非密钥、cookie id 和过期时间)
message = utf8_encode("ton-proof-item-v2/") ++
Address ++
AppDomain ++
Timestamp ++
Payload

signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message)))

公钥必须验证签名:

  • 首先,尝试通过在 Address 部署的智能合约上的 get_public_key get-method 获得公钥。
  • 如果智能合约尚未部署,或缺少get-method,您需要:
  • hash:256 位无符号整数,大端位;
  • AppDomain 是 Length ++ EncodedDomainName

注意:有效载荷是长度可变的非信任数据。我们将其放在最后,以避免使用不必要的长度前缀。

必须使用公钥验证签名:

  1. GO 演示应用

  2. TS 示例

    1. 解析 TonAddressItemReply.walletStateInit 并从 stateInit 获取公钥。您可以将 walletStateInit.code 与标准钱包合约的代码进行比较,并根据找到的钱包版本解析数据。

    2. 检查 TonAddressItemReply.publicKey 是否等于获取的公钥

    3. 检查 TonAddressItemReply.walletStateInit.hash() 是否等于 TonAddressItemReply.address.hash()表示 BoC 哈希值。

参阅

另请参见