使用访问控制保护您的站点
网络的奇妙之处在于如此多的信息是免费提供的。只需支付电话费和互联网服务提供商的月费,您就可以阅读数百份报纸,获取计算机行业的最新动态,并收听来自家乡的广播电台。
即使是最开放、免费的站点通常也包含一个或多个不供公众消费的部分。划分站点部分的原因可能各不相同:也许网站管理员想要一个地方来放置他最喜欢的技巧、一个用于测试新程序的存储库或一个可以放置员工通知的目录。如果站点想要对内容收费或限制组织成员的访问,问题就变得更加明显。
处理这些问题的一种流行方法是创建一个其他人不太可能猜到的目录。但是,这种被称为“通过隐蔽性实现安全”的方法只有在没有人泄露隐藏目录的名称时才有效。一种更稳健的方法将基于用户名、密码组合来限制访问。
本月,我们将研究如何使用 Web 的标准用户名、密码授权方案来限制对您服务器的访问。这些原则应适用于任何 Web 服务器,但在我的示例中,我将使用免费提供的 Apache Web 服务器(可在 https://apache.ac.cn/ 获取)。
访问限制是 HTTP 的一部分,HTTP 是大多数 Web 事务中使用的协议。当您的浏览器使用 HTTP 从服务器请求文档时,通常会立即返回该文档,并在前面加上几个标头(即,名称、值对),描述其长度、上次修改日期以及包含的内容类型。
HTTP 的设计者认识到网站管理员可能希望限制对一个或多个目录的访问。自 1.0 版本以来,HTTP 已经包含了限制对网站某些部分访问的规定。
让我们从计算机的角度看看这种保护是如何工作的,首先看看一个不受保护的站点,然后再看看一个受保护的站点。一旦我们了解了访问保护的工作原理,我们就可以将其融入到我们自己的工作中。
一切都始于用户要求浏览器检索文档。无论用户是在文本字段中键入 URL,还是从书签列表中选择它,还是单击现有 HTML 页面中的超链接,效果都是相同的。浏览器获取 URL,将其分解为协议、服务器和文档,并采取适当的操作。对于诸如以下的 URL
http://www.ssc.com/lj/
协议名称是 http,服务器名称是 www.ssc.com,文档名称(实际上是一个目录)是 /lj/。大多数 Web 服务器都配置为请求目录与请求该目录中的文件 index.html 相同,因此上面的 URL 实际上等同于这个 URL
http://www.ssc.com/lj/index.html我们可以通过自行分解 URL 并使用来自 Linux 命令行 的 HTTP 从 www.ssc.com 请求文档 /lj/ 来模拟浏览器的操作。TELNET 程序通常用于登录远程机器,最常见的是在该机器上打开 shell。通过给 telnet 除了机器名称之外的参数,我们可以指定要连接的端口。由于 Web 服务器默认位于端口 80,我们可以通过键入以下内容连接到 www.ssc.com 上的 Web 服务器
telnet www.ssc.com 80当我们建立与该 Web 服务器的连接时,我们可以输入 HTTP 请求。这些请求以描述我们希望执行的操作(称为“方法”)、我们希望检索的文档名称以及我们正在使用的 HTTP 版本的一行开始。从 HTTP 1.0 开始,初始行之后可以跟随一个或多个标头行,其中包含有关用户浏览器、浏览器愿意期望的文档类型、过去可能已设置的 HTTP Cookie 以及其他有用信息的信息。对于我们的目的,输入这一行就足够了
GET /lj/ HTTP/1.0然后按两次 Enter 键——一次结束包含请求的行,第二次表示我们已完成发送所有标头,现在将等待来自服务器的响应。
如果一切顺利,服务器将通过返回 HTML 页面来响应。在这种特殊情况下,我们将收到 HTML 格式的文本(我们可以从响应顶部的 text/html Content-Type 标头中看出),其中包含有关本杂志的最新信息。您的浏览器负责获取服务器返回的 HTML 并为您显示它。
如果我们尝试检索受保护的文档,事情会变得有点复杂。(我们稍后将看到如何保护文档;现在,假设可以限制对 Web 服务器上文档的访问。)我的主工作站运行 Red Hat Linux 4.2 和 Apache 1.2.4,包含一个“私有”目录,其内容受到限制。让我们检索 /private/ 的内容,就像我之前请求 /lj/ 的内容一样。
从 shell 提示符下,我使用以下命令连接到 Web 服务器
telnet localhost 80
连接后,我请求“private”目录
GET /private/ HTTP/1.0我没有收到 /private 目录的内容或 /private 中包含的 index.html 文件,而是收到了以下响应
HTTP/1.1 401 Authorization Required Date: Mon, 26 Jan 1998 12:08:17 GMT Server: Apache/1.2.4 WWW-Authenticate: Basic realm="TestRealmName" Connection: close Content-Type: text/html <HTML><HEAD> <TITLE>401 Authorization Required</TITLE> </HEAD><BODY> <H1>Authorization Required</H1> This server could not verify that you are authorized to access the document you requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn't understand how to supply the credentials required.<P> </BODY></HTML> Connection closed by foreign host.换句话说,我的请求被拒绝了,因为我没有进行身份验证。它什么时候给我机会这样做呢?
这就是用户身份验证的肮脏小秘密:当您检索受保护的文档时,您的浏览器实际上必须 两次 请求该文档。第一次尝试检索文档时,浏览器会收到类似于我们上面收到的消息,标记为响应代码 401,表明您需要授权才能检索此文档。
旧的或损坏的浏览器会停在那里,向用户显示服务器的错误消息。了解身份验证的现代浏览器会向用户显示一个对话框,用户可以在其中键入用户名和密码。然后,浏览器获取用户名和密码,将两者都转换为 Base64 格式,并在初始请求后在“Authorization”标头中发送该信息。
现代浏览器还通过跟踪您已输入的用户名和密码来节省时间。因此,当您第一次遇到受保护的目录时,系统会提示您输入用户名和密码。第二次您从同一目录检索文件时,系统不会提示您。浏览器是等待接收 401 -- 需要授权 错误后再发送用户名、密码对,还是自动响应消息,取决于实现方式。
因此,如果我的用户名是“reuven”,密码是“password”,我可以使用 TELNET 访问本地计算机上的端口 80 并输入以下内容来检索 /private/ 目录的内容
GET /private/ HTTP/1.0 Authorization: Basic cmV1dmVuOnJldXZlbg==
第一行与我们之前看到的相同;它表明我们想使用 HTTP 1.0 来检索名为 /private/ 的文档(它恰好是一个目录,尽管客户端不知道这一点),使用 GET 方法。我们没有在第一行之后按两次 Enter 键,而是只按一次,然后添加一个额外的标头。这个标头以“Authorization:”开头,这意味着我们即将使用“Basic”算法向系统发送授权信息,这只不过是对用户以 username:password 格式输入的用户名和密码进行 Base64 编码。
如果用户名、密码组合成功,系统将返回浏览器请求的资源的内容。如果请求失败,则会将相同的消息(带有响应代码 401)返回给用户的浏览器。浏览器可以允许用户重试,也可以显示随 401 消息一起发送的错误消息。
在这种情况下,用户名、密码组合确实有效,给了我 /private/ 的内容,即 /private/index.html 文件,以以下方式返回
HTTP/1.1 200 OK Date: Mon, 26 Jan 1998 12:41:14 GMT Server: Apache/1.2.4" Last-Modified: Mon, 26 Jan 1998 10:49:49 GMT ETag: "1057-ca-34cc6a4d" Content-Length: 202 Accept-Ranges: bytes Connection: close Content-Type: text/html <HTML> <Head> <Title>My private site</Title> </Head> <Body> <H1>My private site</H1> <P>This is my private site. From here, you can get to <a href="test.html">my test page</a>.</P> </Body> </HTML>
响应顶部的 200 状态代码表明一切顺利,服务器能够检索我们请求的文档。正如您可以从 Content-Type 标头中看到的那样(或者只是通过查看文档的内容),请求的文档包含 HTML 格式的文本。如果我们通过浏览器查看它,我们无疑会看到不同大小的文本。
如果您想知道我是如何获得用户名、密码组合的 Base64 等效项的,那是借助以下一行 Perl 程序
perl -e 'use MIME::Base64;\ print encode_base64("reuven:password");'
在 shell 中输入上述内容会得到
cmV1dmVuOnBhc3N3b3Jk这一定是 reuven:password 的 Base64 等效项,因为它允许我们访问资源。
MIME::Base64 是一个 Perl 模块,您可以从 CPAN (https://perldotcom.perl5.cn/CPAN/) 获取该模块来处理 MIME 标准邮件。我不记得上次我必须编写程序来处理使用 MIME 编码的电子邮件是什么时候了,但是 Base64 模块在这种非邮件应用程序中非常方便。
如果您有任何保护计算机网络的经验,您可能会惊讶地发现用户名和密码在 Web 浏览器和服务器之间以未加密的方式传递。实际上,虽然文本并非完全以明文形式传递,但只需要另一行 Perl 程序就可以将 Base64 编码的用户名、密码字符串转换回其 ASCII 原始形式。
可以肯定地说,这不是一个非常安全的方案。监控网络上发送的数据包的人必须更加努力才能捕获您的用户名和密码,但这并不会比文本在没有任何转换的情况下发送要困难得多。
至少,请确保使用的用户名和密码与 /etc/passwd 无关,/etc/passwd 文件通常存储 Linux 系统上的用户信息。您的秘密文档仍然可以通过 Web 访问,但您的机器不会容易受到入侵,这是一种更严重的威胁。(入侵您计算机的人可以做的事情远不止读取您的文档。)
一种称为“摘要”的身份验证方案即将推出。它已经在 Apache 中可用,并且正在等待浏览器实现它。摘要方法将函数应用于多个参数,包括将要发送的用户名和密码,以及服务器生成的数字,该数字作为 401 -- 需要授权 响应中的标头的一部分发送。然后,摘要函数的结果通过网络发送,而不是用户名和密码本身。这不是一个万无一失的系统,但它比当前密码容易泄露的情况要好得多。
现在我们已经讨论了所有这些背后的理论,我们将看看保护服务器上目录所必需的内容。
您需要的第一件事是一个可以存储用户名和密码的文件。Apache 附带了一个程序 htpasswd,可用于创建和修改此类文件。语法相当简单
htpasswd [-c] passwordfile username
要创建新的密码文件(或覆盖现有文件),请使用以下语法
htpasswd -c /etc/httpd/conf/passwords reuven如果您在 Linux shell 中输入上面的行(并且 htpasswd 在您的 $PATH 环境变量中),系统将提示您输入密码。在您输入两次密码后,用户名、密码对将存储在您指定的文件中。
-c 选项创建一个新文件或覆盖现有文件。(此选项对于创建用户是不必要的;您可以在没有 -c 选项的情况下执行此操作,如下所述。)请特别注意 -c 选项,因为它会覆盖旧版本的密码文件,而不会发出警告或进行备份。
要将用户添加到现有密码文件或更改现有用户的密码,请在不使用 -c 选项的情况下调用 htpasswd
htpasswd /etc/httpd/conf/passwords reuven
无论您是添加新用户还是更改现有用户的密码,系统都会要求您输入两次用户的密码。完成后,命名文件将被更新。
密码文件仅包含名称和加密密码,格式为
username1:password1 username2:password2 username3:password3
例如,我为此专栏创建的密码文件包含以下条目
reuven:zZDDIZ0NOlPzw reena:SjCCCbsjjz2Z2 foobar:RpubVfdhWwv1U如果您希望在系统上处理许多授权用户请求,并且服务器上的用户数量很高,您可能需要考虑使用更高效的系统进行授权,例如 DBM 或 DB。现代版本的 Apache 支持 DB 和 DBM(尽管必须编译相应的模块),并且支持许多关系数据库,包括 Msql 和 MySQL。有关这些选项的更多信息,请访问 Apache 网站。
现在我们有了正确格式的用户名和密码列表,我们可以使用该列表来保护服务器上的目录。每个目录可以使用不同的文件来包含用户名和密码——因此您的“绝密”目录可以具有与您的“秘密”目录不同的用户列表。
有两种方法可以保护系统上的文件。一种是将一个名为 .htaccess 的文件(默认情况下)放在您要保护的目录中。这使您可以灵活地快速轻松地修改各个目录,并将不同目录的责任交给负责这些目录的人员——但这也会消除一定的集中控制。
因此,我们将查看在 srm.conf(Apache 配置文件之一)中定义访问限制的方法。将访问限制放在 srm.conf 中意味着您将拥有对服务器访问的集中控制,并且每次进行更改时都必须重新启动服务器。
受保护的目录在 srm.conf 中使用 <Directory> 和 </Directory> 语句声明,语法相对简单。例如,我在此文件中添加了以下行来保护本文中使用的目录
<Directory /home/httpd/html/private> AuthType Basic AuthName TestRealmName AuthUserFile /tmp/authusers require valid-user </Directory>
第一行和最后一行将这些声明限制为 /home/httpd/html/private,这是我服务器上的受保护目录。请求 /home/httpd/html(我的 Web 服务器上的根目录)中的文件的人无需输入用户名或密码。尝试检索 /home/httpd/html/private(在外部世界中称为 /private)或 /private 的任何子目录中的文件的人必须输入用户名和密码。
用户名、密码对将使用我们之前看到的“基本”身份验证方案传递,其中用户名、密码使用 Base64 编码,并作为请求后的 HTTP 标头的一部分发送。在浏览器开始支持“摘要”方法(甚至更安全的方法)之前,所有受保护的目录都应将 AuthType 声明为“Basic”。
AuthName 是一种向外界标识此目录的方式。您可能希望将目录称为有意义的名称,例如“Joe 的私有目录”或“仅供参考”。您可以使用 AuthName 来区分 Web 服务器的不同受保护部分,例如“私有区域”和“员工区域”。AuthName 通常显示在用户可以在其中输入其用户名和密码的对话框中。
接下来,我们指示应将哪个密码文件用于此目录。如前所述,每个目录都可以使用单独的密码文件,因此指定您希望使用哪个文件非常重要。如果您希望在系统上使用多个密码文件,您可能需要研究组的使用,组允许您在一个密码文件中向不同的用户子集授予权限。(用户可以放入组中,我们在此处不做介绍,但这允许您将密码文件中的每个用户与一个或多个组关联)。
最后,我们表明我们将仅允许有效用户,这意味着仅允许那些用户名和密码在 AuthUserFile 中命名的密码文件中的用户。您还可以指定允许进入站点的个别用户,例如
require user reuven reena
将此信息放入服务器的 srm.conf 文件后,您需要告诉服务器重新读取其配置文件。您可以通过关闭服务器然后重新启动它,或者通过向其发送 HUP 信号来执行此操作,如下所示
killall -v -1 httpd此命令向当前运行的所有 httpd 实例发送 HUP 信号(又名信号 #1)。请记住,Apache 通常同时运行多个服务器,因此尝试识别单个进程并使用标准 kill 命令可能不是一个好方法。
重新启动服务器后,只有用户名和密码出现在这些目录之一中的人才能访问受保护的目录。如果您想测试保护机制,使用 TELNET(如上所述)假装是 Web 浏览器可能是最好的方法,以避免浏览器的密码缓存。
正如您可以保护包含 HTML 文件和图片的目录一样,您也可以保护包含 CGI 程序的目录。例如,如果您想使选定数量的 CGI 程序仅供选定数量的用户访问,您可以像定义 /private 一样定义 /cgi-bin/private。
例如,这是我添加到 srm.conf 以保护 /cgi-bin/private 的定义
<Directory /home/httpd/cgi-bin/private> AuthType Basic AuthName TestRealmName AuthUserFile /tmp/authusers require valid-user </Directory>
如您所见,除了目录名称之外,定义与 /private 的定义相同。
在这种情况下,如果我们尝试使用 GET 或 POST 执行此目录中的 CGI 程序,系统将要求我们提供用户名、密码组合。(Apache 允许您为每种方法设置单独的访问权限,因此您可以允许所有用户使用 GET,但限制组使用 POST,而另一些用户可以使用 PUT 和 DELETE。)在请求实际发送到有问题的 CGI 程序之前,我们将必须验证自己的身份。
保护 CGI 目录的一个好处是,该目录中的所有程序都可以立即访问一个新的环境变量 REMOTE_USER,其中包含有问题的用户的名称。这对于用 Perl 编写并使用 CGI.pm 的 CGI 程序通过 remote_user 方法可用,但所有程序都可以检索环境变量的值。
这有什么用呢?好吧,我们知道用户名必须是唯一的;没有两个用户可以共享一个用户名。因此,我们可以使用用户名作为关系数据库表中包含有关用户的更多信息(例如其年龄、兴趣和上次访问时间)的主键(即唯一索引)。
实际上,在过去的几个月中,本专栏研究了各种用于跟踪有关用户的信息的技术,最常见的是通过在用户的计算机上设置 HTTP Cookie 并在 Cookie 中设置主键值。
该系统的优点是用户必须先验证其身份才能被允许访问程序——这意味着当 CGI 程序执行时,我们可以确定用户名存在,与真实用户关联,并且该用户代表该人(或有权访问用户的密码)。HTTP Cookie 基于每台计算机运行;如果有人在我不注意的时候使用我的计算机,他们可以从我检索过 Cookie 的所有私有站点检索信息。
使用这种形式的身份验证而不是 Cookie 的另一个优点是,它为用户提供了移动性。用户不再受限于特定的计算机或浏览器。虽然用户必须先登录才能被允许使用该站点,但他们可以从任何地方访问该站点,而不仅仅是从他们在工作场所或家中的计算机访问。
也存在缺点——主要的缺点是与基本身份验证方案相关的固有不安全性。有些用户也不喜欢每次访问站点时都必须输入用户名和密码的麻烦。这些用户宁愿站点自动识别并记住他们的设置。
列表 1 是一个用 Perl 编写的简短 CGI 程序,用于识别输入的用户名。如果此程序放置在不受保护的目录中,它将指示 REMOTE_USER 没有可用值。但是,如果从受保护的目录中运行,它将返回用于访问该目录的用户名。
如果您要在关系数据库(例如 MySQL)中创建表,则可以将主键定义为不超过八个字符的用户名。然后,remote_user 的值可以用作数据库的可靠索引。
随着 Web 的不断成熟,保护网站肯定会成为一个越来越重要的话题。Apache 在这种安全机制方面非常灵活。虽然我提到了组,但没有足够的空间来讨论其他选项,例如按域或 IP 地址限制访问。有关此问题和其他来源的更多信息,请参阅 Apache 文档和边栏。
用户名、密码组合对于限制对网站的访问非常有用,它们也可以用于生成数据库的唯一键。如果您正在考虑创建数据库来跟踪您的用户,您可能需要考虑使用访问控制来强制用户登录。
限制对 Web 站点上目录的访问既不复杂也不困难,并且可以让您将敏感或私有材料放在 Web 上,而无需担心有人发现秘密 URL。
