使用 Vely 在 Linux 上构建您自己的 SaaS

Vely 使您能够在 Web 应用程序中利用 C 语言的强大功能。
1 位读者喜欢这篇文章。
People working together to build

Opensource.com

Vely 结合了 C 语言的高性能和低占用空间,以及像 PHP 这样易于使用和安全性更高的语言的优点。 它是免费和开源软件,根据 GPLv3 和 LGPL 3 许可(用于库),因此您甚至可以用它构建商业软件。

将 Vely 用于 SaaS

您可以使用 Vely 创建一个多租户 Web 应用程序,您可以在 Internet 上以软件即服务 (SaaS) 的形式运行它。 每个用户都拥有与其他用户完全隔离的数据空间。

在这个示例 Web 应用程序中,用户可以注册笔记本服务以创建笔记,然后查看和删除它们。 它在七个源文件的 310 行代码中演示了多种技术集成。 这些技术包括

  • MariaDB
  • Web 浏览器
  • Apache
  • Unix 套接字

工作原理

以下是从用户角度来看应用程序的工作原理。 代码演练在图像之后。

该应用程序允许用户通过指定电子邮件地址和密码来创建新登录名。 您可以随意设置这些样式,例如使用 CSS

Create a user account

(Sergio Mijatovic, CC BY-SA 4.0)

验证用户电子邮件

Verify the user's email address

(Sergio Mijatovic, CC BY-SA 4.0)

每个用户都使用其唯一的用户名和密码登录

The user logs in

(Sergio Mijatovic, CC BY-SA 4.0)

登录后,用户可以添加笔记

The user can add a note

(Sergio Mijatovic, CC BY-SA 4.0)

用户可以获取笔记列表

User lists notes

(Sergio Mijatovic, CC BY-SA 4.0)

应用程序在删除笔记之前会要求确认

The app asks for confirmation before deleting a note

(Sergio Mijatovic, CC BY-SA 4.0)

用户确认后,笔记将被删除

After confirmation, the note is deleted

(Sergio Mijatovic, CC BY-SA 4.0)

设置先决条件

按照 Vely.dev 上的安装说明进行操作。 这是一个快速的过程,它使用标准的打包工具,例如 DNF、APT、Pacman 或 Zypper。

因为它们是此示例的一部分,所以您必须安装 Apache 作为 Web 服务器,MariaDB 作为数据库。

安装 Vely 后,如果您正在使用 Vim,请打开语法高亮显示

vv -m

获取源代码

此演示 SaaS 应用程序的源代码是 Vely 安装的一部分。 为每个应用程序创建一个单独的源代码目录是一个好主意(您可以随意命名)。 在这种情况下,解压源代码会为您执行此操作

$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS

默认情况下,应用程序名为 multitenant_SaaS,但您可以将其命名为任何名称(如果您这样做,请在所有地方更改它)。

设置应用程序

第一步是创建一个应用程序。 使用 Vely 的 vf 实用程序可以轻松完成此操作

$ sudo vf -i -u $(whoami) multitenant_SaaS

此命令创建一个新的应用程序主目录 (/var/lib/vv/multitenant_SaaS) 并为您执行应用程序设置。 主要意味着在主文件夹中创建各种子目录并分配权限。 在这种情况下,只有当前用户(whoami 的结果)拥有这些目录,权限为 0700,这确保了其他人无法访问这些文件。

设置数据库

在进行任何编码之前,您需要一个地方来存储应用程序使用的信息。 首先,创建一个名为 db_multitenant_SaaS 的 MariaDB 数据库,由用户 vely 拥有,密码为 your_password。 您可以更改任何这些值,但请记住在本示例中在所有地方都更改它们。

以 root 身份登录 MySQL 实用程序

create database if not exists db_multitenant_SaaS;
create user if not exists vely identified by 'your_password';
grant create,alter,drop,select,insert,delete,update on db_multitenant_SaaS.* to vely;

然后在数据库中创建数据库对象(表和记录等)

use db_multitenant_SaaS;
source setup.sql;
exit

将 Vely 连接到数据库

为了让 Vely 知道您的数据库在哪里以及如何登录,请创建一个名为 db_multitenant_SaaS 的数据库配置文件。 (这是源代码中数据库语句使用的名称,因此如果您更改它,请确保在所有地方都更改它。)

