Apache,是非常流行的网页服务器软件。通常和脚本语言比如 PHP,数据库 MySQL 一起工作,合称为 LAMP。本文主要参考 apache 官方文档

用默认配置可以启动一个简单的服务,有用户访问时会提供目录 /srv/http 下的内容。

启动 httpd.service systemd 服务,Apache 就会启动,从浏览器中访问 http://localhost/ 会显示一个简单的索引页面。

Apache 本身的架构是 一个核心外围的模块,它的配置也遵循这个结构。

主配置文件

Apache 配置文件位于 /etc/httpd/conf 目录,主配置文件是 /etc/httpd/conf/httpd.conf, 此文件可以通过 Include 指令来引用其它配置文件,可以用通配符引用多个配置文件。这些配置文件中可以使用任何指令。对于主配置文件的修改,只有启动或重启 httpd 之后才会生效。

服务器还会读取一个包含 mine 文档类型的文件,文件名由 TypesConfig 指令指定,默认为 mime.type

配置文件语法

httpd 配置文件每行包含一条指令,如果太长可以在行尾使用 \ 来续接下一行。

指令的参数与指令之间用空白字符分隔,如果参数包含空格,必须用括号引用。

指令前的空格会被忽略,所以如果需要,可以用缩进让配置更有条理。

无论是由 Define 语句定义的变量,还是 shell 的环境变量,都可以在配置文件中使用,语法为 ${VAR}。如果变量名正确,变量值会在原位被替换。在查找变量名时,由 Define 定义的变量比环境变量更有优先级。

只有在服务器启动前定义的环境变量才会被扩展。在配置文件中定义的环境变量,生效太晚,无法用来扩展。

可以用 apachectl configtest-t 选项来检查配置文件是否存在问题。

模块

httpd 是模块化的服务器,这意味着核心服务器只有最基本的功能,其它功能都由扩展的模块来完成。

服务器编译时,默认会收纳一部分模块,如果服务器编译为使用动态加载模块,则这些模块可以单独编译,然后在运行时使用 LoadModule 指令加载。否则,如果添加或删除模块,就要重新编译 httpd。

可以用 <IfModule> 条件结构来检查某些模块是否存在,配置指令可以基于这样的条件来加载。然而,有时修改这些条件结构会让人忽视某个重要模块没有被加载的事实。

要想查看当前有哪些模块被编译到了服务器中,可以使用 -l 选项。还可以用 -M 选项查看动态加载了哪些模块。

指令的作用域

放在主配置文件中的指令作用于整个服务器。如果希望修改作用域,让指令更有针对性,可以将其置于 <Directory><DirectoryMatch><Files><FilesMatch><Location><LocationMatch> 段落中。这些段落将指令作用的范围限制于特定的文件系统路径或网址,可以嵌套,以实现更加 细化 的控制。

httpd 支持同时提供多个不同的网站,称之为 虚拟主机。对应的段落为 <VirtualHost>

本文中把这样的 block 称为 段落容器

段落容器的类型

主要有两类基本的容器,多数的容器会被每个请求解析。容器中的指令只作用于那些匹配该容器的请求。

然而,<ifDefine><IfModule><IfVersion> 这些容器只有在服务器启动和重启时才会解析。解析时,如果当时条件为真,则其中的指令会作用于 所有请求,否则会忽略。

这些容器可以 嵌套,在条件中加上 ! 来表示相反的条件。

<IfDefine>

<IfDefine> 容器中的指令,只有在使用了适当的 httpd 命令行选项时才会生效。例如,如果使用以下的配置,只有在服务器使用 httpd -DClosedForNow 启动时,所有的请求才会被重定向到另一个网站:

<IfDefine ClosedForNow>
    Redirect "/" "http://otherserver.example.com/"
</IfDefine>
<IfModule>

<IfModule> 容器与之很相似,只不过其条件是特定的模块当前在服务器中可用。该模块要么是静态编译在服务器中;要么是动态编译的,而且其 LoadModule 加载的指令在配置文件中必须等于当前容器。只有在该模块的存在对配置有影响时才需要使用这个容器,如果希望其中的指令始终要执行,就不能使用该容器,因为它会抑制关于模块缺失的有用的错误消息。

