Laravel 多租户指南

    本指南将指导你在多租户 Laravel 应用中实现搜索。我们将使用一个客户关系管理 (CRM) 应用的示例,该应用允许用户存储联系人。

    需求

    本指南需要

    提示

    喜欢自托管?请阅读我们的安装指南

    模型和关系

    我们的示例 CRM 是一个多租户应用程序,其中每个用户只能访问其组织拥有的数据。

    从技术角度来看,这意味着

    因此,第一步是定义此类模型及其关系

    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 应用程序中构建搜索界面的组件和帮助程序的一组。如果您喜欢其他 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上查找完整代码。