Skip to content

37. 应用练习:第 17 版

Image

此新版本带来了以下更新:

  • 它将移植到 Apache / Windows 服务器上;
  • 为实现此移植,第17版在其[impots / http-servers/ 12]文件夹中包含了所需的所有依赖项。需要提醒的是,之前的版本会从整个[python-flask-2020]项目中的不同文件夹中获取其依赖项;

37.1. 应用程序依赖项的迁移

Image

需要提醒的是,应用程序依赖项的管理是在脚本 [syspath] 中进行的。在上一版本中,该脚本如下所示:


def configure(config: dict) -> dict:
    import os

    # 该文件的目录
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # 根路径
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # 依赖项
    absolute_dependencies = [
        # 项目文件夹
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # 常量、区间
        f"{root_dir}/impots/v05/entities",
        # 记录器,SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # 主脚本文件夹
        script_dir,
        # 配置[database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        # 控制器
        f"{script_dir}/../controllers",
        # 答案HTTP
        f"{script_dir}/../responses",
        # 视图模板
        f"{script_dir}/../models_for_views",
    ]

    # 设置 syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # 生成配置
    return {
        "root_dir": root_dir,
        "script_dir": script_dir
    }

必须重定位所有绝对路径依赖于第 8 行变量 [root_dir] 的依赖项,即第 13-26 行。

新版本中的脚本 [syspath] 将如下所示:


def configure(config: dict) -> dict:
    import os

    # 此文件的目录
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # 依赖项
    absolute_dependencies = [
        # 应用程序实体
        f"{script_dir}/../entities",
        # 层 [dao]
        f"{script_dir}/../layers/dao",
        # 层 [métier]
        f"{script_dir}/../layers/métier",
        # 实用程序
        f"{script_dir}/../utilities",
        # 主脚本文件夹
        script_dir,
        # 配置[database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        # 控制器
        f"{script_dir}/../controllers",
        # 答案HTTP
        f"{script_dir}/../responses",
        # 视图模板
        f"{script_dir}/../models_for_views",
    ]

    # 设置 syspath
    import sys
    # 为其添加项目的绝对依赖项
    for directory in absolute_dependencies:
        # 检查文件夹是否存在
        existe = os.path.exists(directory) and os.path.isdir(directory)
        if not existe:
            # 向开发人员发出警告
            raise BaseException(f"[set_syspath] le dossier du Python Path [{directory}] n'existe pas")
        else:
            # 将文件夹插入 syspath 的开头
            sys.path.insert(0, directory)

    # 生成配置
    return {
        "script_dir": script_dir,
    }
  • 第 8-27 行:所有依赖项现均相对于第 5 行的变量 [script_dir]
  • 第 42-45 行:变量 [root_dir] 已从 syspath 配置中移除;
  • 第 10 行:应用程序的实体位于 [entities] [1] 文件夹中;
  • 第12行:[dao]层位于[layers/dao]和[2]文件夹中;
  • 第 14 行:图层 [métier] 位于文件夹 [layers/métier] [2] 中;
  • 第 16 行:[Logger, SendMail] 实用程序位于文件夹 [utilities] [3] 中;
  • 第29-40行:计算应用程序的Python路径,且不导入模块[myutils]

Image

37.2. 测试

至此,版本 17 应该可以运行。请进行验证。

37.3. 将 Python / Flask 应用程序移植到 Apache / Windows 服务器

37.3.1. 参考