下面这个范例中,只有在 mod_mime_magic 模块可用时,才会应用 MimeMagicFile 指令。

<IfModule mod_mime_magic.c>
    MimeMagicFile "conf/magic"
</IfModule>
<IfVersion>

只有特定版本的服务器在运行,才会执行该容器中的指令。该容器主要用于测试,以及在大型网络中要同时处理不同版本、不同配置的 httpd 的情况。

<IfVersion >= 2.4>
    # this happens only in versions greater or
    # equal 2.4.0.
</IfVersion>

文件系统、网页空间、布尔表达式

最常使用的配置段落容器是那些用来修改文件系统或网页空间特定位置的。

文件系统:是从操作系统的角度来看磁盘。

网页空间:是从客户端的角度来看网站。

因此,默认安装后,网页空间中的路径 /dir/ 对应的是文件系统中的 /usr/local/apache2/htdocs/dir。网页空间无需直接映射到文件系统,因为有时候网页是由数据库动态生成的,或来自其它路径。

文件系统容器

<Directory><DirectoryMatch><Files><FilesMatch> 这些容器中的指令对文件系统的局部产生作用。

<Directory>

<Directory> 中的指令应用于指定名称的目录及其所有子目录、所有文件。用 .htaccess 文件也能取得同样的效果。

以下范例中,会针对 /var/web/dir1 目录启用目录索引。

<Directory "/var/web/dir1">
    Options +Indexes
</Directory>

<DirectoryMatch> 与此类似,只不过不是直接指定目录名,而是用正则表达式来指定。

<Files>

<Files> 中的指令作用于指定名称的文件,不管其位于什么目录中。

以下范例中,无论哪个目录中的 private.html 都禁止访问:

<Files "private.html">
    Require all denied
</Files>
<Directory><Files> 捆绑使用

要想专门指定特定的文件,可以把两个容器组合使用。

以下范例中,/var/web/dir1 目录中,以及其所有子目录中的 private.html 文件均会被禁止访问。

<Directory "/var/web/dir1">
    <Files "private.html">
        Require all denied
    </Files>
</Directory>
网页空间容器

<Location><LocationMatch> 指令用于修改网页空间内容的配置。

以下范例中,禁止访问所有以 /private 开头的网址:

<LocationMatch "^/private">
    Require all denied
</LocationMatch>

因此,这些网址均会被禁止访问:

http://yoursite.example.com/private

http://yoursite.example.com/private123

http://yoursite.example.com/private/dir/file.html

<Location> 容器不一定非要与文件系统有关系。

以下范例中,是把特定的网址映射到由 mod_status 提供的服务器内部的处理器,与文件系统无关。

<Location "/server-status">
    SetHandler server-status
</Location>
重叠的网页空间

要想处理两个有重叠部分的网页空间,必须安排好它们的先后顺序。

对于 <Location> 应该短地址在先:

<Location "/foo">
</Location>
<Location "/foo/bar">
</Location>

对于 <Alias> 应该长地址在先:

Alias "/foo/bar" "/srv/www/uncommon/bar"
Alias "/foo" "/srv/www/common/foo"

对于 ProxyPass 指令也是长地址在先:

ProxyPass "/special-area" "http://special.example.com" smax=5 max=10
ProxyPass "/" "balancer://mycluster/" stickysession=JSESSIONID|jsessionid nofailover=On
通配符及正则表达式

<Directory><Files><Location> 这些指令都可以使用 shell 风格的通配符:

* 匹配任意序列的字符

? 匹配任意单个字符

[seq] 匹配 seq 中任意一个字符

/ 不会被任何通配符匹配,必须显式指定

如果需要更加复杂的匹配,则使用 <DirectoryMatch><FilesMatch><LocationMatch>,它们可以使用兼容 PERL 的正则表达式进行匹配。

范例

想要修改所有用户目录的配置,在不用正则表达式的 <Directory> 中是这样:

<Directory "/home/*/public_html">
    Options Indexes
</Directory>

而在使用正则表达式的 <FilesMatch> 中,可以同时指定多种图片文件的类型:

<FilesMatch "\.(?i:gif|jpe?g|png)$">
    Require all denied
</FilesMatch>

