Restringiendo el acceso a archivos de nuestra app
Por lo general deseamos que nuestros archivos sean públicos. ¿Pero qué sucede si solo queremos que accedan a estos usuarios logueados o que tengan cierto rol? Hoy veremos cómo.
Funcionamiento
Antes de implementarlo tenemos que entender la estrategia. Básicamente, lo que haremos es:
- Definir un nuevo disco. Esto es para mayor facilidad a la hora de leer (y escribir) archivos
- Definir el controlador y método que se encargará de retornar los archivos
- Crear la ruta de acceso a los elementos de este nuevo disco
- Asegurando nuestra ruta
Suena simple cierto? lo es.
1. Definiendo nuevo disco
Para esto, nos dirigiremos al archivo de configuración que maneja este apartado: config/filesystems.php
. En él se definen los distintos discos que provee nuestra app.
Un “disco” no es más que la reprentación de un driver y ubicación. Puedes tener un disco que apunte al disco local de tu app a AWS S3 o a algún otro proveedor distinto.
Entonces, en nuestro caso crearemos el disco "files"
, este será un disco local que apuntará al directorio /storage/app/files
. En este directorio es donde almacenaremos nuestros archivos restringidos.
'disks' => [
// ...
'files' => [
'driver' => 'local',
'root' => storage_path('app/files'),
'visibility' => 'private',
],
],
Si te fijas, configuramos la visibilidad como 'private'
.
2. Definiendo nuestro controlador
Creamos nuestro controlador:
php artisan make:controller FilesController --invokable
En este caso, opté por un controlador invocable puesto que solo tendrá un método.
Nos dirigimos a nuestro recientemente creado controlador e implementamos nuestra lógica.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FilesController extends Controller
{
public function __invoke(Request $request, $path)
{
abort_if(
! Storage::disk('files') ->exists($path),
404,
"The file doesn't exist. Check the path."
);
return Storage::disk('files')->response($path);
}
}
Revisando el código podemos ver que hacemos 2 cosas: Primero revisamos si el archivo existe. En caso no lo haga, abortaremos la llamada retornando 404
.
Luego de esto, solo utilizaremos la ruta que se envió en el request para devolver el archivo.
Notar que nuestro método espera recibir el parámetro
$path
. Esto lo comprenderás mejor cuando definamos nuestra ruta en el siguiente paso.
3. Definiendo nuestra ruta
Nos vamos a nuestro routes/web.php
y añadimos nuestra nueva ruta. En mi caso le daré la siguiente forma:
<?php
use App\Http\Controllers\FilesController;
use Illuminate\Support\Facades\Route;
// Tus otras rutas..
Route::get('/files/{path}', FilesController::class); // <--
Ahora comprende de donde viene la variable $path
que llega al controlador. Entonces, esta ruta capturará requests como por ejemplo: https://laravel8.test/files/mi-archivo.jpg
4. Asegurando nuestra ruta
Ahora solo nos queda aplicarle la seguridad a la ruta. Para esto tenemos muchos caminos. En este ejemplo utilizaré el middleware 'auth'
para hacerlo. En tu caso puedes hacer esto, utilizar tu sistema de roles y permisos para restringir el acceso a tu ruta o alguna opción adicional.
Route::get('/files/{path}', FilesController::class)->middleware('auth');
^^^^^^^^^^^^^^^^^^^^
A modo de prueba, cargué una imagen de mi equipo favorito en mi directorio seguro /storage/app/arsenal-goal.jpg
. Además, para el apartado de seguridad estoy haciendo uso de Laravel Breeze, con el cual creé un usuario y de este modo probaremos los casos para cuando los usuarios están logueados y no logueados.
Resultado:
A la izquierda tenemos el caso cuando se intenta acceder como usuario logueado. En la derecha podemos apreciar que nos redirige al login, puesto que intenté acceder en modo incógnito sin iniciar sesión.