使用 Apache 在 Linux 上托管 ASP.NET Core

作者:Shayne Boyer

使用本指南,了解如何在 CentOS 7 上将 Apache 设置为反向代理服务器,以将 HTTP 流量重定向到在 Kestrel 服务器上运行的 ASP.NET Core Web 应用。 mod_proxy extension 和相关模块可创建服务器的反向代理。

注意

本文引用了 CentOS,这是一个接近生命周期结束 (EOL) 状态的 Linux 发行版。 请相应地考虑你的使用和规划。 有关详细信息,请参阅 CentOS 生命周期结束指南

先决条件

  • 运行 CentOS 7 的服务器,使用具有 sudo 特权的标准用户帐户。
  • 在服务器上安装 .NET Core 运行时。
    1. 访问下载 .NET Core 页面
    2. 选择最新的 .NET Core 非预览版。
    3. 在“运行应用”-“运行时”下的表格中,下载最新的非预览版运行时 。
    4. 选择 Linux 包管理器说明链接,然后按照 CentOS 说明进行操作 。
  • 一个现有 ASP.NET Core 应用。

升级共享框架后,可随时重启服务器托管的 ASP.NET Core 应用。

通过应用发布和复制

配置应用以进行依赖框架的部署

如果应用在开发环境中本地运行,并且未被服务器配置为建立安全 HTTPS 连接,则采用以下任一方法:

  • 配置应用,以处理安全的本地连接。 有关详细信息,请参阅 HTTPS 配置部分。

  • 将应用配置为在不安全的终结点运行:

    • 在开发环境 (Program.cs) 中停用 HTTPS 重定向中间件:

      if (!app.Environment.IsDevelopment())
      {
          app.UseHttpsRedirection();
      }
      

      有关详细信息,请参阅在 ASP.NET Core 中使用多个环境

    • Properties/launchSettings.json 文件的 applicationUrl 属性中删除 https://localhost:5001(如果存在)。

有关按环境配置的详细信息,请参阅在 ASP.NET Core 中使用多个环境

在开发环境中运行 dotnet publish,将应用打包到可在服务器上运行的目录(例如 bin/Release/{TARGET FRAMEWORK MONIKER}/publish,其中 {TARGET FRAMEWORK MONIKER} 占位符表示目标框架名字对象 (TFM))中:

dotnet publish --configuration Release

如果不希望维护服务器上的 .NET Core 运行时,还可将应用发布为独立部署

使用集成到组织工作流的工具(例如 SCP、SFTP)将 ASP.NET Core 应用复制到服务器。 通常可在 var 目录(例如 var/www/helloapp)下找到 Web 应用 。

注意

在生产部署方案中,持续集成工作流会执行发布应用并将资产复制到服务器的工作。

配置代理服务器

反向代理是为动态 Web 应用提供服务的常见设置。 反向代理终止 HTTP 请求,并将其转发到 ASP.NET 应用。

代理服务器将客户端请求转发到另一个服务器,而不是自身实现这些请求。 反向代理转发到固定的目标,通常代表任意客户端。 在本指南中,Apache 被配置为反向代理,在 Kestrel 为 ASP.NET Core 应用提供服务的同一服务器上运行。

由于请求是通过反向代理转接的,因此使用 Microsoft.AspNetCore.HttpOverrides 包中的转接头中间件。 此中间件使用 X-Forwarded-Proto 标头来更新 Request.Scheme,使重定向 URI 和其他安全策略能够正常工作。

调用转接头中间件后,必须放置依赖于该架构的组件,例如身份验证、链接生成、重定向和地理位置。

转接头中间件应在其他中间件之前运行。 此顺序可确保依赖于转接头信息的中间件可以使用标头值进行处理。 若要在诊断和错误处理中间件后运行转接头中间件,请参阅转接头中间件顺序

调用其他中间件之前,请先在 Startup.Configure 的基础上调用 UseForwardedHeaders 方法。 配置中间件以转接 X-Forwarded-ForX-Forwarded-Proto 标头。

Microsoft.AspNetCore.HttpOverrides 命名空间添加到文件的顶部:

using Microsoft.AspNetCore.HttpOverrides;

在应用处理管道中:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

如果没有为中间件指定 ForwardedHeadersOptions,则要转接的默认标头为 None

