你好开拓者,本日的主题会长一点,我们来谈谈 Laravel 中的多用户,这种架构称为多租户,或多租户。
Tenant来自guest,在这个架构中每个用户都相称于一个guest。
根据我的研究,在项目中运用这种架构紧张有以下三种办法:

无论我读过和见过多少地方,常日都可以归结为这三种方法。
1-利用单独的数据库:在某些情形下,我们乃至可以为每个客户端拥有一个完全的运用程序实例。 如果考虑数据隔离,这将是最安全的,但事实证明,这种方法的掩护、管理和本钱变得昂贵且困难。
2-每个客户具有单独模式的单个数据库实例:在我利用的这种方法中,在我的运用程序中(我稍后将详细先容),它彷佛不太可行,由于对付每个新用户,我必须创建一个数据库租户X。
3-适用于所有租户的单一数据库:这是我选择的方法。 在这种模式中,用户通过 id(“位置键”)链接。 在这里,与其他方法一样,用户显然看不到其他用户的数据。 除非您在另一个用户中定义相同的tenant_id。
关于我的申请
在谈论代码之前,让我们先谈论一下我的运用程序。
在撰写本文时,我的运用程序是利用 Laravel 后端和 Vue.Js 前端开拓的。 Laravel 与 Vue 的通信完备是通过 API 来完成的,即后端与前端很好地分离。
为了在运用程序中进行身份验证(登录),我利用 JTW,当向前端发出登录相应时,令牌和用户将保存在 Vue 的状态管理器 Pinia 中。
这确保了如果用户变动浏览器本地存储中的数据,也不会影响运用程序访问不应访问的数据。
我仍旧不会有注册屏幕,由于我仍旧不想发布该运用程序供所有人利用,因此用户将在银行中手动输入。
废话不多说,让我们开始正题吧……
银行准备事情在此步骤中,我们必须创建三个迁移:
1-创建租户表:
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Schema;return new class extends Migration{ / Run the migrations. / public function up(): void { Schema::create('tenants', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id')->nullable()->index(); $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); }); } / Reverse the migrations. / public function down(): void { Schema::dropIfExists('tenants'); }};
2-变动用户表以具有tenant_id:
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;return new class extends Migration{ / Run the migrations. / public function up(): void { Schema::table('users', function(Blueprint $table) { $table->unsignedBigInteger('tenant_id')->nullable()->after('name')->index(); $table->foreign('tenant_id')->references('id')->on('tenants'); }); } / Reverse the migrations. / public function down(): void { Schema::table('users', function($table) { $table->dropColumn('tenant_id'); }); }};
3-变动其他表以具有tenant_id:
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;return new class extends Migration{ / Run the migrations. / public function up(): void { Schema::table('tabela_1', function(Blueprint $table) { $table->unsignedBigInteger('tenant_id')->nullable()->after('description')->index(); $table->foreign('tenant_id')->references('id')->on('tenants'); }); Schema::table('tabela_2', function(Blueprint $table) { $table->unsignedBigInteger('tenant_id')->nullable()->after('next_installment')->index(); $table->foreign('tenant_id')->references('id')->on('tenants'); }); } / Reverse the migrations. / public function down(): void { Schema::table('tabela_1', function(Blueprint $table) { $table->dropColumn('tenant_id'); }); Schema::table('tabela_2', function(Blueprint $table) { $table->dropColumn('tenant_id'); }); }};
银行准备事情就这样了,我们进入下一步,便是申请
JWT在开始利用多租户代码之前,让我们先看一下我的 JWT 类。
<?phpnamespace App\Tools\Auth;use App\Enums\DateEnum;use App\Models\User;use Firebase\JWT\JWT;use Firebase\JWT\Key;class JwtTools{ public static function createJWT(User $data): string { $payload = array( 'exp' => time() + DateEnum::TREE_HOUR_IN_SECONDS, 'iat' => time(), 'data' => $data ); return JWT::encode($payload, env('SECRET_HASH'), 'HS256'); } public static function validateJWT(string $authorization): bool|object { try { $token = str_replace('Bearer ', '', $authorization); $key = new Key(env('SECRET_HASH'), 'HS256'); return JWT::decode($token, $key); } catch (\Exception $e) { return false; } }}
在 createJWT 中,我基本上是利用 User 模型天生令牌,因此当我再次收到前端令牌并解密它时,我已经有了 User 模型,而不须要查找数据库。
在 validateJWT 中,我们将获取之前天生的令牌并对其进行解密,从而能够返回带有用户模型的 JWT 工具,或者返回 false,以防无法解密。
租户范围类在此类中,我们将在 Laravel 范围中添加对每个表的tenant_id 参数的搜索,而不必在每个数据库查询中连续实行此操作。
它的职责是为每个查询添加where条件,通过研究存储库中的tenant_id进行过滤,从而避免遗忘。
<?phpnamespace App\Scope;use App\Enums\ConfigEnum;use App\Tools\Auth\JwtTools;use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Scope;class TenantScope implements Scope{ public function apply(Builder $builder, Model $model): void { $token = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? ''; $user = JwtTools::validateJWT($token); if (! $user) { return; } $builder->where($model->getTable() . '.tenant_id', $user->data->tenant_id); }}
在这里,我基本上得到了要求令牌,即前面提到的 JWT 天生的要求令牌,如果您设法解密它,我们将得到用户工具,从而得到您的tenant_id。 如果没有,只需退货即可,避免中断。
镇静点,在要求中发送令牌的责任是另一个类的任务,让我们稍后看看。
可承租特质这是卖力将tenant_id注入到我们的模型中的特色。
<?phpnamespace App\Models\Trait;use App\Scope\TenantScope;use App\Models\Tenant;use App\Tools\Auth\JwtTools;use Illuminate\Database\Eloquent\Relations\BelongsTo;trait Tenantable{ protected static function bootTenantable(): void { static::addGlobalScope(new TenantScope); $token = $_SERVER['HTTP_X_MFP_USER_TOKEN'] ?? ''; $user = JwtTools::validateJWT($token); if (! $user) { return; } static::creating(function ($model) use ($user) { $model->tenant_id = $user->data->tenant_id; }); }}
基本上条件与前面的类相同,不同之处在于我们定义利用此特色的模型的tenant_id。
这样,例如,每当我们要进行插入时,我们将始终在模型中定义tenant_id,而无需手动添补此属性。
创建此特色后,我们将其添加到应利用tenant_id 的每个模型中。 只需添加“use Tenantable;” 在每个模型上。
验证 API 要乞降租户模型在 AuthServiceProvider 类中,我们将对 boot 方法进行变动。 我们将在此处验证并逼迫发送 API 令牌和 JWT 令牌。
public function boot(): void{ Auth::viaRequest('authApi', function (Request $request) { $userToken = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? ''; $user = JwtTools::validateJWT($userToken); $apiTokenEncrypted = bcrypt(env('SECRET_HASH')); $apiToken = $request->header('API_TOKEN') ?? ''; return password_verify($apiToken, $apiTokenEncrypted) && $user ? new User() : null; });}
如果此方法返回 null,则将回答 AuthenticateAPI 类中未经身份验证的方法中定义的内容。 我的我是这样离开的:
protected function unauthenticated($request, array $guards): ?string{ $message = 'Tokens obrigatórios ausentes ou inválidos!'; abort(response()->json($message, Response::HTTP_UNAUTHORIZED));}
我们还必须创建一个租户模型。 它内部不须要任何分外的东西,只需创建它即可。
namespace App\Models;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;class Tenant extends Model{ use HasFactory;}
结论
完成所有这些后,多租户该当可以在您的运用程序中运行。 我这里展示的是一个相对超级大略的东西,但我以为缺少评论辩论它的内容。 我什至找到了一个视频(此处),但它不适用于 Vue,由于通信是通过 API 进行的。 为了在这种模式下事情,我必须进行此处提到的变动。
末了它看起来像这样:
我们已经到了末了,再见开拓者。