为了将 Flask 应用移植到 Apache / Windows 环境,我不得不上网搜索。以下链接帮助我入门:[https://medium.com/@madumalt/flask-app-deployment-in-windows-apache-server-mod-wsgi-82e1cfeeb2ed];

我参考了该链接中的信息,但Apache服务器的配置除外。对于Apache服务器的配置,我采用了Laragon提供的示例配置。

37.3.2. 安装 Python 模块 mod_wsgi

我们开发的 Python / Flask 应用程序使用了随 Flask 一起提供的 WSGI(Web Server Gateway Interface)[werkzeug] 服务器。 该服务器的介绍请见|此处|。链接[https://www.fullstackpython.com/wsgi-servers.html]介绍了WSGI服务器的运行原理。 存在多种 |WSGI| 服务器。其中之一是运行在 WSGI 模式下的 Apache 服务器。本文采用此方案,是因为我们安装的 Laragon 自带 Apache 服务器。

为了让 Apache 服务器能够托管 Python 应用程序,我们需要安装 Python 模块 [mod_wsgi]。 该模块的安装较为复杂,因为安装过程中会进行 C++ 编译。要成功安装,需要 Microsoft C++ 编译器。一个简单的解决方案是安装最新版本的 Visual Studio Community [https://visualstudio.microsoft.com/fr/vs/community/]。

如果除了 [mod_wsgi] 之外不需要使用 Visual Studio,可以将安装范围限定在 C++ 环境:

Image

安装好 C++ 编译器后,可在终端中安装 [mod_wsgi] 模块:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>SET MOD_WSGI_APACHE_ROOTDIR=C:\MyPrograms\laragon\bin\apache\httpd-2.4.35-win64-VC15

(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>pip install mod_wsgi
Collecting mod_wsgi
  Using cached mod_wsgi-4.7.1.tar.gz (498 kB)
Using legacy setup.py install for mod-wsgi, since package 'wheel' is not installed.
Installing collected packages: mod-wsgi
    Running setup.py install for mod-wsgi ... done
Successfully installed mod-wsgi-4.7.1
  • 第 1 行:设置环境变量 [MOD_WSGI_APACHE_ROOTDIR] 的值。该值即文件系统中 Apache 服务器的路径。此处路径为 [<laragon>\bin\apache\httpd-2.4.35-win64-VC15],其中 <laragon> 是 Laragon 的安装目录。 您可以通过多种方式获取该路径。以下是通过 Laragon 其中一个选项获取的示例:

Image

[1-3] 中,文件 [httpd.conf] 是 Apache 服务器的主配置文件。随后,该文件将在文本编辑器(下文为 Notepad++)中打开:

Image

[2] 中,Apache 的安装目录位于字符串 [conf\httpd.conf] 之前。

让我们回到[mod_wsgi]模块的安装:

  • 第3-9行:安装[mod_wsgi]模块;

37.3.3. 配置 Laragon 的 Apache 服务器

我们将配置 Laragon 的 Apache 服务器。首先从其主配置文件 [httpd.conf] 开始:

Image

我们将光标移至文件末尾 [httpd.conf]



IncludeOptional "C:/MyPrograms/laragon/etc/apache2/alias/*.conf"
IncludeOptional "C:/MyPrograms/laragon/etc/apache2/sites-enabled/*.conf"
Include "C:/MyPrograms/laragon/etc/apache2/httpd-ssl.conf"
Include "C:/MyPrograms/laragon/etc/apache2/mod_php.conf"

# python mod_wsgi
LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
  • 第 8 行已添加到文件 [httpd.conf] 的末尾。它告诉 Apache 服务器,我们刚刚安装的 [mod_wsgi] 模块中的某个元素位于何处;

获取第8行路径的简便方法是在终端中执行以下命令:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>mod_wsgi-express module-config
LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
WSGIPythonHome "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv"

某些文档指出需将第 2 行和第 3 行添加到 [httpd.conf] 文件末尾。但在我的情况下,上述第 3 行会导致错误(缺少 [encodings] 模块)。 因此,该行未被放入文件 [httpd.conf] 中。仅将第 2 行放入其中。Apache 配置文件中可用的 [mod_wsgi] 模块各项参数的含义,请参阅 |此处|。

接下来,我们将启用 Apache 服务器的 HTTPS 协议:

Image

  • [1-4] 中,启用 Apache 服务器的 HTTPS 协议;

现在,我们可以使用 URL 和 [https://serveur/chemin]

要配置 Apache 以托管 Flask 应用程序,上文提到的链接使用了虚拟主机。Laragon 同样支持管理虚拟主机:

Image

  • [1-3] 中,我们要求 Laragon 自动创建虚拟主机;

下一步是使用 Laragon 创建一个 Web 项目:

 
  • [1-3] 中,创建一个空的 PHP 项目;
  • [4-8] 中, Laragon 创建了一个名为 [auto.projet-test.test] 的虚拟站点,该站点由文件 [auto.projet-test.test.conf]、[8] 配置,位于文件夹 [sites-enabled]、[7] 中。 该文件夹位于 [<laragon>\etc\apache2\sites-enabled] 路径下,其中 [laragon] 是 Laragon 的安装目录;

虽然这与当前的工作无关,但您可能出于好奇想看看我们刚刚创建的 [projet-test] 网站:

Image

  • [1-5] 中,已创建了一个空项目。这是一个位于 [<laragon>/www] 文件夹中的 PHP 项目,其中 [laragon] 是 Laragon 的安装文件夹;

现在让我们查看 Laragon 在 [<laragon>\etc\apache2\sites-enabled] 文件夹中生成的 [auto.projet-test.test.conf] 文件:


define ROOT "C:/MyPrograms/laragon/www/projet-test/"
define SITE "projet-test.test"

<VirtualHost *:80> 
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

<VirtualHost *:443>
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>

    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/MyPrograms/laragon/etc/ssl/laragon.key
 
</VirtualHost>
  • 第 1 行:所创建项目在文件系统中的根目录;
  • 第 2 行:虚拟服务器的名称。该服务器的 URL 文件将采用 [http(s)://projet-test.test/chemin] 的格式;
  • 第 4-12 行:虚拟站点的配置,端口为 80(第 4 行),协议为 HTTP;
  • 第14-27行:配置虚拟站点的443端口(第14行)和HTTPS协议;

让我们来看看虚拟服务器是如何工作的。首先启动 Apache 服务器和 PHP:

随后使用浏览器访问 URL [http://projet-test.test/]:

Image

  • [1] 中,即请求的 URL;
  • [2] 中,使用了协议 HTTP;
  • [3] 中,由于项目 [projet-test] 为空,因此获取到其文件夹索引(内容列表),索引为空;

现在请求受保护的 URL 文件 [https://projet-test.test/]:

Image

  • [1-2] 中,我们得到与之前相同的响应,但协议为 HTTPS [1]

创建虚拟服务器 [projet-test.test] 后,在文件 [<windows>/system32/drivers/etc/hosts] 中生成了一条新条目:

Image

# 版权所有 (c) 1993-2009 微软公司。
#
# 这是 Microsoft HOSTS 文件,用于 Windows 版的 TCP/IP。
#
# 此文件包含 IP 地址到主机名的映射。每个
# 条目应独占一行。IP 地址应
# 位于第一列,后跟相应的主机名。
# IP 地址与主机名之间应至少留有一个
# 空格分隔。
#
# 此外,可以在单行中插入注释(如这些),
# 行中,或在机器名后以“#”符号开头插入。
#
# 例如:
#
#       102.54.9    4.97 rhino.         acme.com # 源服务器
#        38.25.    63.10 x             .acme.com # x 客户端主机

# localhost 的域名解析由 DNS 自身处理。
#     127.0.0.1       本地主机
#     ::1             本地主机

127.0.0.1      projet-test.test     #laragon 真神奇!   
  • 第 23 行:名称为 [projet-test.test] 的地址 IP 为 127.0.0.1,即 [localhost](第 20 行)的地址,即本地机器。 因此,当在浏览器中输入 URL [http://projet-test.test/chemin] 时,请求会被发送到 127.0.0.1 的 80 端口。此时,本地机器(localhost)上的 Apache 服务器会进行响应。

有人可能会问,为什么当输入请求 [http://projet-test.test/] 时,Apache 服务器会使用文件 [<laragon>\etc\apache2\sites-enabled\auto.projet-test.test.conf] 中的配置:

Image

要理解这一点,我们需要查看浏览器在发出此请求时向 Apache 服务器发送的内容。让我们使用 Postman 进行测试:

Image

  • [1-3] 中,我们发送的请求为 HTTPS [1]
  • 当请求为 [4] 时,Postman 提示未识别安全证书。 HTTPS协议在Web客户端(此处为Postman)与Apache服务器之间建立加密连接。该加密连接通过客户端与服务器之间交换的证书实现。由服务器发起建立加密连接的对话,向客户端发送安全证书。 要使该证书被客户端接受,它必须经过签名,即从有权签发安全证书的公司购买。当我们启用 Laragon 的 HTTPS 协议时,Laragon 会自行生成安全证书。这种证书被称为自签名证书。 大多数Web客户端在收到自签名证书时会发出警告。Postman在[4]中就是如此。此时,大多数Web客户端会建议禁用对服务器发送的安全证书的验证。Postman在[5]中也提供了这一选项;

我们点击链接 [5] 以禁用安全套接层 (SSL) 验证 SSL。 SSL / TSL(传输层安全协议)是一种安全协议,可在互联网上的两台机器之间建立安全的通信通道。这是 Apache 在此处使用的协议。响应如下:

Image

我们收到的页面与使用传统浏览器时完全相同。现在让我们在 Postman 控制台(Ctrl-Alt-C)中查看客户端与服务器的对话:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
Host: projet-test.test
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.1 200 OK
Date: Fri, 14 Aug 2020 09:19:24 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19 mod_wsgi/4.7.1 Python/3.8
Content-Length: 161
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html;charset=UTF-8
  • 第 6 行:HTTP [Host] 头部指定了 Web 客户端所访问的目标服务器名称。这就是虚拟服务器的原理。 在同一地址(此处为 127.0.0.1)下,一台 Web 服务器可以托管多个名称不同的网站。 HTTP [Host] 头部使客户端能够指定其要连接的服务器(此处为 127.0.0.1);

那么 Apache 会做什么呢?

启动时,Apache会读取[[<laragon>\etc\apache2\sites-enabled]

Image

每个配置文件都定义了一个虚拟服务器。例如在文件 [auto.projet-test.test.conf] 中,有以下这一行:


define ROOT "C:/MyPrograms/laragon/www/projet-test/"
define SITE "projet-test.test"

第 2 行定义了虚拟服务器 [projet-test.test]。文件 [auto.projet-test.test.conf] 即为该虚拟服务器的配置文件。 由于 Apache 服务器在启动时会读取 [<laragon>\etc\apache2\sites-enabled] 文件夹中的所有配置文件,因此它知道存在一个名为 [projet-test.test] 的虚拟服务器。因此,当它从 Postman 客户端收到 HTTPS 的请求时:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
Host: projet-test.test
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

它会识别出该请求是发往虚拟服务器 [projet-test.test](第 6 行),且该服务器确实存在。随后,它将使用虚拟服务器 [projet-test.test] 的配置来响应 Postman 客户端。

37.4. 创建第一个 Apache 虚拟服务器

既然我们已经了解了虚拟服务器的用途以及如何配置它们,接下来我们将创建一个。它将用于运行一个安装在[Apache]文件夹中的Python Flask应用程序,该文件夹属于正在Apache服务器上部署的17版本:

我们已将第 |链接| 段中开发的应用程序——一个日期/时间网络服务——放入文件夹 [http-servers/12/apache/exemple] 中:

Image

服务器 [date_time_server.py] 如下:


# 导入
import os
import sys
import time

# 我们需要将模块文件夹添加到 Python 路径中
# 以便移植到 Windows 版 Apache
sys.path.insert(0"C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages")

from flask import Flask, make_response, render_template

# Flask 应用程序
script_dir = os.path.dirname(os.path.abspath(__file__))
application = Flask(__name__, template_folder=f"{script_dir}")

# 首页 URL
@application.route('/')
def index():
    # 向客户端发送时间
    # time.localtime:自 1970 年 1 月 1 日以来的毫秒数
    # time.strftime 用于格式化时间和日期
    # 日期和时间显示格式
    # d:2位数的日期
    # m:2位数的月份
    # y:2位数的年份
    # H:小时 0,23
    # M:分钟
    # S:秒

    # 当前日期/时间
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    # 生成要发送给客户的文档
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    # 向客户发送响应 HTTP
    response = make_response(document)
    print("response", type(response), response)
    return response

# 仅主线程
if __name__ == '__main__':
    application.config.update(ENV="development", DEBUG=True)
    application.run()

Flask 应用程序通过标识符 [application] 进行引用(第 14、43、44 行)。该名称是必填的。 如果使用其他标识符引用该 Flask 应用程序,应用程序将无法运行,并显示一条错误消息,指出无法找到所请求的 URL。该错误消息并未提供任何关于错误来源的提示。因此,必须对此点保持警惕。

第34行引用的HTML文件内容如下:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
  • [date-time-server]将是托管该应用程序的虚拟主机。它将由文件[<laragon>\etc\apache2\sites-enabled\date-time-server.conf]进行配置(需注意该名称可自定义——Apache会读取[sites-enabled]目录下的所有文件以识别所托管的虚拟站点);

我们首先通过复制文件 [auto.projet-test.test.conf] 来获取该文件,然后对其进行修改。

Image

修改后的 [date-time-server.conf] 文件内容如下:


# 应用程序的 Python-Flask 脚本文件夹
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache/exemple"

# 该文件配置的网站名称
# 此处将命名为 date-time-server
# URL 将采用 http(s)://date-time-server/路径 的形式
define SITE "date-time-server"

# 将 IP 的地址 127.0.0.1 添加到 c:/windows/system32/drivers/etc/hosts 文件中,用于 SITE 网站

# URL HTTP
<VirtualHost *:80>
    # 使用别名 / URL 将采用 http(s)://日期-时间-服务器/路径/... 的形式
    WSGIScriptAlias / "${ROOT}/date_time_server.py"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

# URL 通过 HTTPS 进行加密
<VirtualHost *:443>
    # 使用别名 / URL 将采用 http(s)://日期-时间-服务器/路径/... 的形式
    WSGIScriptAlias / "${ROOT}/date_time_server.py"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>

    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key

</VirtualHost>
  • 第 7 行:为该文件配置的虚拟服务器命名;
  • 第 2 行:为第 14 行和第 27 行中使用的变量 [ROOT] 赋值;
  • 第 14 行和第 27 行:指定当虚拟服务器收到请求时需执行的 Python 脚本路径。此处指定针对服务器 [date-time-server] 的请求由 Python 脚本 [date_time_server.py] 处理。 与文件 [auto.projet-test.test.conf] 的区别在于:前者配置的是 PHP 服务器,而后者配置的是 Python 服务器;
  • 第 14 行和第 27 行:此处的 [WSGIScriptAlias /] 属性表明,[date-time-server] 服务器的根目录将是 [/]。 因此,该应用程序中的 URL 将呈现为 [http(s)://date-time-server/chemin] 的形式;
  • 第 14 行和第 27 行,我们可以为应用程序指定另一个根目录,例如 [WSGIScriptAlias /show]那么应用程序中的 URL 将变为 [http(s)://show/date-time-server/chemin]

我们还需要在文件 [<windows>/system32/drivers/etc/hosts] 中添加一行:

# 版权所有 (c) 1993-2009 微软公司。
#
# 这是 Microsoft HOSTS 文件,由 Windows 版的 Microsoft TCP/IP 使用。
#
# 此文件包含 IP 地址到主机名的映射。每个
# 条目应单独占一行。IP 地址应
# 应置于第一列,后跟相应的主机名。
# IP 地址与主机名之间应至少留有一个
# 空格分隔。
#
# 此外,可以在单行中插入注释(如这些),
# 行中,或紧跟在以“#”符号标示的机器名之后。
#
# 例如:
#
#       102.54.9    4.97 rhino.         acme.com # 源服务器
#        38.25.    63.10 x             .acme.com # x 客户端主机

# localhost 的域名解析由 DNS 自身处理。
#     127.0.0.1       本地主机
#     ::1             本地主机

127.0.0.1    flask-impots-withmysql        # Python 课程——使用 MySQL 开发税务应用
127.0.0.1    flask-impots-withpgres        # Python课程 - 使用PostgreSQL开发的税务应用
127.0.0.1    date-time-server            # Python课程 - 当前时间与日期
127.0.0.1           projet-test.test                #Laragon 真神奇!

在第 25 行添加该内容,将地址 IP [127.0.0.1] 分配给虚拟服务器 [date-time-server]

让我们验证一下。启动 Apache 服务器:

Image

随后,我们使用浏览器访问 URL [https://date-time-server]

Image

  • [1] 中,显示请求的 URL;
  • [3] 中,显示的是服务器的响应;
  • [2] 中,浏览器提示 HTTPS 连接不安全,因为它检测到 Apache 服务器发送的证书是自签名的;

现在在文件 [date-time-server.conf] 中,我们在第 14 行和第 27 行添加别名:


    WSGIScriptAlias /show-date-time "${ROOT}/date_time_server.py"

Apache 服务器不会立即应用此修改。需要重新加载服务器:

Image

随后请求 URL [https://date-time-server/show-date-time]。此时服务器的响应如下:

Image

37.5. 将税款计算应用程序移植到 Apache / Windows 环境

文件 [apache][2] 最初是通过复制文件 [main] 生成的。 为了确保从 [1] 复制到 [2] 的脚本 [syspath.py] 的路径仍然有效,这两个文件必须位于同一层级。 为避免干扰正在运行的 [impots / http-servers/ 12] 应用程序,我们将由 Apache 服务器执行的配置放入 [apache] 中;

Image

  • [2]中的[config][1]中的[config]相同;
  • [2] 中的 [syspath][1] 中的 [syspath] 相同;
  • [2]生成的[main_withmysql]文件,是[1]生成的[main]文件,并进行了以下修改:

主脚本 [main] 接收一个名为 [mysql / pgres] 的参数,该参数指定应使用哪个 SGBD。 脚本 [main_withmysql] 使用 SGBD 和 MySQL:


# 需要一个 mysql 或 pgres 参数
import os
import sys

# 正在使用 MySQL 配置应用程序
import config
config = config.configure({'sgbd'"mysql"})

# 依赖项
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError

第7行,将SGBD设置为MySQL。

  • [main_withpgres] 文件由 [2] 生成,其对应文件为 [main](由 [1] 生成),并进行了以下修改: 它使用了 SGBD 和 PostgreSQL:

# 需要指定 mysql 或 pgres 参数
import os
import sys

# 正在使用 MySQL 配置应用程序
import config
config = config.configure({'sgbd'"pgres"})

# 依赖项
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError

第 7 行,将 SGBD 修改为 PostgreSQL。

完成上述操作后,创建如下脚本 [main_withmysql.wsgi](后缀不重要):

# 该文件的目录
import os
script_dir = os.path.dirname(os.path.abspath(__file__))

# 将其添加到 syspath 中,以便后续能够进行导入
import sys
sys.path.insert(0, script_dir)

# 导入 Flask 应用程序 [app],并将其命名为 [application]
from main_withmysql import app as application

脚本 [main_withmysql.wsgi] 将作为目标,由 Apache 服务器在 WSGI 模式下执行:

  • Apache服务器的目标本可以是脚本[main_withmysql.py],就像之前对脚本[date_time_server.py]所做的那样。但需要对其进行一些修改:
    • 与使用控制台脚本的执行模式不同,在 Apache 环境中,包含目标 [main_withmysql.py] 的文件夹并不属于 Python Path。因此,脚本 [main_withmysql.py] 的第 6 行会引发错误;
    • 需要进行的第二项修改是:在 [main_withmysql] 中,Flask 应用程序应通过标识符 [app] 进行引用。 已知对于 Apache / WSGI,它也必须通过标识符 [application] 进行引用;
  • 与其修改 [main_withmysql.py],不如更改 Apache 的目标。今后将使用上述的 [main_withmysql.wsgi] 脚本:
    • 第1-7行:将脚本所在的文件夹添加到Python路径中。因此,[main_withmysql.py]中的第6行不再引发错误;
    • 第 9-10 行:导入 [main_withmysql.py] 会触发其执行。 此外,通过标识符 [application] 引用 [main_withmysql.py] 中找到的 Flask 应用程序 [app],该标识符是 Apache 在 WSGI 模式下所需的;

对脚本 [main_withpgres.wsgi] 也采用同样的处理方式:


# 该文件的目录
import os
script_dir = os.path.dirname(os.path.abspath(__file__))

# 将其添加到 syspath 中,以便后续能够进行导入
import sys
sys.path.insert(0, script_dir)

# 导入 Flask 应用程序 [app],并将其命名为 [application]
from main_withpgres import app as application

现在我们已经有了Apache服务器的可执行目标。接下来需要创建两个虚拟服务器,每个目标对应一个。

[<laragon>\etc\apache2\sites-enabled] 中,我们创建文件 [flask-impots-withmysql.conf](文件名不限):

Image


# .wsgi 脚本文件夹
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"

# 由该文件配置的网站名称
# 此处将命名为 flask-impots-withmysql
# URL 将采用 http(s)://flask-impots-withmysql/路径 的形式
define SITE "flask-impots-withmysql"

# 将 IP 的地址 127.0.0.1 添加到 c:/windows/system32/drivers/etc/hosts 文件中,用于 SITE 网站

# 在此处填写要使用的 Python 库路径——用逗号分隔
# 此处填写 Python 虚拟环境的库
WSGIPythonPath  "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"

# Python 安装目录 - 仅当安装了多个 Python 版本时才需要
# WSGIPythonHome "C:/Program Files/Python38"

# URL HTTP
<VirtualHost *:80>
    # 使用别名 /,URL 将呈现为 /{prefixe_url}/action/...
    # 使用别名 /impots 时,URL 将采用 /impots/{prefixe_url}/action/... 的形式
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

# URL 通过 HTTPS 进行安全保护
<VirtualHost *:443>
    # 使用别名 /,URL 将采用 /{prefixe_url}/action/... 的形式
    # 别名为 /impots,URL 的格式为 /impots/{prefixe_url}/action/...
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>

    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key

</VirtualHost>
  • 第 2 行:应用程序根目录,即我们创建的 [apache] 文件夹;
  • 第 23、38 行:我们创建的目标 [main_witmysql.wsgi]
  • 第 7 行:虚拟服务器将命名为 [flask-impots-withmysql]
  • 第 13 行:[WSGIPythonPath] 指令用于将文件夹添加到 Python 路径中。在此,Apache 并不知道我们使用了虚拟环境来开发应用程序,也不知道应用程序使用的所有模块都位于该虚拟环境中。因此,在第 13 行,我们添加了包含所用虚拟环境中所有模块的文件夹。 一种方法是将该文件夹复制到文件系统的其他位置,并引用该位置。另一种方法是直接在目标 [main_witmysql.wsgi] 中将该文件夹添加到 Python Path 中(这可能是更好的解决方案);
  • 第 16 行:可向 Apache 指定文件系统中 Python 的安装目录。 通常,该路径位于机器的 PATH 中,因此此行通常是多余的(本例中即如此)。但机器上可能存在多个 Python 安装,且所需的 Python 版本未包含在机器的 PATH 中。此时,此行代码即可解决问题;

同样地,创建一个名为 [flask-impots-withpgres.conf] 的文件:


# .wsgi脚本文件夹
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"

# 由该文件配置的网站名称
# 此处将命名为 flask-impots-withmysql
# URL 将采用 http(s)://flask-impots-withmysql/路径 的形式
define SITE "flask-impots-withpgres"

# 将 IP 的地址 127.0.0.1 添加到 c:/windows/system32/drivers/etc/hosts 文件中,用于 SITE 网站

# 在此处填写要使用的 Python 库路径——用逗号分隔
# 此处填写 Python 虚拟环境的库
WSGIPythonPath  "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"

# Python Home - 仅当安装了多个 Python 版本时才需要
# WSGIPythonHome "C:/Program Files/Python38"

# URL HTTP
<VirtualHost *:80>
    # 使用别名 /,URL 将呈现为 /{prefixe_url}/action/...
    # 使用别名 /impots 时,URL 将采用 /impots/{prefixe_url}/action/... 的形式
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

# URL 通过 HTTPS 进行安全保护
<VirtualHost *:443>
    # 使用别名 /,URL 将采用 /{prefixe_url}/action/... 的形式
    # 别名为 /impots,URL 的格式为 /impots/{prefixe_url}/action/...
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
    DocumentRoot "${ROOT}"
    ServerName ${SITE}
    ServerAlias *.${SITE}
    <Directory "${ROOT}">
        AllowOverride All
        Require all granted
    </Directory>

    SSLEngine on
    SSLCertificateFile      C:/MyPrograms/laragon/etc/ssl/laragon.crt
    SSLCertificateKeyFile   C:/myprograms/laragon/etc/ssl/laragon.key

</VirtualHost>

保存所有这些文件后,启动 Apache 服务器以及 SGBD、MySQL 和 PostgreSQL。 应用程序配置为使用 URL、[/do][with_csrftoken=False] (无令牌 CSRF)配置在 [configs/parameters.py] 中。我们请求 URL [https://flask-impots-withmysql/do]。服务器的响应如下:

Image

现在我们请求 URL 和 [https://flask-impots-pgres/do]。响应如下:

Image

这两个应用程序均运行正常。

现在将参数 [WSGIScriptAlias] 修改为 [flask-impots-withmysql.conf]


# .wsgi脚本文件夹
define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"



# URL HTTP
<VirtualHost *:80>
    # 使用别名 / 时,URL 将呈现为 /{prefixe_url}/action/...
    # 使用别名 /impots 时,URL 将采用 /impots/{prefixe_url}/action/... 的形式
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
    …
</VirtualHost>

# URL 通过 HTTPS 进行安全保护
<VirtualHost *:443>
    # 的别名为 /,而 URL 的格式为 /{prefixe_url}/action/...
    # 别名为 /impots,URL 的形式将为 /impots/{prefixe_url}/action/...
    # 其中 [prefixe_url] 在 parameters.py 中定义
    WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
    …

</VirtualHost>
  • 第11行和第20行,别名 WSI 现改为 [/impots]

我们停止/重启 Apache 服务器,然后请求 URL [https://flask-impots-withmysql/impots/do]。服务器的响应如下:

Image

系统发生崩溃。URL [1] 指出了原因。它本应是 [https://flask-impots-withmysql/impots/do/afficher-vue-authentification]。 缺少别名 WSGI。这是我们应用程序的错误。它能够处理 URL 这种前缀(/do 确实存在)。 人们可能会认为,给应用程序添加前缀 [/impots/do] 就能解决上述问题。但事实并非如此。这样反而会引发其他类型的问题。别名 WSGI 并未表现为 URL 的前缀。

让我们试着弄清楚发生了什么。我们请求了 URL 和 [https://flask-impots-withmysql/impots/do]。 我们原本期望看到身份验证页面。在 [1] 中,上文可见应用程序请求了该页面的显示,但使用的并非正确的 URL。让我们来分析 [https://flask-impots-withmysql/impots/do] 请求的处理路径。

首先执行了以下路由(configs/routes.py):


    # Flask 应用程序的路由
    # 应用程序根目录
    app.add_url_rule(f'{prefix_url}/', methods=['GET'],
                     view_func=routes.index)

在本例中,第 3 行中的路由为 [https://flask-impots-withmysql/impots/do]。 可以看到,该路由去除了 [https://flask-impots-withmysql/impots] 部分,简化为 [/do]。对于 [https://flask-impots-withmysql] 部分,这是正常的,因为服务器名称并未包含在路由中。 但可以看到,它也没有包含别名 WSGI [/impots]。这一点很重要。即使存在别名 WSGI,我们的初始路由仍然有效。

现在我们来看看第 4 行(configs/[index])中的 [index] 函数的作用:


# 应用程序根目录
def index() -> tuple:
    # 重定向至 /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

第 4 行,我们被重定向到 [init_session] 函数中的 URL。在 [configs/routes.py] 中,该函数已被关联到路径 [/do/init-session/html]


    # init-session
    app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
                     view_func=routes.init_session)

第2行,在我们的测试中,[csrftoken_param]是空字符串。该应用程序在此处不支持CSRF令牌。

函数 [init_session] 的定义如下(configs/routes_without_csrftoken):


# init-session
def init_session(type_response: str) -> tuple:
    # 执行与该操作关联的控制器
    return front_controller()

第 4 行,开始处理 [init-session] 操作的字符串。该字符串在 [responses/HtmlResponse] 中以如下方式结束:



# 现在需要生成重定向的URL,如果需要,别忘了CSRF令牌
        if config['parameters']['with_csrftoken']:
            csrf_token = f"/{generate_csrf()}"
        else:
            csrf_token = ""

        # 重定向响应
        return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND

操作 [init-session] 是一个 ADS 操作(Do Something 操作),该操作以重定向到视图结束(第 9 行)。问题就出在这里。 第9行的[redirect]函数并未自动将别名WSGI添加到重定向对象URL中。 上图的屏幕截图展示了这一点。重定向对象 URL 中缺少别名 /impots。

下一个版本将解决 WSGI 别名的问题。