可以把包含指定组名和反向引用的表达式添加到环境变量中,用大写字母表示。这样一来,就可以在表达式或 mod_rewrite 这样的模块中引用文件名路径和网址中的元素。

<DirectoryMatch "^/var/www/combined/(?<SITENAME>[^/]+)">
    require ldap-group "cn=%{env:MATCH_SITENAME},ou=combined,o=Example"
</DirectoryMatch>
布尔表达式

<If> 指令会根据条件来修改配置,其条件可以用布尔表达式来表示。

以下范例中,如果 HTTP 的 Referer 标头不是以 http://www.example.com/ 开头的,则拒绝访问。

<If "!(%{HTTP_REFERER} -strmatch 'http://www.example.com/*')">
    Require all denied
</If>
如何选择

在文件系统容器和网页空间容器之间做选择其实是很简单的:

  • 如果指令要作用的对象在文件系统一侧,则使用文件系统容器
  • 如果指令要作用的对象不在文件系统一侧,则使用网页空间容器,如数据库生成的网页

有一点很重要:绝对不要用 <Location> 来限制对文件系统对象的访问。因为可能有许多不同的网页空间其网址是映射到同一个文件系统的路径的,这个 Location 被限制,可以通过其它 Location 来访问,而它们映射的可能是同一个目录,这就导致限制被规避。

例如:

<Location "/dir/">
    Require all denied
</Location>

如果请求对象是 http://yoursite.example.com/dir/,该配置会正常工作。但假如当前系统是大小写不敏感的,用户就可以使用 http://yoursite.example.com/DIR/ 来规避限制了。

相比之下,<Directory> 指令则从文件系统一侧加以限制,无论它被映射为什么网址,其限制都有效。但是,因为 <Directory> 中的符号链接不会被解析为真实地址,所以也有一定的风险。因此应该用适当的 Options 指令来禁用符号链接。

因此,在可能的情况下应该尽量使用 文件系统容器,以避免多重映射带来的危险。

当然,对于使用 <Location "/"> 来做限制就可以完全放心了,因为它会作用于所有请求,不管网址是什么。

容器的嵌套

有些容器类型可以嵌套于别的容器。

  • <Files> 可以嵌套在 <Directory>
  • <If> 可以嵌套在 <Directory><Location><Files>
  • <If> 不可以嵌套于另一个 <If>

嵌套的容器会在同类型的非嵌套容器之后合并。

虚拟主机

<VirtualHost> 容器中的指令作用于特定的主机。在同一系统提供多个虚拟主机时可以分别配置各主机。

代理

<Proxy><ProxyMatch> 容器用于正向代理,匹配的网址会通过 mod_proxy 代理服务器来访问。

以下范例中,只允许 yournetwork.example.com 这个域中的 host 通过代理访问 www.example.com 网站。通常代表局域网中的用户通过代理访问互联网。

<Proxy "http://www.example.com/*">
    Require host yournetwork.example.com
</Proxy>

容器中允许使用的指令

<Directory> 段落中可以使用的指令也可以在 <DirectoryMatch><Files><FilesMatch><Location><LocationMatch><Proxy><ProxyMatch> 中使用。

还有一些例外的情况:

  • AllowOverride 指令只能用于 <Directory>
  • FollowSymLinksSymLinksIfOwnerMatch 这两个 Options 只能用于 <Directory>.htaccess 文件
  • Options 指令不能用于 <Files><FilesMatch>

各段落是如何合并的

各个配置段落是以特定 顺序 应用的,这对配置指令的解析有重要的影响。

合并的顺序:

  1. <Directory>.htaccess 同时解析。不包括 <DirectoryMatch>。如果允许,.htaccess 可以覆盖 <Directory>
  2. <DirectoryMatch><Directory "~">
  3. <Files><FilesMatch>
  4. <Location><LocationMatch>
  5. <If>
重要提示

以上每一条,我们这里暂且称为一个指令组。

  • 除了 <Directory>, 每个指令组中的段落按其在配置文件中出现的先后次序来处理。
  • <Directory>,即上面的指令组 1 中,先处理最短的目录,再处理长的目录。
  • 如果有多个 <Directory> 段落应用到同一个目录,则按其在配置文件中出现的顺序处理。
  • Include 指令引用进来的配置,会被看作它们本就处在 Include 指令中。
  • 如果某个指令同时在主配置中和 <VirtualHost> 中存在,<VirtualHost> 中的会覆盖主配置中的。
  • 如果请求是由 mod_proxy 处理的,在处理次序上 <Proxy> 会代替 <Directory> 第一个处理。