Vely 使用原生的 MariaDB 数据库连接,因此您可以指定给定数据库允许您的任何选项

$ echo '[client]
user=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306' > db_multitenant_SaaS

构建应用程序

使用 vv 实用程序来构建应用程序,使用 --db 选项来指定 MariaDB 数据库和数据库配置文件

$ vv -q --db=mariadb:db_multitenant_SaaS

启动应用程序服务器

要启动 Web 应用程序的应用程序服务器,请使用 vf FastCGI 进程管理器。 应用程序服务器使用 Unix 套接字与 Web 服务器通信(创建反向代理)

$ vf -w 3 multitenant_SaaS

这将启动三个守护程序进程来处理传入的请求。 您还可以启动一个自适应服务器,该服务器会增加进程数量以处理更多请求,并在不需要进程时逐渐减少进程数量

$ vf multitenant_SaaS

请参阅 vf 以获取更多选项,以帮助您获得最佳性能。

当您需要停止应用程序服务器时,请使用 -m quit 选项

$ vf -m quit multitenant_SaaS

设置 Web 服务器

这是一个 Web 应用程序,因此该应用程序需要一个 Web 服务器。 此示例通过 Unix 套接字侦听器使用 Apache。

1. 设置 Apache

要将 Apache 配置为反向代理并将您的应用程序连接到它,您需要启用 FastCGI 代理支持,这通常意味着使用 proxyproxy_fcgi 模块。

对于 Fedora 系统(或其他系统,如 Arch),通过在 /etc/httpd/conf/httpd.conf Apache 配置文件中添加(或取消注释)适当的 LoadModule 指令来启用 proxyproxy_fcgi 模块。

对于 Debian、Ubuntu 和类似系统,启用 proxyproxy_fcgi 模块

$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi

对于 OpenSUSE,将这些行添加到 /etc/apache2/httpd.conf 的末尾

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

2. 配置 Apache

现在您必须将代理信息添加到 Apache 配置文件中

ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS

您的配置位置可能因您的 Linux 发行版而异

  • Fedora、CentOS、Mageia 和 Arch:/etc/httpd/conf/httpd.conf
  • Debian、Ubuntu、Mint:/etc/apache2/apache2.conf
  • OpenSUSE:/etc/apache2/httpd.conf

3. 重启

最后,重启 Apache。 在 Fedora 和类似系统以及 Arch Linux 上

$ sudo systemctl restart httpd

在 Debian 和基于 Debian 的系统以及 OpenSUSE 上

$ sudo systemctl restart apache2

设置本地邮件

此示例使用电子邮件作为其功能的一部分。 如果您的服务器已经可以发送电子邮件,则可以跳过此步骤。 否则,您可以使用本地邮件 (myuser@localhost) 进行测试。 为此,请安装 Sendmail。

在 Fedora 和类似系统上

$ sudo dnf install sendmail
$ sudo systemctl start sendmail

在 Debian 系统上(如 Ubuntu)

$ sudo apt install sendmail
$ sudo systemctl start sendmail

当应用程序向本地用户发送电子邮件时,例如 OS_user@localhost,那么您可以通过查看 /var/mail/(“邮件池”)来验证电子邮件是否已发送。

从浏览器访问应用程序服务器

假设您正在本地运行该应用程序,请使用 http://127.0.0.1/multitenant_SaaS?req=notes&action=begin 从 Web 浏览器访问您的应用程序服务器。 如果您在 Internet 上的实时服务器上运行此程序,则可能需要调整防火墙设置以允许 HTTP 流量。

源代码

此示例应用程序包含七个源文件。 您可以自己查看代码(请记住,这些文件中只有 310 行代码),但这里是每个文件的概述。

SQL 设置 (setup.sql)

创建的两个表是

  • users:有关每个用户的信息。 users 表中的每个用户都有其唯一的 ID(userId 列)以及其他信息,例如电子邮件地址以及是否已验证。 还有一个哈希密码。 实际密码永远不会以纯文本(或其他方式)存储; 使用单向哈希来检查密码。
  • notes:用户输入的笔记。 notes 表包含笔记,每个笔记都带有 userId 列,用于说明哪个用户拥有它们。 userId 列的值与 users 表中同名的列匹配。 这样,每条笔记都清楚地属于单个用户。

文件内容