默认情况下,在环回地址 (127.0.0.0/8, [::1])(包括标准 localhost 地址 (127.0.0.1))上运行的代理受信任。 如果组织内的其他受信任代理或网络处理 Internet 与 Web 服务器之间的请求,请使用 ForwardedHeadersOptions 将其添加到 KnownProxiesKnownNetworks 的列表。 以下示例会将 IP 地址为 10.0.0.100 的受信任代理服务器添加到 Startup.ConfigureServices 中的转接头中间件 KnownProxies

System.Net 命名空间添加到文件的顶部:

using System.Net;

使用以下服务注册:

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

有关详细信息,请参阅配置 ASP.NET Core 以使用代理服务器和负载均衡器

安装 Apache

将 CentOS 包更新为其最新稳定版本:

sudo yum update -y

使用单个 yum 命令在 CentOS 上安装 Apache Web 服务器:

sudo yum -y install httpd mod_ssl

运行该命令后的示例输出:

Downloading packages:
httpd-2.4.6-40.el7.centos.4.x86_64.rpm               | 2.7 MB  00:00:01
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Installing : httpd-2.4.6-40.el7.centos.4.x86_64      1/1 
Verifying  : httpd-2.4.6-40.el7.centos.4.x86_64      1/1 

Installed:
httpd.x86_64 0:2.4.6-40.el7.centos.4

Complete!

注意

在此示例中,输出反映了 httpd.86_64,因为 CentOS 7 版本是 64 位的。 若要验证 Apache 的安装位置,请从命令提示符运行 whereis httpd

配置 Apache

Apache 的配置文件位于 /etc/httpd/conf.d/ 目录内。 在 Ubuntu 上的 Apache 中,所有虚拟主机配置文件都存储在 /etc/apache2/sites-available 中。 除了 /etc/httpd/conf.modules.d/ 中的模块配置文件外(其中包含加载模块所需的任何配置文件),将对任何带 .conf 扩展名的文件按字母顺序进行处理。

为应用创建名为 helloapp.conf 的配置文件:

<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}s
</VirtualHost>

<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:5000/
    ProxyPassReverse / http://127.0.0.1:5000/
    ServerName www.example.com
    ServerAlias *.example.com
    ErrorLog ${APACHE_LOG_DIR}/helloapp-error.log
    CustomLog ${APACHE_LOG_DIR}/helloapp-access.log common
</VirtualHost>

注意:2.4.6 之前的 Apache 版本不需要 RequestHeader set 包含尾随的 s

<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

有关详细信息,请参阅 Apache 模块 mod_headers 中的 %{VARNAME}s

VirtualHost 块可以在服务器上的一个或多个文件中多次出现。 在前面的配置文件中,Apache 接受端口 80 上的公共流量。 正在向域 www.example.com 提供服务,*.example.com 别名解析为同一网站。 有关详细信息,请参阅基于名称的虚拟主机支持。 请求会通过代理从根位置转到 127.0.0.1 处的服务器的端口 5000。 对于双向通信,需要 ProxyPassProxyPassReverse。 要更改 Kestrel 的 IP/端口,请参阅 Kestrel:终结点配置

VirtualHost 块可以在服务器上的一个或多个文件中多次出现。 在前面的配置文件中,Apache 接受端口 80 上的公共流量。 正在向域 www.example.com 提供服务,*.example.com 别名解析为同一网站。 有关详细信息,请参阅基于名称的虚拟主机支持。 请求会通过代理从根位置转到 127.0.0.1 处的服务器的端口 5000。 对于双向通信,需要 ProxyPassProxyPassReverse。 要更改 Kestrel 的 IP/端口,请参阅 Kestrel:终结点配置

创建一个指向 /etc/apache2/sites-enabled 目录的符号链接,供 Apache 在启动期间读取:

sudo ln -s /etc/apache2/sites-available/helloapp.conf /etc/apache2/sites-enabled/

警告

未能指定 VirtualHost 块中的正确 ServerName 指令会公开应用的安全漏洞。 如果可控制整个父域(区别于易受攻击的 *.com),则子域通配符绑定(例如,*.example.com)不具有此安全风险。 有关详细信息,请参阅 RFC 9110:HTTP 语义(第 7.2 节:托管和授权)

可以使用 ErrorLogCustomLog 指令配置每个 VirtualHost 的日志记录。 ErrorLog 是服务器记录错误的位置,CustomLog 则设置文件名和日志文件的格式。 在这种情况下,这是记录请求信息的位置。 每个请求将各占一行。

