Sesiones compartidas de Laravel entre dominios
Veamos con qué facilidad se pueden lograrse sesiones compartidas en diferentes dominios en Laravel. Esto es ideal para aplicaciones SaaS donde los inquilinos pueden traer sus propios dominios, como en medium.com o hashnode.com. El código completo está aquí.
Sesiones compartidas
Introducción
Lo primero es lo primero, tenga en cuenta que lo que se entiende por sesiones compartidas es que para un visitante determinado, todos los dominios compartirán una sola sesión. Esto implica que si está en el dominio john.x y tiene Session::put(‘x’, ‘y’)
, podrá Session::get(‘x’)
en alice.y.
Déjeme decirle también que no necesitará ninguna migración, no necesitará rasgos ni nada tonto. Su aplicación seguirá siendo su aplicación. Y creo que el código es bastante válido para cualquier versión de Laravel.
La técnica discutida aquí se puede utilizar para:
- Autenticación centralizada: un usuario está conectado en todas partes una vez que ha iniciado sesión y se desconecta en todas partes una vez que se destruye su sesión (desconectada o expirada). Básicamente,
Auth::user()
es coherente en todos los dominios. Eso puede sonar similar a SSO, OAuth, JWT y otros, pero es muy fácil de implementar y no estamos hablando de compartir solo usuarios aquí, sino sesiones. - Carritos globales: ¿está ejecutando algo como Shopify o BigCartel, pero quiere que el cliente mantenga sus productos en su carrito mientras salta de tienda en tienda o de restaurante en restaurante? ¡Siga leyendo!
- Análisis: mejor seguimiento al considerar a Jane como Jane en diferentes dominios.
- Muchos otros: el único límite es su imaginación.
El uso compartido entre subdominios no se incluye en el alcance de este artículo, ya que se trata simplemente de cambiar el parámetro SESSION_DOMAIN
. El truco simple que mostraré en un minuto es genérico y funciona con cualquier dominio que se ejecute en una sola aplicación Laravel. El uso compartido entre diferentes aplicaciones se mencionará brevemente más adelante.
Entonces, ¿cuál es la magia?
Simple: para un visitante determinado, mantenga el identificador de sesión Session:getId()
, que está oculto en la cookie de sesión, consistente en todos los dominios / aplicaciones. Normalmente, Laravel crearía una sesión para cada dominio y para cada visitante, ya que los navegadores no permiten compartir cookies entre dominios.
Hay dos conceptos clave a los que debemos ceñirnos:
- Un solo dominio debería ser responsable de crear sesiones. Lo llamaremos el dominio del portal. Simplemente puede ser su dominio principal / central.
- Otros dominios, denominados dominios de inquilinos, deben utilizar las sesiones creadas en ese dominio del portal. Aunque aquí estamos hablando de inquilinos, estos dominios pueden ser sitios web completamente diferentes que necesitan sesiones comunes.
Puede haber varias formas de implementar esto. Estaré encantado de escuchar tus ideas. Puede continuar leyendo para ver mi implementación.
El flujo
El código
Tenga en cuenta que estoy almacenando sesiones en la base de datos, y la implementación a continuación hace uso de DatabaseSessionHandler
, pero puede adaptarlo fácilmente a cualquier otro controlador de sesión. Consulte el documento para configurar sesiones de base de datos. El objetivo es evitar que los dominios de inquilinos creen sesiones y permitirles recuperar sesiones del dominio del portal.
.env
PORTAL_DOMAIN=localhost SESSION_DRIVER=shared
config\app.php
<?php return [ // ... 'portal_domain' => env('PORTAL_DOMAIN', 'localhost'), // ... 'providers' => [ // ... App\Providers\SessionServiceProvider::class, ], // ... ];
app\Providers\SessionServiceProvider.php
<?php namespace App\Providers; use Illuminate\Support\Facades\Session; use Illuminate\Support\ServiceProvider; class SessionServiceProvider extends ServiceProvider { public function boot() { Session::extend('shared', function ($app) { $table = $app['config']['session.table']; $lifetime = $app['config']['session.lifetime']; $connection = $app['db']->connection($app['config']['session.connection']); return new \App\Extensions\DatabaseSessionHandler($connection, $table, $lifetime, $app); }); } }
app\Extensions\DatabaseSessionHandler.php
<?php namespace App\Extensions; use Illuminate\Session\DatabaseSessionHandler as BaseDatabaseSessionHandler; class DatabaseSessionHandler extends BaseDatabaseSessionHandler { protected function performInsert($sessionId, $payload) { // if we're not in the portal domain and we're trying to create a session, we redirect to the portal // that way, we are preventing all domains except the portal from creating sessions if (request()->getHost() != config('app.portal_domain')) { // assuming the portal's route is in the same app return redirect()->route('session', ['origin' => request()->fullUrl()])->send(); } parent::performInsert($sessionId, $payload); } }
app\Http\Kernel.php
<?php protected $middlewareGroups = [ 'web' => [ // ... // \Illuminate\Session\Middleware\StartSession::class, \App\Http\Middleware\StartSession::class, // ...
app\Http\Middleware\StartSession.php
<?php namespace App\Http\Middleware; use Illuminate\Http\Request; use Illuminate\Session\Middleware\StartSession as BaseStartSession; class StartSession extends BaseStartSession { public function getSession(Request $request) { if ($request->getHost() == config('app.portal_domain')) { return tap($this->manager->driver(), function (\Illuminate\Contracts\Session\Session $session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); } return tap($this->manager->driver(), function (\Illuminate\Contracts\Session\Session $session) use ($request) { if ($request->query('session')) { $session->setId(\Crypt::decryptString($request->query('session'))); } else { $session->setId($request->cookies->get($session->getName())); } }); } }
routes\web.php
<?php // ... Route::domain(config('app.portal_domain')) ->group(function () { Route::get('/session', function (Request $request) { return redirect()->intended(merge_parameters_to_url($request->get('origin'), ['session' => \Crypt::encryptString(\Session::getId())])); })->name('session'); }); Route::domain('tenant') ->group(function () { Route::get('/', function (Request $request) { return 'I am the tenant. You have a valid session if you\'re reading this.'; })->name('tenant.index'); }); // ... // may put this function somewhere else function merge_parameters_to_url($url, array $parameters = []) { foreach ($parameters as $key => $value) { $value = urlencode($value); $url = preg_replace('/(.*)(?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&'); $url = substr($url, 0, -1); if (strpos($url, '?') === false) { $url = $url . '?' . $key . '=' . $value; } else { $url = $url . '&' . $key . '=' . $value; } } return $url; }
Preocupaciones de seguridad
Como habrá notado por la implementación diferida, estamos pasando el ID de sesión cifrado en la URL cuando se redirige desde el portal al dominio del inquilino. Laravel utiliza internamente el mismo mecanismo de cifrado para ocultar el ID de sesión en la cookie de sesión, pero pasar datos sensibles en las URL puede causar problemas de seguridad según sus requisitos, como se explica aquí y aquí. Puede modificar mi implementación como desee para satisfacer sus demandas. Por ejemplo, en lugar de pasar ID de sesión (encriptados), pasar tokens de uso único y de corta duración que los inquilinos usarán para recuperar las sesiones podría ser un buen punto de partida.
Conclusión
Vimos cómo lograr sesiones compartidas entre dominios dentro de una sola aplicación Laravel. También es posible tener el portal como una aplicación y dejar que otras aplicaciones distintas lo utilicen, pero sus aplicaciones deberán leer las sesiones desde el mismo lugar (por ejemplo, una base de datos común).
Si sabe que un usuario no requerirá una sesión, en el caso de un rastreador, por ejemplo, puede omitir todo el proceso para que no se produzca ninguna redirección. En cuanto a un usuario normal, será redirigido solo una vez hasta que expire su sesión / cookie. Dependiendo de su configuración, puede ser una redirección única hasta que el usuario salga de su navegador, ya que puede ser una redirección única durante un año completo.
También puede verificar cómo Hashnode implementó una idea similar aquí usando Cloudflare Workers (desafortunadamente, redireccionan en cada página), no olvide abrir la pestaña Red de su navegador para verificar las redirecciones.
Tenga en cuenta que en caso de que necesite almacenar datos específicos del dominio, siempre puede hacerlo con cookies o usar un grupo de sesiones paralelas.
Por último, como beneficio adicional, puede usar el siguiente código JS para deshacerse de ese feo parámetro de sesión de consulta:
replaceQueryParameter('session', ''); function replaceQueryParameter(parameter, value) { var queryParams = new URLSearchParams(window.location.search); if (value == '') queryParams.delete(parameter); else queryParams.set(parameter, value); history.replaceState(null, null, queryParams.toString() == '' ? window.location.href.split('?')[0] : '?' + queryParams.toString()); }
Eso es todo amigos. Y recuerde, el mundo es una sesión compartida.