create table if not exists notes (dateOf datetime, noteId bigint auto_increment primary key, userId bigint, note varchar(1000));
create table if not exists users (userId bigint auto_increment primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100));
create unique index if not exists users1 on users (email);

运行时数据 (login.h)

为了正确显示“登录”、“注册”和“注销”链接,您需要一些在应用程序中任何位置都可用的标志。 此外,应用程序使用 Cookie 来维护会话,因此这需要在任何地方都可用,例如,验证会话是否有效。 发送到应用程序的每个请求都以这种方式确认。 仅允许带有可验证 Cookie 的请求。

因此,为此,您有一个 global_request_data 类型 reqdata(请求数据),其中包含 sess_userId(用户 ID)和 sess_id(用户当前会话 ID)。 您还有一些不言自明的标志,可以帮助呈现页面

#ifndef _VV_LOGIN
#define _VV_LOGIN

typedef struct s_reqdata {
    bool displayed_logout; // true if Logout link displayed
    bool is_logged_in; // true if session verified logged-in
    char *sess_userId; // user ID of current session
    char *sess_id; // session ID
} reqdata;

void login_or_signup ();

#endif

会话检查和会话数据 (_before.vely)

Vely 具有 before_request_handler 的概念。 您编写的代码在处理请求的任何其他代码之前执行。 为此,您只需将此代码写入名为 _before.vely 的文件中,其余的将自动处理。

SaaS 应用程序执行的任何操作,例如处理发送到应用程序的请求,都必须经过安全验证。 这样,应用程序就知道调用者是否具有执行操作所需的权限。

权限检查是在请求前处理程序中完成的。 这样,无论您有什么其他代码处理请求,您都已经拥有会话信息。

为了使会话数据(如会话 ID 和用户 ID)在代码中的任何位置都可用,您可以使用 global_request_data。 它只是一个指向内存的通用指针 (void*),任何处理请求的代码都可以访问它。 这非常适合处理会话,如下所示

#include "vely.h"
#include "login.h"

// _before() is a before-request-handler. It always executes before
// any other code that handles a request. It's a good place for any
// kind of request-wide setting or data initialization
void _before() {
    // Output HTTP header
    out-header default
    reqdata *rd; // this is global request data, see login.h
    // allocate memory for global request data, will be automatically deallocated
    // at the end of request
    new-mem rd size sizeof(reqdata)
    // initialize flags
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // set the data we created to be global request data, accessible
    // from any code that handles a request
    set-req data rd
    // check if session exists (based on cookies from the client)
    // this executes before any other request-handling code, making it
    // easier to just have session information ready
    _check_session ();
}

检查会话是否有效 (_check_session.vely)

多租户 SaaS 应用程序中最重要的任务之一是检查(尽快)会话是否有效,方法是检查用户是否已登录。 这是通过从客户端(例如 Web 浏览器)获取会话 ID 和用户 ID Cookie,并将其与存储会话的数据库进行比较来完成的

#include "vely.h"
#include "login.h"


