Esame 2019 Ordinaria - Turismo digitale BLE - Soluzione completa | Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
SOLUZIONE ESAME 2019 ORDINARIA — INFORMATICA + SISTEMI E RETI
A cura di Prof. Giagnotti — soluzione completata e ragionata in prima persona

Esame 2019 Ordinaria: Turismo digitale con BLE Beacon — Come l'ho risolta io, pezzo per pezzo

Questa traccia per me è bellissima perché unisce tutto: rete WiFi cittadina, geolocalizzazione indoor che non è banale, DB ben fatto, PHP con sessioni, tariffe che sembrano marketing ma sono logica pura, e VPN. Ve la racconto come l'ho pensata io

📅 2019 Ordinaria 📡 BLE Beacon + WiFi cittadina 🗄️ MySQL + PHP 8.x 🔒 IPSec + OpenVPN
🎙️ Come l'ho affrontata io

Io parto sempre da due domande: 1) come verifico che il tablet sia davvero dentro il POI? 2) come impedisco che uno usi il suo telefono per accedere gratis? Se rispondi a queste due, tutto il resto viene giù. E la risposta per me è BLE + certificato client. Ve lo spiego.

0. Come ho letto io la traccia — i 5 vincoli che non puoi sbagliare

Leggendola io ho estratto questi punti che per me sono fondamentali e che la soluzione originale dava per scontati:

  • Solo minitablet forniti dall'InfoPoint — non smartphone personali. Quindi io devo fare device binding, non basta la password del biglietto. Altrimenti uno fotografa la password e la usa da casa.
  • Contenuti solo su server — niente offline. Quindi streaming HLS, non download. È più giusto così perché protegge i contenuti culturali.
  • Password univoca giornaliera dal biglietto — validità 1 giorno. Io la gestisco con data_emissione = CURDATE() e hash bcrypt, non in chiaro.
  • Solo in prossimità del POI — e qui casca l'asino: GPS indoor non funziona. Io ho scelto BLE.
  • Restituzione InfoPoint con documento o carta di credito — quindi devo tracciare quale tablet è stato dato a chi, ma questo esula dal DB esame e lo metto nel gestionale noleggi.
💡 Le due sfide che ho visto io subito

1) Geolocalizzazione indoor precisa (GPS no, WiFi RSSI poco preciso, QR falsificabile) → BLE beacon. 2) Vincolo dispositivo (solo tablet forniti) → certificato client X.509 sul tablet + WPA2-Enterprise. Così anche se uno ruba la password, senza certificato non entra nella WiFi.

1. Infrastruttura: il mio schema server + WiFi + minitablet

Io ho immaginato così l'infrastruttura, perché secondo me è l'unica che regge centinaia di turisti con video:

  • Data center comunale o cloud PA GDPR-compliant — io lo metterei comunale per governance pubblica e bassa latenza.
  • Web server Apache/Nginx + PHP 8.x — serve pagine dinamiche con sessione.
  • Media server separato — e qui molti lo omettono, ma per me è fondamentale: se 200 turisti guardano video HLS 5 min in 7 lingue contemporaneamente, un solo server va in ginocchio. Io separo web e media, e metto anche CDN locale sugli AP WiFi che fa cache dei video più richiesti.
  • DB Server MySQL/MariaDB + Redis per sessioni — sessioni in RAM, veloci, validazione password biglietto rapida.
  • Rete WiFi cittadina 802.11ac/ax con WPA2-Enterprise + RADIUS — solo tablet con certificato entrano.
  • Minitablet con certificato client + app che legge BLE.
LivelloProtocolloPerché l'ho scelto io
WiFi802.11ac/axBanda per streaming video 5 min, per me minimo
Sicurezza WiFiWPA2-Enterprise 802.1XSolo tablet con cert X.509 — è il device binding che voglio io
AppHTTPS TLS 1.3 + cookie Secure/HttpOnly/SameSite=StrictSessione sicura, no hijacking
VideoHLS su HTTPSStreaming adattivo, chunk, no download
MonitorSNMP v3 + Syslog TLSMonitoro AP e switch, log centralizzati

2. Geolocalizzazione: perché ho scelto BLE Beacon e non GPS

Questa è la parte che nella soluzione originale era spiegata male. Io la completo:

TecPrecisioneIndoorCostoIl mio giudizio
GPS3-5m outdoorNOBassoPer me solo fallback outdoor, dentro un museo non prende
BLE Beacon1-5mSÌ€5-20La mia scelta principale — 1 beacon per POI con UUID univoco
WiFi RSSI3-10mParziale0Fallback secondario, uso AP esistenti
QR CodeUtente dipendenteSìMinPer me no, falsificabile con foto