保存文件,并测试配置。 如果一切正常,响应应为 Syntax [OK]

sudo apachectl configtest

重新启动 Apache:

sudo systemctl restart httpd
sudo systemctl enable httpd

有关标头指令值的详细信息,请参阅 Apache 模块 mod_headers

监视应用

Apache 现在已设置为将对 http://localhost:80 发起的请求转发到在 http://127.0.0.1:5000 中的 Kestrel 上运行的 ASP.NET Core 应用。 但是,未将 Apache 设置为管理 Kestrel 进程。 使用 systemd ,并创建服务文件以启动和监视基础 Web 应用。 systemd 是一个 init 系统,可以提供用于启动、停止和管理进程的许多强大的功能。

创建服务文件

创建服务定义文件:

sudo nano /etc/systemd/system/kestrel-helloapp.service

应用的一个示例服务文件:

[Unit]
Description=Example .NET Web API App running on CentOS 7

[Service]
WorkingDirectory=/var/www/helloapp
ExecStart=/usr/local/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=apache
Environment=ASPNETCORE_ENVIRONMENT=Production 

[Install]
WantedBy=multi-user.target

注意:设置发行版的 local/bin 文件夹。 某些版本的 Ubuntu 需要 ExecStart=/usr/bin/dotnet

在前面的示例中,管理服务的用户由 User 选项指定。 用户 (apache) 必须存在并且拥有正确应用文件的所有权。

使用 TimeoutStopSec 配置在收到初始中断信号后等待应用程序关闭的持续时间。 如果应用程序在此时间段内未关闭,则将发出 SIGKILL 以终止该应用程序。 提供作为无单位秒数的值(例如,150)、时间跨度值(例如,2min 30s)或 infinity 以禁用超时。 TimeoutStopSec 默认为管理器配置文件(systemd-system.confsystem.conf.dsystemd-user.confuser.conf.d)中 DefaultTimeoutStopSec 的值。 大多数分发版的默认超时时间为 90 秒。

# The default value is 90 seconds for most distributions.
TimeoutStopSec=90

必须转义某些值(例如,SQL 连接字符串)以供配置提供程序读取环境变量。 使用以下命令生成适当的转义值以供在配置文件中使用:

systemd-escape "<value-to-escape>"

环境变量名不支持冒号 (:) 分隔符。 使用双下划线 (__) 代替冒号。 环境变量读入配置时,环境变量配置提供程序将双下划线转换为冒号。 以下示例中,连接字符串密钥 ConnectionStrings:DefaultConnectionConnectionStrings__DefaultConnection 形式设置到服务定义文件中:

环境变量名不支持冒号 (:) 分隔符。 使用双下划线 (__) 代替冒号。 环境变量读入配置时,环境变量配置提供程序将双下划线转换为冒号。 以下示例中,连接字符串密钥 ConnectionStrings:DefaultConnectionConnectionStrings__DefaultConnection 形式设置到服务定义文件中:

Environment=ConnectionStrings__DefaultConnection={Connection String}

保存该文件并启用该服务:

sudo systemctl enable kestrel-helloapp.service

启动该服务,并确认它正在运行:

sudo systemctl start kestrel-helloapp.service
sudo systemctl status kestrel-helloapp.service

◝ kestrel-helloapp.service - Example .NET Web API App running on CentOS 7
    Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
    Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
    CGroup: /system.slice/kestrel-helloapp.service
            └─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.dll

在配置了反向代理并通过 systemd 管理 Kestrel 后,Web 应用现已完全配置,并能在本地计算机上的浏览器中从 http://localhost 进行访问。 检查响应标头,服务器标头表示 ASP.NET Core 应用由 Kestrel 提供服务:

HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked

查看日志

由于使用 Kestrel 的 Web 应用是通过 systemd 进行管理的,因此事件和进程将记录到集中日志。 但是,此日志包含由 systemd 管理的所有服务和进程的条目。 若要查看特定于 kestrel-helloapp.service 的项,请使用以下命令:

sudo journalctl -fu kestrel-helloapp.service

若要进行时间筛选,请使用命令指定时间选项。 例如,使用 --since today 筛选出当天或 --until 1 hour ago 来查看前一小时的条目。 有关详细信息,请参阅 journalctl 手册页

sudo journalctl -fu kestrel-helloapp.service --since "2016-10-18" --until "2016-10-18 04:00"

数据保护

