Laravel 5 - простой Поддомен для Мультитенантного приложения

В Laravel 5, чтобы определить маршрут к поддомену и получить к нему доступ, вам нужно будет указать идентификатор / ключевое слово для каждого маршрута.

Пример из официальной документации:

Route::domain('{account}.myapp.com')->group(function () {
  Route::get('user/{id}', function ($account, $id) {
    //
  });
});

И чтобы создать URL-адрес:

route('routeName', ['account' => 'account-one']);

Id будет доступно, как если бы это был параметр маршрута.

Это очень полезно, если вы хотите, чтобы пользователь мог перемещаться между различными поддоменами. Но если что если вы хотите не вводить каждый идентификатор, когда вы определяете маршрут и URL?

Чтобы сделать это, мы можем использовать всю мощь Middleware.

Получить клиента Поддомена

Первоначальные настройки

Создайте таблицу арендаторов с данными арендатора и полем slug. Пуля будет использоваться для сопоставления поддомена.

Middleware

With the setup done, we can start to work on the subdomain part. All we need is a Middleware file and few lines of codes.

First, generate a new Middleware:

php artisan make:middleware CheckTenant

Modify the generated Middleware:

 

Code breakdown

list($subdomain) = explode('.', $request->getHost(), 2);

Extract the tenant slug from the request URL. Here we use a simple explodemethod, you may modify it based on how you define the stored slug pattern.

$tenant = Tenant::whereSlug($subdomain)->first() ?: abort(404);

Try to get the tenant data from database with the slug. If it does not exists, abort the request with 404 not found error.

$request->session()->put('tenant', $tenant);

If the tenant exists, store the tenant data into session to be accessed later. Throughout your application, the tenant data is then accessible with a session(‘tenant’) call using Laravel session helper.

(* Note that unless you modify the config in Laravel, by default the sessions won’t be shared between different subdomain.)

Register Middleware

Next, in app/Http/Kernel.php, register the Middleware to ‘web’ group. The middleware now applies to all routes:

protected $middlewareGroups = [
'web' => [
...,
\App\Http\Middleware\CheckUser::class,
],
...
];

Result

Try to access the web application with existing and non-existing tenant. You should only able to access the tenant with existing record.

By using this method, you don’t have to supply the id / keyword when you define the route / generate URL. When you create a link anywhere in the application using route() helper, it will contain the full subdomain URL.

(If you are developing using localhost, note that you can access it withhttp://{TENANT-SLUG}.localhost:{PORT} subdomain.)


2. (Additional) Tenant’s User

With the simple change above, now we are able to determine which tenant the user is trying to access on every request. We also stored it in session so we can now get the tenant data anywhere with a session(‘tenant’) call.

In most cases, each tenant have own set of Users. Therefore, the next thing we could add is to check if the user can access to the requested tenant.

Setup

Start with the default auth template provided by Laravel.

php artisan make:auth

After that,

  1. Add a tenant_id foreign key to the users table so that users could be granted access to tenant.
  2. Define one-to-many relationships within the Model files. (One tenant has many users)

Middleware

Modify the Middleware by adding some additional logic:

 

Code breakdown

if ($request->user() == null) {
return $next($request);
}

If the user is not logged in yet, we could just pass this request for the Laravel’s auth logic to handle it.

$has_access = $request->user()->tenant == $tenant;
if (!$has_access) {
Auth::logout();
return redirect('/login')->with('no_access', true);
} else {
$request->session()->put('has_access', true);
}

If the user is logged in, we check if the user has access to the requested tenant. If no access, the user is logged out, and redirected to login page with a flash message to inform them.

If everything’s good, we assign a session ‘has_access’ as true, then let it pass.

So far, we are able to check for the tenant and whether the user can access it. But there is still one flaw: with every single request, we need to make at least one database call for the checking. To solve this, we could check the value of ‘has_access’ session at the beginning of the middleware:

$has_access = $request->session()->get('has_access');
if ($has_access) {
return $next($request);
}

If ‘has_access’ is true, it means the user has been granted access previously, so we just let it pass without needing to check again.

Login View

At last, add the following to login view page. As set in Line 48, this flash message would be shown if an user try to access other tenant’s page.

@if (session('no_access'))
<div class="alert alert-danger">
You have no access to this Tenant.
</div>
@endif

Result

Try to access http://{SUBDOMAIN}/login. You can only sign in with user that are assigned to the respective tenant. After you’ve signed in, you should also unable to access another tenant’s data by manually changing the subdomain.


To Study Further

Multi-tenancy is a huge topic by itself and usually requires lots of setup with careful planning, starting from the infrastructure level.

The example shown in this article is useful for multi-tenant Laravel application with simple structure. If your app requires more complex structure, here are some very useful resources I find:

  1. Multi-Tenancy Talk from Laracon US 2017 by Tom Schlick
  2. Tutorials on setting up multi-tenant app with Laravel by Ashok Gelal
  3. Overviews on Domain/Subdomain/Path based multi-tenancy by Ollie Read.