柔晶美网络工作室

柔晶美网络工作室,倾心于web技术的博客站点

关注我 微信公众号

您现在的位置是: 首页 > 博客日记

laravel saas项目设计及注意事项

2021-06-26 admin laravel  2019

SaaS 非常类似于早些年的客户端软件模式,其中客户端(在这种情况下通常是 Web 浏览器)提供对服务器上运行的软件的访问点。SaaS 是消费者最熟悉的云服务形式。SaaS 将管理软件及其部署的任务转移到第三方服务。最熟悉的业务 SaaS 应用程序是客户关系管理应用程序,如 Salesforce,像 Google Apps 这样的生产力软件,以及 Box 和 Dropbox 等存储解决方案的软件。

使用 SaaS 应用程序往往会降低软件所有权成本,因为不需要技术人员来管理软件的安装,管理和升级,同时这也可以降低软件许可的成本。

saas 系统分层大概是:租户识别 > 应用层 > 数据访问层 > 缓存层 > 数据库。

saas 系统说起来很简单,任何系统似乎加个 tenant_id (租户 id) 就变成 saas 系统了。比如原来的用户登录是:

select username,password from users where email='abc@qq.com'

改成:

select username,password from users where email='abc@qq.com' and tenant_id =1;

对于复杂业务的 saas 系统,这样做法非常危险,而且开发效率很低。你想想如果那个程序员写 sql 时候忘了加 “and tenant_id =1” . 结果不堪设想。

比较好做法是在数据库访问层对租户鉴别进行封装,SQL 进行改写:

function getUserInfo($where,tenant_id){
//sql
}

关于多租户的数据隔离方案,目前有这三种方式来实现:

1. 独立数据库

这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。

优点:

为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。

缺点:

增多了数据库的安装数量,随之带来维护成本和购置成本的增加。

这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。

2.共享数据库,隔离数据架构

这是第二种方案,即多个或所有租户共享 Database,但是每个租户一个 Schema(也可叫做一个 user)。

优点:

为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。

缺点:

如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;如果需要跨租户统计数据,存在一定困难。

3.共享数据库,共享数据架构

这是第三种方案,即租户共享同一个 Database、同一个 Schema,但在表中增加 TenantID 多租户的数据字段。这是共享程度最高、隔离级别最低的模式。

优点:

三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。

缺点:

隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;数据备份和恢复最困难,需要逐表逐条备份和还原。如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。

基于 laravel 框架开发 SaaS 系统

利用 laravel 内置的前置中间件对租户请求域名进行判断,进而区分租户。

每个租户的密钥来对应请求域名,后端鉴权通过每个租户不同的密钥和请求域名来制造签名,中心 API 服务器以此鉴权。实现 ACL,防止数据遭窃。

分库分表,实现数据隔离,可以使分库作为本公司 master 数据库的一个备库扩展库,从而实现读写分离,数据安全,查询优化等功能。

为 saas 用户提供服务器前端项目,不提供数据链接,服务器前端项目为可转发请求的服务治理中间件功能的框架(curl,guzzle),租户可以买账号直接使用系统,也可以搭在自己服务器,自行增减配置运维,写前端代码添加个性化的内容比如商标、首页、详情页等等。后端接口维护在本公司的服务器,如果系统有 bug 可以随时处理而不需要去所有租户的服务器重新部署,实现平滑升级。

租户识别方案

比较好做法是通过 url 识别租户。系统是给租户生成一个随机的三级域名,比如 abc.crm.baidu.com. 如果客户想使用自己的域名,可以在 cname 到我们生成的三级域名,并在管理系统里面做绑定。

租户管理系统(计费,订购,定制,充值,催缴)

Saas 系统是必须考虑计费系统和租户控制系统。这个系统需要都是独立设计。比如那个租户购买了那些模块,一个月多少钱。租户可以创建最多的用户数。计费到期邮件提醒等功能。

计费方式一般有两种,周期性计费,类似月租方案,和使用量计费,用多少付多少。 周期性计费比较简单。也可以两者结合起来。

定制化开发

SAAS 的优势在于一套系统多人使用,似乎和定制化开发有冲突。比如 A 客户想要 A 功能,B 客户不想要。但定制化开发是无法避免的,比如 CRM 系统这样复杂的系统,不可能一套系统满足所有公司的要求。定制化开发尽可能分系统,分模块去做。然后通过控制台中配置不同租户订购不同模块,那些模块可以在前端页面上显示。不同的子系统需要分开部署。前端可通过 nginx 根据 url 分发,比如 abc.crm.baidu.com/bi/xxx/xx 这个地址,就分发到 BI 子系统。

还有开发和产品,现有需求一定要分析清楚,不要一上线发现后患无穷。新功能尽量做的独立可以配置。

灰度升级

