Laravel 多租戶指南
本指南將引導您在多租戶 Laravel 應用程式中實作搜尋功能。我們將使用客戶關係管理 (CRM) 應用程式的範例,該應用程式允許使用者儲存聯絡人。
需求
本指南需要
- 一個 Laravel 10 應用程式,其中 Laravel Scout 已設定為使用
meilisearch
驅動程式 - 一個正在執行的 Meilisearch 伺服器 — 請參閱我們的 快速入門
- 一個搜尋 API 金鑰 — 可在您的 Meilisearch 儀表板中找到
- 一個搜尋 API 金鑰 UID — 使用 金鑰端點 擷取
提示
偏好自我託管?請閱讀我們的安裝指南。
模型 & 關係
我們的範例 CRM 是一個多租戶應用程式,其中每個使用者只能存取屬於其組織的資料。
在技術層面,這表示
- 一個屬於
Organization
的User
模型 - 一個屬於
Organization
的Contact
模型 (只能由來自同一組織的使用者存取) - 一個擁有許多
User
和許多Contact
的Organization
模型
考慮到這一點,第一步是定義這些模型及其關係
在 app/Models/Contact.php
中
<?php
namespace App\Models;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Contact extends Model
{
use Searchable;
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
}
在 app/Models/User.php
中
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
}
以及在 app/Models/Organization.php
中
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Organization extends Model
{
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
}
現在您已對應用程式的模型及其關係有了紮實的了解,您可以開始產生租戶權杖。
產生租戶權杖
目前,所有 User
都可以搜尋屬於所有 Organizations
的資料。為了防止這種情況發生,您需要為每個組織產生一個租戶權杖。然後,您可以使用此權杖來驗證對 Meilisearch 的請求,並確保使用者只能存取其組織的資料。同一 Organization
內的所有 User
將共用相同的權杖。
在本指南中,您將在從資料庫擷取組織時產生權杖。如果組織沒有權杖,您將產生一個權杖並將其儲存在 meilisearch_token
屬性中。
更新 app/Models/Organization.php
<?php
namespace App\Models;
use DateTime;
use Laravel\Scout\EngineManager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;
class Organization extends Model
{
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
protected static function booted()
{
static::retrieved(function (Organization $organization) {
// You may want to add some logic to skip generating tokens in certain environments
if (env('SCOUT_DRIVER') === 'array' && env('APP_ENV') === 'testing') {
$organization->meilisearch_token = 'fake-tenant-token';
return;
}
// Early return if the organization already has a token
if ($organization->meilisearch_token) {
Log::debug('Organization ' . $organization->id . ': already has a token');
return;
}
Log::debug('Generating tenant token for organization ID: ' . $organization->id);
// The object belows is used to generate a tenant token that:
// • applies to all indexes
// • filters only documents where `organization_id` is equal to this org ID
$searchRules = (object) [
'*' => (object) [
'filter' => 'organization_id = ' . $organization->id,
]
];
// Replace with your own Search API key and API key UID
$meiliApiKey = env('MEILISEARCH_SEARCH_KEY');
$meiliApiKeyUid = env('MEILISEARCH_SEARCH_KEY_UID');
// Generate the token
$token = self::generateMeiliTenantToken($meiliApiKeyUid, $searchRules, $meiliApiKey);
// Save the token in the database
$organization->meilisearch_token = $token;
$organization->save();
});
}
protected static function generateMeiliTenantToken($meiliApiKeyUid, $searchRules, $meiliApiKey)
{
$meilisearch = resolve(EngineManager::class)->engine();
return $meilisearch->generateTenantToken(
$meiliApiKeyUid,
$searchRules,
[
'apiKey' => $meiliApiKey,
'expiresAt' => new DateTime('2030-12-31'),
]
);
}
}
現在 Organization
模型正在產生租戶權杖,您需要向前端提供這些權杖,以便它可以安全地存取 Meilisearch。
將租戶權杖與 Laravel Blade 搭配使用
使用 檢視組合器向檢視提供您的搜尋權杖。這樣,您可以確保權杖在所有檢視中都可用,而無需手動傳遞它。
提示
如果您願意,可以使用 with
方法將權杖手動傳遞給每個檢視。
建立新的 app/View/Composers/AuthComposer.php
檔案
<?php
namespace App\View\Composers;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Vite;
use Illuminate\View\View;
class AuthComposer
{
/**
* Create a new profile composer.
*/
public function __construct() {}
/**
* Bind data to the view.
*/
public function compose(View $view): void
{
$user = Auth::user();
$view->with([
'meilisearchToken' => $user->organization->meilisearch_token,
]);
}
}
現在,在 AppServiceProvider
中註冊此檢視組合器
<?php
namespace App\Providers;
use App\View\Composers\AuthComposer;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Use this view composer in all views
View::composer('*', AuthComposer::class);
}
}
就這樣!現在所有檢視都可以存取 meilisearchToken
變數。您可以在前端中使用此變數。
建構搜尋 UI
本指南使用 Vue InstantSearch 來建構您的搜尋介面。Vue InstantSearch 是一組元件和輔助程式,用於在 Vue 應用程式中建構搜尋 UI。如果您偏好其他 JavaScript 風格,請查看我們的其他 前端整合。
首先,安裝依賴項
npm install vue-instantsearch @meilisearch/instant-meilisearch
現在,建立一個使用 Vue InstantSearch 的 Vue 應用程式。開啟一個新的 resources/js/vue-app.js
檔案
import { createApp } from 'vue'
import InstantSearch from 'vue-instantsearch/vue3/es'
import Meilisearch from './components/Meilisearch.vue'
const app = createApp({
components: {
Meilisearch
}
})
app.use(InstantSearch)
app.mount('#vue-app')
此檔案會初始化您的 Vue 應用程式,並將其設定為使用 Vue InstantSearch。它也會註冊您接下來將建立的 Meilisearch
元件。
Meilisearch
元件負責初始化 Vue Instantsearch 客戶端。它使用 @meilisearch/instant-meilisearch
套件來建立與 Instantsearch 相容的搜尋客戶端。
在 resources/js/components/Meilisearch.vue
中建立它
<script setup lang="ts">
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch"
const props = defineProps<{
host: string,
apiKey: string,
indexName: string,
}>()
const { searchClient } = instantMeiliSearch(props.host, props.apiKey)
</script>
<template>
<ais-instant-search :search-client="searchClient" :index-name="props.indexName">
<!-- Slots allow you to render content inside this component, e.g. search results -->
<slot name="default"></slot>
</ais-instant-search>
</template>
您可以在任何 Blade 視圖中使用 Meilisearch
元件,並提供租戶令牌。別忘了加入 @vite
指令,以便將 Vue 應用程式包含在您的視圖中。
<!-- resources/views/contacts/index.blade.php -->
<div id="vue-app">
<meilisearch index-name="contacts" api-key="{{ $meilisearchToken }}" host="https://edge.meilisearch.com">
</meilisearch>
</div>
@push('scripts')
@vite('resources/js/vue-app.js')
@endpush
瞧!您現在有一個安全且多租戶的搜尋介面。使用者只能存取他們組織的資料,而且您可以確信其他租戶的資料是安全的。
結論
在本指南中,您看到了如何在 Laravel 應用程式中實作安全的多租戶搜尋。接著,您為每個組織產生了租戶令牌,並使用它們來保護對 Meilisearch 的存取。您還使用 Vue InstantSearch 建構了一個搜尋介面,並向其提供了租戶令牌。
本指南中的所有程式碼都是我們在 Laravel CRM 範例應用程式中實作的簡化範例。在 GitHub 上找到完整程式碼。