Come l'ho implementata io: in ogni POI metto un BLE beacon con UUID univoco (es. POI-001-UUID salvato in tabella POI.beacon_uuid). Il tablet fa scanning BLE continuo, quando vede UUID con RSSI > -70dBm (vicino) invia al server id_poi + beacon_uuid. Il server verifica che il beacon_uuid corrisponda davvero a quel POI nel DB. Se non corrisponde, 403. È semplice e per me è più giusto così perché non si può barare fotografando un QR.

🎯 Perché non basta il GPS

Io lo spiego così in classe: GPS dentro una chiesa di pietra di 1 metro di spessore non prende. BLE invece funziona indoor, costa poco, batteria 2 anni, e lo nascondi dietro un quadro. È la tecnologia giusta per musei.

3. Database: l'ER che ho disegnato io e il logico con SCELTA_POI

Qui molti studenti sbagliano le cardinalità. Io ho disegnato così:

  • VISITATORE 1—N BIGLIETTO — un visitatore può avere più biglietti in giorni diversi, ma un biglietto appartiene a un solo visitatore.
  • BIGLIETTO 1—N SCELTA_POI — solo per tariffa intermedia: ogni biglietto può scegliere max 3 POI avanzati. Questa tabella per me è fondamentale, e nella soluzione originale era spiegata male.
  • POI 1—N CONTENUTO — ogni POI ha contenuto base (IT, 1 min) + contenuti avanzati (7 lingue, 5 min).
  • POI 1—N IMMAGINE — galleria fino a 3 immagini per base, più per avanzata.
  • POI 1—1 beacon_uuid — ogni POI ha un beacon univoco, unique constraint.
SQL - SCHEMA LOGICO CHE HO SCRITTO IO (completo)
-- Io metto password_hash bcrypt, non in chiaro, mai! CREATE TABLE VISITATORE ( id_visitatore INT PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, cognome VARCHAR(100) NOT NULL, documento VARCHAR(50) UNIQUE, carta_credito_hash VARCHAR(255) -- tokenizzato, non PAN in chiaro per PCI-DSS ); CREATE TABLE BIGLIETTO ( id_biglietto INT PRIMARY KEY AUTO_INCREMENT, password_hash VARCHAR(255) NOT NULL, -- bcrypt tipo_tariffa ENUM('base','intermedia','piena') NOT NULL, data_emissione DATE NOT NULL, -- validità giornaliera che verifico io id_visitatore INT NOT NULL, FOREIGN KEY (id_visitatore) REFERENCES VISITATORE(id_visitatore) ); CREATE TABLE POI ( id_poi INT PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(150) NOT NULL, tipo ENUM('monumentale','artistico') NOT NULL, indirizzo VARCHAR(200), coord_lat DECIMAL(10,7), coord_lon DECIMAL(10,7), beacon_uuid VARCHAR(36) UNIQUE -- UUID BLE che ho scelto io ); CREATE TABLE CONTENUTO ( id_contenuto INT PRIMARY KEY AUTO_INCREMENT, id_poi INT NOT NULL, tipo_pagina ENUM('base','avanzata') NOT NULL, lingua CHAR(2) NOT NULL, -- IT EN FR DE ES PT ZH url_video VARCHAR(500), durata_min TINYINT, -- 1 o 5 FOREIGN KEY (id_poi) REFERENCES POI(id_poi), UNIQUE (id_poi, tipo_pagina, lingua) ); CREATE TABLE IMMAGINE ( id_immagine INT PRIMARY KEY AUTO_INCREMENT, id_poi INT NOT NULL, url VARCHAR(500) NOT NULL, didascalia_it VARCHAR(500), ordine TINYINT DEFAULT 0, FOREIGN KEY (id_poi) REFERENCES POI(id_poi) ); -- Questa tabella per me è il cuore della tariffa intermedia -- Io la uso così: ogni riga = un POI avanzato scelto + lingua scelta CREATE TABLE SCELTA_POI ( id_biglietto INT NOT NULL, id_poi INT NOT NULL, lingua CHAR(2) NOT NULL DEFAULT 'IT', PRIMARY KEY (id_biglietto, id_poi), FOREIGN KEY (id_biglietto) REFERENCES BIGLIETTO(id_biglietto), FOREIGN KEY (id_poi) REFERENCES POI(id_poi) );
💡 Perché SCELTA_POI è fatta così secondo me

Io salvo lingua dentro SCELTA_POI per intermedia perché la scelta lingua è legata al POI: se scegli Colosseo in EN, resta EN. Non puoi cambiarla dopo aver consumato la scelta, altrimenti aggiri il limite di 3. Per la piena invece lingua in $_SESSION globale, modificabile da menu. Questa distinzione nella soluzione originale mancava, io la aggiungo perché è più giusta.

4. Pagine web: login con password giornaliera + verifica prossimità

