Skip to content

Проверка токена на бэкенде

Когда enableTokens равен true в server.json, мод добавляет подписанный токен к каждому открываемому URL:

https://your-hud.example.com?webgui_token=<токен>

Ваш бэкенд может проверить этот токен, чтобы убедиться, что запрос пришёл от настоящего клиента WebGUI.

Формат токена

Токен состоит из двух частей, разделённых точкой .:

<base64url-payload>.<base64url-signature>
ЧастьСодержимое
payloadUTF-8 строка 1|<playerUUID>|<expiresAtEpochSeconds>, закодированная в base64url без padding
signatureHMAC-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 мин). Короткоживущие токены уменьшают окно для атаки повторного использования.