ASP.NET Core 数据保护堆栈由多个 ASP.NET Core 中间件(包括 cookie 中间件等身份验证中间件)和跨站点请求伪造 (CSRF) 保护使用。 即使用户代码不调用数据保护 API,也应该配置数据保护,以创建持久的加密密钥存储。 如果不配置数据保护,则密钥存储在内存中。重启应用时,密钥会被丢弃。

如果密钥环存储于内存中,则在应用重启时:

  • 所有基于 cookie 的身份验证令牌都无效。
  • 用户需要在下一次请求时再次登录。
  • 无法再解密使用密钥环保护的任何数据。 这可能包括 CSRF 令牌ASP.NET Core MVC TempData cookie

若要配置数据保护以持久保存并加密密钥环,请参阅:

保护应用

配置防火墙

Firewalld 是管理防火墙的动态守护程序,支持网络区域。 仍可以使用 iptable 管理端口和数据包筛选。 默认情况下应安装 Firewalldyum 可用于安装包或验证是否已安装。

sudo yum install firewalld -y

使用 firewalld 仅打开应用所需的端口。 本例中使用的是端口 80 和 443。 以下命令将端口 80 和 443 永久设置为打开:

sudo firewall-cmd --add-port=80/tcp --permanent
sudo firewall-cmd --add-port=443/tcp --permanent

重新加载防火墙设置。 检查默认区域中可用的服务和端口。 通过检查 firewall-cmd -h 获取可用选项。

sudo firewall-cmd --reload
sudo firewall-cmd --list-all
public (default, active)
interfaces: eth0
sources: 
services: dhcpv6-client
ports: 443/tcp 80/tcp
masquerade: no
forward-ports: 
icmp-blocks: 
rich rules: 

HTTPS 配置

配置应用,以进行安全的 (HTTPS) 本地连接

