Laravel 多租户指南
本指南将引导您在多租户 Laravel 应用程序中实现搜索功能。我们将使用一个客户关系管理 (CRM) 应用程序的示例,该应用程序允许用户存储联系人。
要求
本指南需要
- 一个 Laravel 10 应用程序,并配置了 Laravel Scout 以使用
meilisearch
驱动程序 - 正在运行的 Meilisearch 服务器 - 请参阅我们的 快速入门
- 一个搜索 API 密钥 - 在您的 Meilisearch 仪表盘中可用
- 一个搜索 API 密钥 UID - 使用 密钥端点检索它
提示
喜欢自托管?阅读我们的 安装指南。
模型 & 关系
我们的示例 CRM 是一个多租户应用程序,每个用户只能访问属于其组织的数据。
在技术层面上,这意味着
- 一个
User
模型,属于一个Organization
- 一个
Contact
模型,属于一个Organization
(只能由来自同一组织的用户访问) - 一个
Organization
模型,有多个User
和多个Contact
考虑到这一点,第一步是定义这些模型及其关系
在 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
都可以搜索属于所有 Organization
的数据。为了防止这种情况发生,您需要为每个组织生成一个租户令牌。然后,您可以使用此令牌对 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 上查找完整代码。