Проверка токена на бэкенде
Когда enableTokens равен true в server.json, мод добавляет подписанный токен к каждому открываемому URL:
https://your-hud.example.com?webgui_token=<токен>Ваш бэкенд может проверить этот токен, чтобы убедиться, что запрос пришёл от настоящего клиента WebGUI.
Формат токена
Токен состоит из двух частей, разделённых точкой .:
<base64url-payload>.<base64url-signature>| Часть | Содержимое |
|---|---|
| payload | UTF-8 строка 1|<playerUUID>|<expiresAtEpochSeconds>, закодированная в base64url без padding |
| signature | HMAC-SHA256 от байт payload, подписанный общим секретом, закодированный в base64url без padding |
Секрет — это байты tokenSecretBase64 из server.json, декодированные из base64.
Получение токена в SPA
Используйте хук useWebGUIToken из @webgui/react, чтобы прочитать токен из URL перед отправкой на бэкенд:
tsx
import { useWebGUIToken } from '@webgui/react'
export function App() {
const token = useWebGUIToken() // читает ?webgui_token=... из URL
useEffect(() => {
if (!token) return
fetch('/api/data?webgui_token=' + token)
.then(r => r.json())
.then(setData)
}, [token])
}Python-бэкенд
Требует только стандартную библиотеку — сторонние пакеты не нужны.
python
import base64
import hashlib
import hmac
import time
import uuid
def _b64url_decode(s: str) -> bytes:
padding = '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(s + padding)
def verify_webgui_token(token: str, secret_b64: str) -> dict | None:
"""
Проверяет подписанный токен WebGUI.
Возвращает {'player_uuid': str, 'expires_at': int} при успехе,
или None, если токен недействителен или истёк.
secret_b64 — значение tokenSecretBase64 из server.json.
"""
if not token:
return None
dot = token.find('.')
if dot <= 0 or dot >= len(token) - 1:
return None
try:
payload_bytes = _b64url_decode(token[:dot])
sig_bytes = _b64url_decode(token[dot + 1:])
except Exception:
return None
secret = base64.b64decode(secret_b64)
expected = hmac.new(secret, payload_bytes, hashlib.sha256).digest()
if not hmac.compare_digest(expected, sig_bytes):
return None
try:
parts = payload_bytes.decode('utf-8').split('|', 2)
except UnicodeDecodeError:
return None
if len(parts) != 3 or parts[0] != '1':
return None
try:
player_uuid = str(uuid.UUID(parts[1]))
exp = int(parts[2])
except (ValueError, AttributeError):
return None
if time.time() > exp:
return None
return {'player_uuid': player_uuid, 'expires_at': exp}Пример с FastAPI
python
import os
from fastapi import FastAPI, Query, HTTPException
app = FastAPI()
SECRET_B64 = os.environ['WEBGUI_TOKEN_SECRET'] # tokenSecretBase64 из server.json
@app.get('/api/data')
async def get_data(webgui_token: str = Query(...)):
data = verify_webgui_token(webgui_token, SECRET_B64)
if not data:
raise HTTPException(status_code=401, detail='Недействительный или истёкший токен')
# data['player_uuid'] содержит UUID игрока Minecraft
return {'player': data['player_uuid']}Пример с Flask
python
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
SECRET_B64 = os.environ['WEBGUI_TOKEN_SECRET']
@app.get('/api/data')
def get_data():
token = request.args.get('webgui_token', '')
data = verify_webgui_token(token, SECRET_B64)
if not data:
abort(401)
return jsonify({'player': data['player_uuid']})Node.js-бэкенд
Требует только встроенный модуль node:crypto.
js
const crypto = require('node:crypto');
/**
* Проверяет подписанный токен WebGUI.
*
* Возвращает { playerUuid: string, expiresAt: number } при успехе,
* или null, если токен недействителен или истёк.
*
* @param {string} token - Токен из query-параметра ?webgui_token.
* @param {string} secretB64 - Значение tokenSecretBase64 из server.json.
*/
function verifyWebGuiToken(token, secretB64) {
if (!token) return null;
const dot = token.indexOf('.');
if (dot <= 0 || dot >= token.length - 1) return null;
let payloadBytes, sigBytes;
try {
payloadBytes = Buffer.from(token.slice(0, dot), 'base64url');
sigBytes = Buffer.from(token.slice(dot + 1), 'base64url');
} catch {
return null;
}
const secret = Buffer.from(secretB64, 'base64');
const expected = crypto.createHmac('sha256', secret).update(payloadBytes).digest();
if (expected.length !== sigBytes.length || !crypto.timingSafeEqual(expected, sigBytes)) {
return null;
}
const parts = payloadBytes.toString('utf8').split('|');
if (parts.length !== 3 || parts[0] !== '1') return null;
const exp = parseInt(parts[2], 10);
if (isNaN(exp) || Date.now() / 1000 > exp) return null;
return { playerUuid: parts[1], expiresAt: exp };
}Пример с Express
js
const express = require('express');
const app = express();
const SECRET = process.env.WEBGUI_TOKEN_SECRET; // tokenSecretBase64 из server.json
app.get('/api/data', (req, res) => {
const result = verifyWebGuiToken(req.query.webgui_token, SECRET);
if (!result) return res.status(401).json({ error: 'Недействительный или истёкший токен' });
// result.playerUuid содержит UUID игрока Minecraft
res.json({ player: result.playerUuid });
});Пример с Hono / Edge
ts
import { Hono } from 'hono'
import { verifyWebGuiToken } from './verify' // вставьте функцию выше
const app = new Hono()
const SECRET = process.env.WEBGUI_TOKEN_SECRET!
app.get('/api/data', (c) => {
const result = verifyWebGuiToken(c.req.query('webgui_token') ?? '', SECRET)
if (!result) return c.json({ error: 'Недействительный или истёкший токен' }, 401)
return c.json({ player: result.playerUuid })
})Заметки по безопасности
tokenSecretBase64генерируется автоматически при первом запуске сервера. Скопируйте его изconfig/webgui/server.jsonв переменные окружения бэкенда — никогда не хардкодьте в исходниках.- Всегда используйте сравнение за константное время (
hmac.compare_digest/crypto.timingSafeEqual), чтобы предотвратить атаки по времени. - Имя query-параметра в
server.jsonпо умолчаниюwebgui_token. Передайте нужное имя вuseWebGUIToken(paramName)и используйте его на бэкенде. - Используйте
tokenTtlSecondsдля управления временем жизни токена (по умолчанию: 900 с / 15 мин). Короткоживущие токены уменьшают окно для атаки повторного использования.