El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Servidor

Cómo Crear un Sitio Web para Servidor de MU Online desde Cero

Aprende a construir el sitio web de tu servidor de MU Online desde cero: estructura, base de datos, panel de registro y gestión de cuentas paso a paso.

EQ Equipo ViciadosMU · Actualizado el 4 jul 2026 · ⏱ 18 min de lectura

Por qué el Sitio Web es Parte del Servidor, No un Complemento

Cuando la mayoría de los administradores nuevos piensan en montar un servidor de MU Online, se enfocan en el emulador, en los archivos de configuración y en los rates. El sitio web queda para el final, tratado como un adorno. Ese es un error de arquitectura.

El sitio web es la primera interfaz que un jugador toca antes de instalar el cliente. Es donde se registra, recupera su contraseña, consulta los rankings y entiende las reglas del servidor. Un sitio web roto o inexistente comunica, antes que cualquier otra cosa, que el servidor no está bien administrado.

Esta guía cubre la construcción del sitio web desde la infraestructura hasta las funcionalidades esenciales: estructura de directorios, conexión a la base de datos del emulador, sistema de registro de cuentas y panel básico de estadísticas públicas. No asumimos ninguna plantilla de terceros; todo se construye con lógica propia.

Nota: Esta guía utiliza PHP 8.x como lenguaje de backend y MySQL/MariaDB como base de datos de ejemplo. Si tu emulador usa MSSQL (SQL Server), la lógica es idéntica; solo cambia el driver de conexión (PDO con sqlsrv en lugar de mysql).

Estructura de Directorios del Proyecto

Antes de escribir una sola línea de código, define la estructura de carpetas. Una estructura clara separa responsabilidades y facilita el mantenimiento a largo plazo.

/var/www/mu-servidor/
│
├── public/                  → Raíz pública (DocumentRoot de Apache/Nginx)
│   ├── index.php            → Punto de entrada principal
│   ├── registro.php         → Formulario de registro
│   ├── ranking.php          → Página de rankings públicos
│   └── assets/
│       ├── css/
│       ├── js/
│       └── img/
│
├── src/                     → Lógica de la aplicación (nunca expuesta al público)
│   ├── Database.php         → Clase de conexión PDO
│   ├── Account.php          → Modelo de cuenta de jugador
│   ├── Character.php        → Modelo de personaje (solo lectura)
│   └── Validator.php        → Validaciones de formularios
│
├── templates/               → Plantillas HTML reutilizables
│   ├── header.php
│   ├── footer.php
│   └── nav.php
│
├── config/                  → Configuración (NUNCA en el DocumentRoot)
│   └── database.php         → Credenciales de base de datos
│
└── logs/                    → Registros de errores internos

La regla fundamental: todo lo que contiene lógica sensible o credenciales vive fuera del directorio public/. El servidor web solo debe poder servir archivos desde public/. Si un atacante accede directamente a config/database.php a través del navegador y obtiene el contenido del archivo, la arquitectura está mal configurada.

En Apache, apunta el DocumentRoot a /var/www/mu-servidor/public. En Nginx, el bloque root debe apuntar al mismo directorio.


Conexión a la Base de Datos del Emulador

La base de datos del emulador es el corazón del sitio web. Toda información de cuentas, personajes y rankings vive ahí. La conexión debe ser robusta, reutilizable y protegida contra inyección SQL.

<?php
// src/Database.php

declare(strict_types=1);

class Database
{
    private static ?PDO $instance = null;

    public static function connect(): PDO
    {
        if (self::$instance !== null) {
            return self::$instance;
        }

        // Carga credenciales desde config/ (fuera del DocumentRoot)
        $config = require __DIR__ . '/../config/database.php';

        $dsn = sprintf(
            'mysql:host=%s;dbname=%s;charset=utf8mb4',
            $config['host'],     // → ej. '127.0.0.1'
            $config['dbname']    // → ej. 'MuOnline'
        );

        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,   // → sentencias preparadas reales
        ];

        self::$instance = new PDO(
            $dsn,
            $config['user'],     // → usuario con permisos mínimos
            $config['password'],
            $options
        );

        return self::$instance;
    }

    // Previene clonación del singleton
    private function __clone() {}
}

El archivo config/database.php es un array PHP simple:

<?php
// config/database.php

