签名与验证
本页描述了一种让后台确保用户真正拥有所声明地址的方法。 请注意,并非所有 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
- 读取用户的
TonProofItemReply
信息。 - 验证接收的域是否与应用程序的域一致。
- 检查
TonProofItemReply.payload
是否为原始服务器所允许,是否仍处于活动状态。 - 检查
timestamp
是否为当前实际值。 - 根据 信息方案 组装信息。
- 从应用程序接口 (a) 或通过后端逻辑 (b) 获取
public_key
- 从用户处检索
TonProofItemReply
。- 使用 TON API 方法
POST /v2/tonconnect/stateinit
从walletStateInit
获取{public_key, address}
。 - 检查从
walletStateInit
提取的address
是否与用户声明的钱包address
一致。
- 使用 TON API 方法
- 验证接收到的域是否对应于应用程序的域。
- 通过钱包合约 get method 获取钱包的
public_key
。 - 如果合约未激活,或者缺少旧版本钱包(v1-v3)中的 get_method,则无法通过这种方式获取密钥。相反,您需要解析前端提供的 walletStateInit。确保 TonAddressItemReply.walletStateInit.hash() 等于 TonAddressItemReply.address.hash(),表明是 BoC 哈希值。
- 通过钱包合约 get method 获取钱包的
- 6a:
React 示例
- 将 token 提供者添加到应用的根目录:
function App() {
const [token, setToken] = useState<string | null>(null);
return (
<BackendTokenContext.Provider value={{token, setToken}}>
{ /* Your app */ }
</BackendTokenContext.Provider>
)
}
- 通过后台集成在前端实施身份验证:
示例
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
}
您可以查看我们展示主要方法的 示例:
- generatePayload:为 TON 证明生成有效载荷
- checkProof:检查证明并返回访问令牌。
概念解释
如果请求 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
注意:有效载荷是长度可变的非信任数据。我们将其放在最后,以避免使用不必要的长度前缀。
必须使用公钥验证签名:
-
-
解析
TonAddressItemReply.walletStateInit
并从 stateInit 获取公钥。您可以将walletStateInit.code
与标准钱包合约的代码进行比较,并根据找到的钱包版本解析数据。 -
检查
TonAddressItemReply.publicKey
是否等于获取的公钥 -
检查
TonAddressItemReply.walletStateInit.hash()
是否等于TonAddressItemReply.address
。.hash()
表示 BoC 哈希值。
-