dotnet run 命令使用应用的 Properties/launchSettings.json 文件,该文件将应用配置为侦听 applicationUrl 属性(例如 https://localhost:5001;http://localhost:5000)提供的 URL。

使用以下方法之一配置应用,使其在开发过程中将证书用于 dotnet run 命令或开发环境(Visual Studio Code 中的 F5 或 Ctrl+F5):

配置反向代理,以便进行安全 (HTTPS) 客户端连接

警告

本部分中的安全配置是一种常规配置,可以作为进一步进行自定义的起点。 我们无法为第三方工具、服务器和操作系统提供支持。 如果使用本部分中的配置,请自行承担风险。 有关更多信息,请访问以下资源:

若要为 Apache 配置 HTTPS,请使用 mod_ssl 模块。 安装了 httpd 模块时,也会安装了 mod_ssl 模块。 如果未安装,请使用 yum 将其添加到配置。

sudo yum install mod_ssl

若要强制使用 HTTPS,请安装 mod_rewrite 模块以启用 URL 重写:

sudo yum install mod_rewrite

修改 helloapp.conf 文件以在端口 443 上启用安全通信。

下面的示例未将服务器配置为重定向不安全的请求。 建议使用 HTTPS 重定向中间件。 有关详细信息,请参阅在 ASP.NET Core 中强制使用 HTTPS

注意

对于由服务器配置(而非 HTTPS 重定向中间件)处理安全重定向的开发环境,建议使用临时重定向 (302) 替代永久性重定向 (301)。 链接缓存会导致开发环境中的行为不稳定。

添加 Strict-Transport-Security (HSTS) 标头可确保由客户端发起的所有后续请求都通过 HTTPS。 有关设置 Strict-Transport-Security 标头的指南,请参阅在 ASP.NET Core 中强制使用 HTTPS

<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:443>
    Protocols             h2 http/1.1
    ProxyPreserveHost     On
    ProxyPass             / http://127.0.0.1:5000/
    ProxyPassReverse      / http://127.0.0.1:5000/
    ErrorLog              /var/log/httpd/helloapp-error.log
    CustomLog             /var/log/httpd/helloapp-access.log common
    SSLEngine             on
    SSLProtocol           all -SSLv3 -TLSv1 -TLSv1.1
    SSLHonorCipherOrder   off
    SSLCompression        off
    SSLSessionTickets     on
    SSLUseStapling        off
    SSLCertificateFile    /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
    SSLCipherSuite        ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
</VirtualHost>

注意

此示例中使用了本地生成的证书。 SSLCertificateFile 应为域名的主证书文件。 SSLCertificateKeyFile 应为创建 CSR 时生成的密钥文件。 SSLCertificateChainFile 应为证书颁发机构提供的中间证书文件(如有)。

需要 Apache HTTP 服务器版本 2.4.43 或更高版本,才能使用 OpenSSL 1.1.1 运行 TLS 1.3 Web 服务器。

注意

上面的示例禁用了在线证书状态协议 (OCSP) 装订。 有关启用 OCSP 的更多信息和指南,请参阅 OCSP 装订(Apache 文档)

保存文件,并测试配置:

sudo service httpd configtest

重新启动 Apache:

sudo systemctl restart httpd

其他 Apache 建议

通过共享框架更新重启应用

在服务器上升级共享框架后,重启服务器托管的 ASP.NET Core 应用。

其他标头

为了防止恶意攻击,应对一些标头进行修改或添加一些标头。 确保已安装 mod_headers 模块:

sudo yum install mod_headers

保护 Apache 免受点击劫持攻击

点击劫持(也称为 UI 伪装攻击)是一种恶意攻击,其中网站访问者会上当受骗,从而导致在与当前要访问的页面不同的页面上单击链接或按钮。 使用 X-FRAME-OPTIONS 可保护网站。

缓解点击劫持攻击:

  1. 编辑 httpd.conf 文件:

    sudo nano /etc/httpd/conf/httpd.conf
    

    添加行 Header append X-FRAME-OPTIONS "SAMEORIGIN"

  2. 保存该文件。

  3. 重启 Apache。

MIME 类型探查

X-Content-Type-Options 标头阻止 Internet Explorer 进行 MIME 探查(从文件内容中确定文件的 Content-Type)。 如果服务器通过设置 nosniff 选项将 Content-Type 标头设置为 text/html,则不管文件内容为何,Internet Explorer 都会将内容呈现为 text/html

编辑 httpd.conf 文件:

sudo nano /etc/httpd/conf/httpd.conf

添加行 Header set X-Content-Type-Options "nosniff"。 保存该文件。 重启 Apache。

负载平衡

此示例演示如何在同一实例计算机上的 CentOS 7 和 Kestrel 上设置和配置 Apache。 为了不出现单一故障点,使用 mod_proxy_balancer 并修改 VirtualHost 可实现在 Apache 代理服务器后方管理 Web 应用的多个实例。

sudo yum install mod_proxy_balancer

在下面所示的配置文件中,helloapp 的其他实例设置为在端口 5001 上运行。 “代理” 部分设置了具有两个成员的均衡器配置,以便对 byrequests 进行负载均衡。

<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
    RewriteEngine On
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>

<VirtualHost *:443>
    ProxyPass / balancer://mycluster/ 

    ProxyPassReverse / http://127.0.0.1:5000/
    ProxyPassReverse / http://127.0.0.1:5001/

    <Proxy balancer://mycluster>
        BalancerMember http://127.0.0.1:5000
        BalancerMember http://127.0.0.1:5001 
        ProxySet lbmethod=byrequests
    </Proxy>

    <Location />
        SetHandler balancer
    </Location>
    ErrorLog /var/log/httpd/helloapp-error.log
    CustomLog /var/log/httpd/helloapp-access.log common
    SSLEngine on
    SSLProtocol all -SSLv2
    SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:!RC4+RSA:+HIGH:+MEDIUM:!LOW:!RC4
    SSLCertificateFile /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
</VirtualHost>

速率限制

使用 httpd 模块中包含的 mod_ratelimit ,客户端的带宽可以限制为:

sudo nano /etc/httpd/conf.d/ratelimit.conf

示例文件将根位置下的带宽限制为 600 KB/秒:

<IfModule mod_ratelimit.c>
    <Location />
        SetOutputFilter RATE_LIMIT
        SetEnv rate-limit 600
    </Location>
</IfModule>

较长的请求标头字段

代理服务器默认设置通常将请求标头字段限制为 8190 字节。 某些应用可能需要长度超过默认值的字段(例如,使用 Microsoft Entra ID 的应用)。 如果需要更长的字段,则代理服务器的 LimitRequestFieldSize 指令需要进行调整。 要应用的值具体取决于方案。 有关详细信息,请参见服务器文档。

警告

除非必要,否则不要提高 LimitRequestFieldSize 的默认值。 提高该值将增加缓冲区溢出的风险和恶意用户的拒绝服务 (DoS) 攻击风险。

其他资源