return [
    'host'     => '127.0.0.1',
    'dbname'   => 'MuOnline',
    'user'     => 'web_readonly',   // → usuario de mínimo privilegio
    'password' => 'TU_PASSWORD_AQUI',
];

> [!ATENCION] > Nunca uses el usuario root de la base de datos en el sitio web. Crea un usuario dedicado con permisos estrictamente necesarios: SELECT en tablas de estadísticas, INSERT y UPDATE solo en la tabla de cuentas. Si el sitio web es comprometido, el daño quedará contenido.


Sistema de Registro de Cuentas

El registro es la funcionalidad más crítica del sitio web y la más atacada. Cada campo del formulario es una superficie de ataque potencial.

Validación del Formulario

<?php
// src/Validator.php

declare(strict_types=1);

class Validator
{
    private array $errors = [];

    public function validateRegistration(array $data): bool
    {
        // Usuario: 4-10 caracteres alfanuméricos
        if (!preg_match('/^[a-zA-Z0-9]{4,10}$/', $data['username'] ?? '')) {
            $this->errors['username'] = 'El usuario debe tener entre 4 y 10 caracteres alfanuméricos.';
        }

        // Contraseña: mínimo 8 caracteres, al menos un número
        if (!preg_match('/^(?=.*\d).{8,20}$/', $data['password'] ?? '')) {
            $this->errors['password'] = 'La contraseña debe tener entre 8 y 20 caracteres e incluir al menos un número.';
        }

        // Confirmación de contraseña
        if (($data['password'] ?? '') !== ($data['password_confirm'] ?? '')) {
            $this->errors['password_confirm'] = 'Las contraseñas no coinciden.';
        }

        // Correo electrónico
        if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
            $this->errors['email'] = 'El correo electrónico no es válido.';
        }

        return empty($this->errors);
    }

    public function getErrors(): array
    {
        return $this->errors;
    }
}

Modelo de Cuenta

<?php
// src/Account.php

declare(strict_types=1);

class Account
{
    private PDO $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function exists(string $username): bool
    {
        $stmt = $this->db->prepare(
            'SELECT COUNT(*) FROM MEMB_INFO WHERE memb___id = :username'
            //                     → tabla estándar del emulador Season 6
        );
        $stmt->execute([':username' => $username]);
        return (int) $stmt->fetchColumn() > 0;
    }

    public function create(string $username, string $password, string $email): bool
    {
        // Nunca almacenes contraseñas en texto plano
        $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

        $stmt = $this->db->prepare(
            'INSERT INTO MEMB_INFO
             (memb___id, memb__pwd, mail_addr, bloc_code, ctl1_code)
             VALUES (:id, :pwd, :email, 0, 0)'
        );

        return $stmt->execute([
            ':id'    => $username,
            ':pwd'   => $hash,
            ':email' => $email,
        ]);
    }
}

> [!CONSEJO] > La estructura de la tabla MEMB_INFO varía según el emulador (GameServer, MuEmu, OpenMU, etc.). Revisa el esquema de tu base de datos antes de adaptar las consultas. Los nombres de columna aquí corresponden al esquema Season 6 estándar; algunos emuladores usan columnas adicionales obligatorias con valores por defecto.


Rankings Públicos: Consultas de Solo Lectura

El ranking es la página más visitada de cualquier servidor. Debe ser rápida y nunca debe exponer datos sensibles.

<?php
// src/Character.php — método para ranking público