SAAS 付费企业客户对系统问题都特别敏感。 为了减少升级可能出现问题的影响范围,一般都采用灰度升级策略。如果使用了 url 来区分不同租户,灰度升级配置就会很方便。可以配置 nginx 来根据域名做分发,比如租户 A(aaa.com)到实例 1(版本 1.0),租户 B (bbb.com) 到实例 2 (版本). 当需要域名配置非常多的时候,nginx 配置文档会乱。这块时候可以考虑使用 nignx_lua 来写一些扩展模块。

laravel扩展包

目前laravel saas已有多个成熟的扩展可以使用,比较常用的有stancl/tenancy,我们以这为例来创建基于laravel r saas系统。

安装:

composer require stancl/tenancy
php artisan tenancy:install
php artisan migrate

在config/app.php文件中添加一行,注册服务提供者:

App\Providers\TenancyServiceProvider::class,


现在你需要创建一个租户模型。这个扩展包带有一个默认的租户模型,它包含了许多功能,但它大部分并不是对外开放的,因此,我们需要创建一个自定义模型来使用域名和数据库。像下面这样创建app/Models/Tenant.php文件:

namespace App\Models;

use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;

class Tenant extends BaseTenant implements TenantWithDatabase
{
    use HasDatabase, HasDomains;
}

请注意:如果你的模型文件夹在其他位置,你应该相应调整本教程的代码和命令。

现在我们需要告诉扩展包使用这个自定义模型。打开config/tenancy.php文件,修改下面这一行

'tenant_model' => \App\Models\Tenant::class,


事件

这里的默认设置是开箱即用的,但简短的解释一下会对你更有帮助。 app/Providers 目录中的 TenancyServiceProvider 文件将租户事件映射到监听器。 默认情况下,当一个租户被创建时,它会运行一个 JobPipeline(扩展的这部分比较智能),它确保 CreateDatabase、MigrateDatabase 和可选的其他工作(例如 SeedDatabase)按顺序运行。

中心路由

我们将对 app/Providers/RouteServiceProvider.php 文件进行一些小的更改。 具体来说,我们将确保中心路由仅在中心域上注册。

protected function mapWebRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::middleware('web')
            ->domain($domain)
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));
    }
}

protected function mapApiRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::prefix('api')
            ->domain($domain)
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
}

protected function centralDomains(): array
{
    return config('tenancy.central_domains');
}

从你的RouteServiceProvider的boot()方法手动调用这些方法,而不是调用$this->routes()。

public function boot()
{
    $this->configureRateLimiting();

    $this->mapWebRoutes();
    $this->mapApiRoutes();
}

中心域名

现在我们需要具体的指定一个中心域名。中心域名是为你的 "SAAS应用 "内容服务的域名,例如租户注册的登陆页面。打开config/tenancy.php文件,把它们加进去:

'central_domains' => [
    'saas.test', // Add the ones that you use. I use this one with Laravel Valet.
],

如果您使用 Laravel Sail,则无需更改,默认值就可以了:

'central_domains' => [
    '127.0.0.1',
    'localhost',
],

租户路由

你的租户路由默认会是这样的:

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

这些路由将只能在租户(非中心域名)域名上访问--PreventAccessFromCentralDomains中间件强制执行这一点。

让我们做一个小改动,把数据库中的所有用户都打印出来,这样我们就能真正看到多租户的工作流程。打开文件routes/tenant.php并进行以下修改:

Route::get('/', function () {
    dd(\App\Models\User::all());
    return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});


注意,上面的文件中,一定要注释掉原有的,不然就一直报错,下面的也无法进行:



迁移

为了在租户数据库中拥有用户,让我们把users 表迁移

database/migrations/2014_10_12_000000_create_users_table.php

或类似文件)移到database/migrations/tenant。这将防止该表在中央数据库中被创建,而当租户被创建时,这个表将在租户数据库中被创建--这得感谢我们的事件设置。

创建租户

为了测试,我们将在tinker中创建一个租户--暂时没必要浪费时间创建控制器和视图文件。

$ php artisan tinker
>>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);

现在我们将在每个租户的数据库中创建一个用户:

App\Models\Tenant::all()->runForEach(function () {
    App\Models\User::factory()->create();
});

尝试一下吧

现在我们在浏览器中访问foo.localhost,用之前修改的config/tenancy.php文件中的central_domains的其中一个值替换localhost。我们应该看到一个用户表的打印,我们看到列出了一些用户。如果我们访问bar.localhost,这时我们应该会看到一个不同的用户表的信息。

详细文档:点击查看。

走过的弯路:

1.  三级域名泛解析,如阿里云后台,解析*.yzm.dzbfsj.com到服务器ip,宝塔面板也添加相应的域名即可。

2. php7.3版本报错问题:


解决办法,修改此行为:

public static $controllerNamespace = '';

这样就不报错了。

3. 看文档不仔细导致数据库创建一直失败

首先,这个默认是多数据库模式,env中的数据库用户必须的创建数据库的权限;其次,在测试时,可以直接添加一个dd即可:

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        dd(\App\Models\User::all());
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

文章评论


需要 登录 才能发表评论
热门评论
0条评论

暂时没有评论!