We generally want our app files to be public (profile pictures, product images, and so on). But what happens if we want to restrict the access to them to only logged-in users or users that have a certain role? Today you'll know how.
What we need to do
Before implementing it we have to understand the strategy. Basically, we'll do this:
- Define a new disk. This is for ease of reading (and writing) files
- Define the controller and method that will be responsible for returning the files
- Create the path to the elements of this new disk
- Securing our route
Simple right? Well, it is.
1. Defining a new disk
For this, head to the configuration file that handles it: config/filesystems.php
. It defines the different disks that our app provides.
A "disk" is nothing more than a representation of a driver and location. You can have a disk that points to your app's local disk, AWS S3, or another different provider.
In our case we will create the 'files'
disk. This will be a local disk that will point to the /storage/app/files
directory. In that directory is where we will store our restricted files.
'disks' => [
// ...
'files' => [
'driver' => 'local',
'root' => storage_path('app/files'),
'visibility' => 'private',
],
],
Notice that we set the visibility as
'private'
.
2. Defining our controller
We create our controller:
php artisan make: controller FilesController --invokable
I opted for a single action controller since it will only have one method.
We head over to our newly created controller and implement our logic.
<?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);
}
}
Checking the code you'll see that we do 2 things: First, we check if the file exists. If it doesn't, we will abort the call by returning a 404
response. After this, we will only use the path that was sent in the request to return the file.
Notice that our method expects to receive the $path
parameter.
3. Defining our route
We go to routes/web.php
and add our new route. In my case I will give it the following form:
<?php
use App\Http\Controllers\FilesController;
use Illuminate\Support\Facades\Route;
// Other routes..
Route::get('/files/{path}', FilesController::class); // <--
Now you understand where the $path
variable that reaches the controller comes from. So, this route will capture requests such as: https://laravel8.test/files/my-file.jpg
4. Securing our route
Now we only have to secure the route. To accomplish it we have different options and that will depend on how you'll want to handle it in your app. In this example I will use the 'auth'
middleware to do it. You could also use your ACL system to restrict access to your route or another alternative.
Route::get('/files/{path}', FilesController::class)->middleware('auth');
^^^^^^^^^^^^^^^^^^^^
As a test, I uploaded an image to the secure directory /storage/app/arsenal-goal.jpg
. In addition, I installed Laravel Breeze and created a user to compare the requests with/without an authenticated user.
Result:
On the left, we have the case when trying to access as a logged in user. On the right, you can see that it redirects us to the login, since I tried to access from incognito mode without logging in.
Top comments (0)