public function getTopByResets(int $limit = 20): array
{
    $stmt = $this->db->prepare(
        'SELECT
             Name        AS nombre,       -- → nombre del personaje
             Class       AS clase,        -- → clase (int, convertir a texto en PHP)
             cLevel      AS nivel,
             Resets      AS resets,
             OnlineCode  AS en_linea      -- → 1 si está conectado ahora
         FROM Character
         ORDER BY Resets DESC, cLevel DESC
         LIMIT :limite'
    );
    $stmt->bindValue(':limite', $limit, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll();
}

Presenta estos datos en una tabla HTML con paginación. Agrega un índice en la columna Resets de la tabla Character si el servidor tiene muchos personajes registrados; sin índice, esa consulta hace un escaneo completo de la tabla en cada carga de página.

-- Ejecutar una sola vez desde el cliente SQL
CREATE INDEX idx_resets ON Character (Resets DESC, cLevel DESC);

Seguridad Antes de Abrir al Público

Un sitio web con vulnerabilidades no solo pone en riesgo las cuentas de los jugadores: puede comprometer toda la base de datos del servidor. Antes de anunciar el servidor, verifica esta lista:

Protección CSRF. Todo formulario POST debe incluir un token único por sesión. Genera el token al cargar el formulario y valídalo al procesar el envío. Un atacante que logre que un usuario autenticado visite una página maliciosa podría enviar formularios en su nombre sin este mecanismo.

Rate limiting en registro. Sin límite de solicitudes, un bot puede registrar miles de cuentas en minutos. Implementa un contador por IP en la sesión o en la base de datos: máximo 3-5 registros por IP en 24 horas es un valor razonable para servidores nuevos.

Headers de seguridad HTTP. Configura en Nginx o Apache los headers Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff y Referrer-Policy: strict-origin. Estos no reemplazan la validación de backend, pero reducen el impacto de ciertos ataques del lado del cliente.

HTTPS obligatorio. Un servidor sin HTTPS expone las credenciales de los jugadores en texto plano durante la transmisión. Configura un certificado y redirige todo el tráfico HTTP a HTTPS con un redirect 301 permanente.

Nota: El sitio web no es una funcionalidad secundaria del servidor: es su cara pública y su primera línea de defensa. Un panel bien construido, seguro y rápido comunica profesionalismo y genera confianza en la comunidad mucho antes de que los jugadores entren al juego.

Próximos Pasos

Con la base del sitio web funcionando, el siguiente nivel de administración implica construir sobre esta misma arquitectura:

  • Panel de administración interno — gestión de bloqueos de cuenta, ajuste de items y consulta de logs de actividad sospechosa.
  • Sistema de noticias y eventos — una tabla simple en la base de datos con las novedades del servidor, presentada en la portada.
  • Integración con Discord — notificaciones automáticas de nuevos registros, caídas del servidor o boss kills usando webhooks de Discord desde el backend PHP.
  • Caché de rankings — cuando el servidor tiene cientos de personajes, regenerar el ranking en cada petición es costoso. Un sistema de caché simple (archivo JSON actualizado cada 5 minutos por un cron job) mantiene la página rápida bajo carga.

Cada una de estas funcionalidades se construye sobre los mismos principios: separación entre lógica pública y privada, consultas preparadas, validación estricta de entradas y permisos de base de datos mínimos.

Perguntas frequentes

¿Qué lenguaje de programación es más recomendable para el sitio web de un servidor MU?

PHP sigue siendo el lenguaje más usado en la comunidad MU por su amplia compatibilidad con hosting compartido y la cantidad de recursos disponibles. Sin embargo, si ya tienes experiencia con Node.js o Python, puedes construir tu panel con cualquiera de estos; lo fundamental es que el backend pueda consultar y escribir en la base de datos del servidor (MSSQL o MySQL, según tu emulador).

¿El sitio web necesita estar en el mismo servidor que el emulador?

No es obligatorio. Muchos administradores separan el servidor de juego y el servidor web en máquinas distintas. Lo importante es que ambos compartan acceso a la misma base de datos o que el sitio web consuma una API expuesta por el emulador. Mantenerlos separados también mejora la seguridad: si el sitio web es comprometido, el núcleo del servidor de juego permanece aislado.

¿Cómo evito que jugadores manipulen datos del personaje desde el sitio web?

Nunca expongas tablas críticas (Character, Inventory, etc.) directamente al usuario. Toda acción que modifique datos del personaje debe pasar por procedimientos almacenados (stored procedures) o una capa de validación en el servidor. Aplica el principio de mínimo privilegio: el usuario de base de datos que usa el sitio web solo debe tener permisos de lectura en tablas de estadísticas y escritura únicamente en tablas de cuenta (registro, contraseña).

¿Qué medidas de seguridad son esenciales antes de abrir el registro al público?

Como mínimo: validación y sanitización de todos los campos del formulario, contraseñas almacenadas con hash (bcrypt o Argon2), protección CSRF en todos los formularios POST, rate limiting en el endpoint de registro para evitar bots, y HTTPS con certificado válido. Opcionalmente, agrega un captcha y verificación por correo electrónico para reducir cuentas falsas.

EQ

Equipo ViciadosMU

Equipe editorial do ViciadosMU — portal de MU Online no ar desde 2003.

Sigue leyendo

Artículos relacionados