Flusso che ho implementato io per tariffa base (che è garantita a tutti):

  1. Login: visitatore inserisce password biglietto → io verifico hash bcrypt + data_emissione = oggi → creo sessione PHP.
  2. Lista POI: mostro tutti i POI con pagina base.
  3. Selezione POI: tablet invia id_poi + beacon_uuid rilevato via BLE.
  4. Verifica prossimità: io controllo su DB che beacon_uuid corrisponda davvero a id_poi. Se no, 403.
  5. Erogo contenuto base con video HLS 1 min IT + max 3 immagini.
PHP - LOGIN CHE HO SCRITTO IO (con validità giornaliera)
<?php session_start(); require 'db.php'; if ($_SERVER['REQUEST_METHOD']==='POST'){ $pwd = trim($_POST['password']); $oggi = date('Y-m-d'); // Io cerco solo biglietti di oggi, così validità giornaliera è automatica $stmt = $pdo->prepare("SELECT id_biglietto, password_hash, tipo_tariffa FROM BIGLIETTO WHERE data_emissione = ?"); $stmt->execute([$oggi]); foreach($stmt->fetchAll() as $row){ if(password_verify($pwd, $row['password_hash'])){ $_SESSION['id_biglietto'] = $row['id_biglietto']; $_SESSION['tipo_tariffa'] = $row['tipo_tariffa']; $_SESSION['data_login'] = $oggi; header('Location: poi_list.php'); exit; } } $errore = 'Password non valida o biglietto scaduto — io verifico data_emissione = oggi'; } ?>
PHP - VERIFICA PROSSIMITÀ BLE CHE HO AGGIUNTO IO
<?php session_start(); require 'db.php'; if($_SESSION['data_login'] !== date('Y-m-d')){ header('Location: login.php'); exit; } $id_poi = intval($_GET['poi'] ?? 0); $beacon_uuid = trim($_GET['beacon'] ?? ''); // Verifica che il beacon rilevato sia davvero di quel POI - è il controllo che voglio io $stmt = $pdo->prepare("SELECT nome FROM POI WHERE id_poi = ? AND beacon_uuid = ?"); $stmt->execute([$id_poi, $beacon_uuid]); if(!$stmt->fetch()){ http_response_code(403); die('Accesso non consentito: non sei vicino a questo POI — io verifico beacon_uuid nel DB'); } // Recupero contenuto base $stmt = $pdo->prepare("SELECT url_video FROM CONTENUTO WHERE id_poi = ? AND tipo_pagina='base' AND lingua='IT'"); $stmt->execute([$id_poi]); $contenuto = $stmt->fetch(); ?>

5. Tariffe: base/intermedia/piena — la logica che ho implementato io

Qui la soluzione originale era carente. Io la completo così:

TariffaBaseAvanzataLingua avanzataDove salvo
BaseTutti i POI, video 1 min IT———
IntermediaTutti base3 POI a scelta, video 5 min1 delle 7, scelta al 1° accesso a quel POISCELTA_POI.lingua — legata al POI, non modificabile dopo
PienaTutti baseTutti i POI, video 5 min1 delle 7, modificabile da menu globale$_SESSION['lingua'] globale

Funzione che ho scritto io per decidere cosa mostrare:

PHP - LOGICA TARIFFE CHE HO COMPLETATO IO
function getTipoPagina($id_biglietto, $id_poi, $pdo){ // Io leggo tariffa dalla sessione $tariffa = $_SESSION['tipo_tariffa'] ?? 'base'; if($tariffa === 'piena') return 'avanzata'; // piena = tutto avanzato if($tariffa === 'intermedia'){ $stmt = $pdo->prepare("SELECT 1 FROM SCELTA_POI WHERE id_biglietto=? AND id_poi=?"); $stmt->execute([$id_biglietto, $id_poi]); if($stmt->fetch()) return 'avanzata'; // scelto tra i 3 } return 'base'; // altrimenti base }

6. Quesito I — Commenti e voti: tabella RECENSIONE e media che ho aggiunto io

Qui aggiungo tabella che mancava di vincoli:

SQL - RECENSIONE CHE HO AGGIUNTO IO
CREATE TABLE RECENSIONE ( id_recensione INT PRIMARY KEY AUTO_INCREMENT, id_poi INT NOT NULL, id_biglietto INT NOT NULL, voto TINYINT NOT NULL CHECK (voto BETWEEN 1 AND 5), commento TEXT, timestamp_ins DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (id_poi) REFERENCES POI(id_poi), FOREIGN KEY (id_biglietto) REFERENCES BIGLIETTO(id_biglietto), UNIQUE (id_poi, id_biglietto) -- una recensione per POI per biglietto, lo metto io per evitare spam );

Pagina media voti: io faccio SELECT AVG(voto), COUNT(*) FROM RECENSIONE WHERE id_poi=? — semplice, efficace, e mostro stelle.

7. Quesito II — Dispositivi personali: perché per me non si può aprire a smartphone

Qui io sono netto: se apro a smartphone personali, perdo controllo. Ecco perché dico no:

  • Sicurezza contenuti: su tablet managed ho certificato client + kiosk mode, non possono fare screenshot o registrare video 5 min facilmente. Su smartphone personale sì.
  • Esperienza: tablet con app dedicata + BLE scanning continuo — su smartphone dovrei fare app per iOS/Android, permessi BLE, batteria, ecc. Complicato.
  • Business model: noleggio tablet è parte del biglietto (documento o carta di credito a garanzia). Se usi il tuo telefono, come giustifico il noleggio?
  • Geolocalizzazione: su mio tablet controllo che app BLE sia attiva. Su telefono personale utente può disattivare Bluetooth e aggirare verifica prossimità.

Se proprio volessi aprire a personali, io metterei: app dedicata con certificate pinning + login con SPID + verifica BLE obbligatoria + watermark su video. Ma per me resta sconsigliato.

8. Quesito III — Sicurezza DB e RBAC: SQL injection, password_hash e ruoli

Qui completo quello che mancava:

  • SQL injection: io uso solo prepared statement $pdo->prepare() con placeholder ?, mai concatenazione stringhe. È la base.
  • Password: mai in chiaro, solo password_hash($pwd, PASSWORD_BCRYPT) e verifica con password_verify(). Validità giornaliera con data_emissione.
  • RBAC che ho aggiunto io: tabella RUOLO (admin, operatore InfoPoint, visitatore) + UTENTE con ruolo. Admin vede tutto, operatore gestisce noleggi, visitatore solo contenuti POI. Sessione con controllo ruolo su ogni pagina.
  • XSS: sempre htmlspecialchars() su output, cookie HttpOnly/Secure/SameSite=Strict.
  • HTTPS ovunque: TLS 1.3, HSTS, no HTTP.

9. Quesito IV — VPN site-to-site + roadwarrior: la differenza che spiego sempre

La traccia chiede VPN tra le due sedi (Roma-Milano) e accesso agenti commerciali. Io la spiego così perché molti confondono:

Site-to-site (sede-sede): due router/firewall che fanno tunnel IPSec permanente in modalità tunnel. IKE Phase 1 (AES-256, SHA-256, DH group 14, cert X.509) + Phase 2 (traffico 192.168.1.0/24 ↔ 192.168.2.0/24, ESP AES-256 HMAC). Trasparente agli utenti: PC Roma vede server Milano come LAN locale. Si alza all'avvio, sempre on.

Remote Access (roadwarrior agenti): ogni agente ha client OpenVPN/IPSec che si connette esplicitamente alla VPN gateway della sede principale da 4G/WiFi hotel. Non è permanente, è on-demand. Autenticazione con certificato + MFA. Una volta dentro, gli assegno IP da pool 10.8.0.0/24 e vede rete interna.

🎯 Differenza che dico sempre io

Site-to-site = gateway parlano tra loro automaticamente, utenti non sanno di usare VPN, è trasparente e permanente. Roadwarrior = ogni utente ha client e deve connettersi esplicitamente, è esplicito e on-demand. Per me è fondamentale dirlo all'orale.

ESEMPIO OPENVPN ROADWARRIOR CHE HO CONFIGURATO IO
server 10.8.0.0 255.255.255.0 ca ca.crt cert server.crt key server.key dh dh.pem cipher AES-256-GCM auth SHA256 # Io metto MFA con plugin auth-user-pass-verify /etc/openvpn/check_mfa.sh via-env client-to-client keepalive 10 120

10. Riepilogo che porto io all'orale

✓BLE + cert client è la mia soluzione per i due vincoli critici: prossimità indoor e solo tablet forniti. GPS no, QR no per me.
✓Media server separato + CDN locale — l'ho aggiunto io perché 200 video HLS su un solo server lo buttano giù.
✓SCELTA_POI con lingua dentro — per intermedia lingua legata alla scelta POI, per piena in sessione globale. È più giusto così.
✓Sicurezza: prepared statement, password_hash bcrypt, htmlspecialchars, HTTPS ovunque, RBAC — lo metto sempre.
✓VPN: site-to-site permanente e trasparente vs roadwarrior on-demand con client — differenza che spiego sempre all'orale.

📚 Come la presento io all'orale

Io parto dai vincoli “solo tablet + solo vicino al POI” e da lì derivo BLE + cert client. Poi dico perché ho separato media server, perché ho messo lingua in SCELTA_POI, perché RAID non è backup (anche se qui non c'è ma lo dico lo stesso), e chiudo con site-to-site vs roadwarrior. È il mio filo logico e per me funziona.