各模块与配置段落的关系

mod_rewrite 这样的模块,其指令是如何、什么时候被处理的呢?

每个 httpd 的模块都会管理其自身的配置,httpd.conf 配置文件中的每个模块的指令都指定特定上下文中的配置。

运行时 中,httpd 的内核会对所有的配置段落进行迭代,就按照上面所说的顺序,来决定哪些用来处理当前的请求。如果匹配了第一个段落,则把这个配置用于该请求;如果也匹配随后的段落,则前后两个段落中的模块及指令就有机会合并到一起,这样就得到了一个 合并之后的配置。然后会继续解析其他的段落,直到所有段落解析完毕。

在以上的步骤之后,真正针对 HTTP 请求的处理开始了:

每个模块都有机会运行,完成各种任务。它们随时可以 从 httpd 内核中获取其合并后的配置,据此来决定该如何操作。

范例

本例中的配置使用 mod_header 模块中的 Header 指令来指定 HTTP 标头。

<Directory "/">
    Header set CustomHeaderName one
    <FilesMatch ".*">
        Header set CustomHeaderName three
    </FilesMatch>
</Directory>

<Directory "/example">
    Header set CustomHeaderName two
</Directory>

根据以上配置,针对指向 /example/index.html 的请求,httpd 应该在 CustomHeaderName 标头中设置什么值呢?

  • 在第一个 <Directory> 段落中,匹配目录 / ,所以此时为 CustomHeaderName 标头配置的值为 one
  • 在第二个 <Directory> 段落中,匹配目录 /example,因为模块 mod_headers 在其代码中被设定为合并时允许覆盖,所以一产生新的配置,为 CustomHeaderName 标头分配新值 two
  • <FilesMatch> 段落中,匹配 .*,于是另一个合并的机会产生了,导致 CustomHeaderName 标头再一次分配新值 three
  • 最后,在随后对 HTTP 请求进行处理时,mod_headers 被调用,它会收到该配置,为 CustomHeaderName 标头分配值 three。mod_headers 通常使用该配置完成其工作,即配置标头。

因为 .htaccess<Directory> 有相同的优先级,所以对于它也是如此。

有一个重要的概念要理解:像 <Directory><FilesMatch> 这样的配置段落不能拿来与特定模块指令相比较,如 HeaderRewriteRule,因为它们是在不同的级别操作的。

非常有用的范例

以下范例是为了展示合并的顺序。假设它们都适用于当前的请求,范例中的指令会按 A > B > C > D > E 的顺序应用。

<Location "/">
    E
</Location>

<Files "f.html">
    D
</Files>

<VirtualHost *>
    <Directory "/a/b">
        B
    </Directory>
</VirtualHost>

<DirectoryMatch "^.*b$">
    C
</DirectoryMatch>

<Directory "/a/b">
    A
</Directory>

再来一个实际的范例:

<Location "/">
    Require all granted
</Location>

# 这个 <Directory> 段落根本不会生效
<Directory "/">
    <RequireAll>
        Require all granted
        Require not host badguy.example.com
    </RequireAll>
</Directory>

即使 <Directory> 费了半天劲做了限制,但最后应用的是 <Location> 段落,它说一路放行,于是它说了算。

.htaccess 文件

httpd 可以通过把特殊的文件放到网页目录树中实现 去中心化的配置管理。所谓特殊的文件通常名为 .htaccess,也可以用 AccessFileName 指令来指定。

放置在该文件中的指令作用于它所在的目录,及其所有子目录。

其语法也与主配置文件相同。

因为处理每个请求时都会读取一次该文件,因此对它的修改会立即生效。

如果同一个指令,先出现在主配置,之后又出现在 .htaccess 中,服务器需要是否要用后者覆盖前者。如果在主配置中用 AllowOverride 标明,就会进行覆盖。

AllowOverride AuthConfig Indexes

值得注意的是,AllowOverride 只在 <Directory> 段落中有效,在其它段落中无效。