// Check if session is valid
void _check_session () {
    // Get global request data
    reqdata *rd;
    get-req data to rd
    // Get cookies from user browser
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // Check if session ID is correct for given user ID
        char *email;
        run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount
            query-result email to email
        end-query
        if (rcount == 1) {
            // if correct, set logged-in flag
            rd->is_logged_in = true;
            // if Logout link not display, then display it
            if (rd->displayed_logout == false) {
                @Hi <<p-out email>>! <a href="https://open-source.net.cn/?req=login&action=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}

注册、登录、注销 (login.vely)

任何多租户系统的基础是用户注册、登录和注销的能力。 通常,注册涉及验证电子邮件地址; 通常,同一个电子邮件地址用作用户名。 这里就是这种情况。

此处实现了几个子请求,这些子请求是执行该功能所必需的

  • 当注册新用户时,显示 HTML 表单以收集信息。 此 URL 请求签名是 req=login&action=newuser
  • 作为对注册表单的响应,创建一个新用户。 URL 请求签名是 req=login&action=createuserinput-param 信号获取 emailpwd POST 表单字段。 密码值是单向哈希,电子邮件验证令牌被创建为随机五位数字。 这些插入到 users 表中,创建一个新用户。 发送验证电子邮件,并提示用户阅读电子邮件并输入代码。
  • 通过输入发送到该电子邮件的验证码来验证电子邮件。 URL 请求签名是 req=login&action=verify
  • 显示一个登录表单供用户登录。 URL 请求签名是 req=login(例如,action 为空)。
  • 通过验证电子邮件地址(用户名)和密码登录。 URL 请求签名是 req=login&action=login
  • 根据用户请求注销。 URL 请求签名是 req=login&action=logout
  • 应用程序的着陆页。 URL 请求签名是 req=login&action=begin
  • 如果用户当前已登录,请转到应用程序的着陆页。

请参阅下面的示例

#include "vely.h"
#include "login.h"

// Handle session maintenance, login, logout, session verification
// for any multitenant Cloud application
void login () {
    // Get URL input parameter "action"
    input-param action

    // Get global request data, we record session information in it, so it's handy
    reqdata *rd;
    get-req data to rd

    // If session is already established, the only reason why we won't proceed to
    // application home is if we're logging out
    if (rd->is_logged_in) {
        if (strcmp(action, "logout")) {
            _show_home();
            exit-request
        }
    }

    // Application screen to get started. Show links to login or signup and show
    // home screen appropriate for this
    if (!strcmp (action, "begin")) {
        _show_home();
        exit-request

    // Start creating new user. Ask for email and password, then proceed to create user
    // when this form is submitted.
    } else if (!strcmp (action, "newuser")) {
        @Create New User<hr/>
        @<form action="https://open-source.net.cn/?req=login" method="POST">
        @<input name="action" type="hidden" value="createuser">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<input type="submit" value="Sign Up">
        @</form>

    // Verify code sent to email by user. The code must match, thus verifying email address    
    } else if (!strcmp (action, "verify")) {
        input-param code
        input-param email
        // Get verify token based on email
        run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email
            query-result db_verify to define db_verify
            // Compare token recorded in database with what user provided
            if (!strcmp (code, db_verify)) {
                @Your email has been verifed. Please <a href="https://open-source.net.cn/?req=login">Login</a>.
                // If matches, update user info to indicate it's verified
                run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email
                exit-request
            }
        end-query
        @Could not verify the code. Please try <a href="https://open-source.net.cn/?req=login">again</a>.
        exit-request

    // Create user - this runs when user submits form with email and password to create a user     
    } else if (!strcmp (action, "createuser")) {
        input-param email
        input-param pwd
        // create hashed (one-way) password
        hash-string pwd to define hashed_pwd
        // generate random 5 digit string for verify code
        random-string to define verify length 5 number
        // create user: insert email, hashed password, verification token. Current verify status is 0, or not verified
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // if cannot add user, it probably doesn't exist. Either way, we can't proceed.
            login_or_signup();
            @User with this email already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // Create email with verification code and email it to user
            write-string define msg
                @From: vely@vely.dev
                @To: <<p-out email>>
                @Subject: verify your account
                @
                @Your verification code is: <<p-out verify>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st
            if (st != 0) {
                @Could not send email to <<p-out email>>, code is <<p-out verify>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // Inform the user to go check email and enter verification code
            @Please check your email and enter verification code here:
            @<form action="https://open-source.net.cn/?req=login" method="POST">
            @<input name="action" type="hidden" value="verify" size="50" maxlength="50">
            @<input name="email" type="hidden" value="<<p-out email>>">
            @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button type="submit">Verify</button>
            @</form>
        }

    // This runs when logged-in user logs out.    
    } else if (!strcmp (action, "logout")) {
        // Update user table to wipe out session, meaning no such user is logged in
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows
            if (arows == 1) {
                rd->is_logged_in = false; // indicate user not logged in
                @You have been logged out.<hr/>
            }
        }
        _show_home();

    // Login: this runs when user enters user name and password
    } else if (!strcmp (action, "login")) {
        input-param pwd
        input-param email
        // create one-way hash with the intention of comparing with user table - password is NEVER recorded
        hash-string pwd to define hashed_pwd
        // create random 30-long string for session ID
        random-string to rd->sess_id length 30
        // Check if user name and hashed password match
        run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // If match, update user table with session ID
            run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows
            if (arows != 1) {
                @Could not create a session. Please try again. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // Set user ID and session ID as cookies. User's browser will return those to us with every request
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // Display home, make sure session is correct first and set flags
            _check_session();
            _show_home();
            exit-request
        end-query
        @Email or password are not correct. <<.login_or_signup();>><hr/>

    // Login screen, asks user to enter user name and password    
    } else if (!strcmp (action, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<form action="https://open-source.net.cn/?req=login" method="POST">
        @<input name="action" type="hidden" value="login" size="50" maxlength="50">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<button type="submit">Go</button>
        @</form>
    }
}

// Display Login or Sign Up links
void login_or_signup() {
        @<a href="https://open-source.net.cn/?req=login">Login</a> & & <a href="https://open-source.net.cn/?req=login&action=newuser">Sign Up</a><hr/>
}

通用应用程序 (_show_home.vely)

通过本教程,您可以创建任何您想要的多租户 SaaS 应用程序。 上面的多租户处理模块 (login.vely) 调用 _show_home() 函数,该函数可以容纳您的任何代码。 此示例代码显示了 Notes 应用程序,但它可以是任何东西。 _show_home() 函数调用您希望的任何代码,并且是一个通用的多租户应用程序插件

#include "vely.h"

void _show_home() {
    notes();
    exit-request
}

笔记应用程序 (notes.vely)

该应用程序能够添加、列出和删除任何给定的笔记

#include "vely.h"
#include "login.h"

// Notes application in a multitenant Cloud
void notes () {
    // get global request data
    reqdata *rd;
    get-req data to rd
    // If session invalid, display Login or Signup
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // Greet the user
    @<h1>Welcome to Notes!</h1><hr/>
    // If not logged in, exit - this ensures security verification of user's identity
    if (!rd->is_logged_in) {
        exit-request
    }
    // Get URL parameter that tells Notes what to do
    input-param subreq
    // Display actions that Notes can do (add or list notes)
    @<a href="https://open-source.net.cn/?req=notes&subreq=add">Add Note</a> <a href="https://open-source.net.cn/?req=notes&subreq=list">List Notes</a><hr/>

    // List all notes for this user
    if (!strcmp (subreq, "list")) {
        // select notes for this user ONLY
        run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId
            query-result dateOf to define dateOf
            query-result note to define note
            query-result noteId to define noteId
            // change new lines to <br/> with fast cached Regex
            match-regex "\n" in note replace-with "<br/>\n" result define with_breaks status define st cache
            if (st == 0) with_breaks = note; // nothing was found/replaced, just use original
            // Display a note
            @Date: <<p-out dateOf>> (<a href="https://open-source.net.cn/?req=notes&subreq=delete_note_ask&note_id=%3C%3Cp-out%20noteId%3E%3E">delete note</a>)<br/>
            @Note: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }

    // Ask to delete a note
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you sure you want to delete a note? Use Back button to go back, or <a href="https://open-source.net.cn/?req=notes&subreq=delete_note&note_id=%3C%3Cp-out%20note_id%3E%3E">delete note now</a>.
    }

    // Delete a note
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // Delete note
        run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote
        // Inform user of status
        if (arows == 1) {
            @Note deleted
        } else {
            @Could not delete note (<<p-out errnote>>)
        }
    }

    // Add a note
    else if (!strcmp (subreq, "add_note")) {
        // Get URL POST data from note form
        input-param note
        // Insert note under this user's ID
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote
        // Inform user of status
        if (arows == 1) {
            @Note added
        } else {
            @Could not add note (<<p-out errnote>>)
        }
    }

    // Display an HTML form to collect a note, and send it back here (with subreq="add_note" URL param)
    else if (!strcmp (subreq, "add")) {
        @Add New Note
        @<form action="https://open-source.net.cn/?req=notes" method="POST">
        @<input name="subreq" type="hidden" value="add_note">
        @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
        @<button type="submit">Create</button>
        @</form>
    }
}

具有 C 性能的 SaaS

Vely 使您能够在 Web 应用程序中利用 C 语言的强大功能。 多租户 SaaS 应用程序是受益于此的用例的主要示例。 看一下代码示例,编写一些代码,然后试用 Vely。

me as a cartoon
我从十几岁开始编程,并通过获得计算机科学硕士学位(和电气工程学位,这在我需要更换灯泡时很有用)将其作为我的职业,然后在多家公司工作,包括在 Oracle 的核心工程部门担任高级软件工程师超过 10 年。

评论已关闭。

© . All rights reserved.