Python 3反爬虫原理与绕过实战
上QQ阅读APP看书,第一时间看更新

2.1 nginx服务器

Web网站的功能由编程语言实现,例如Java、Python和PHP等。编程语言专注于网站功能的实现,资源映射与连接处理则由服务器软件完成。常见的服务器软件有Apache、nginx和Tomcat等,接下来我们将通过nginx来增进对服务器的了解。

nginx是一个HTTP和反向代理服务器,同时也是邮件代理服务器和通用的TCP / UDP代理服务器。它具有模块化设计、可扩展、低内存消耗、支持热部署等优秀特性,所以非常多的Web应用将其作为服务器软件。本书也将使用它实现一些反爬虫的功能。

nginx有一个主进程和若干工作进程,其中主进程用于读取和评估配置并维护工作进程,工作进程会对请求进行实际处理。nginx采用基于事件的模型和依赖于操作系统的机制,有效地在工作进程之间分发请求。工作进程数在配置文件中进行定义,可以设定具体数值或使用默认选项。

2.1.1 nginx的信号

信号(signal)是控制nginx工作状态的模块,我们可以在终端使用信号来控制nginx的启动、停止和配置重载等。信号的语法格式如下:

nginx -s signal

其中signal是信号名称。常用的nginx信号有以下几种。

❑ stop:快速关机。

❑ quit:正常关机。

❑ reload:重新加载配置文件。

❑ reopen:重新打开日志文件。

假如我们需要停止nginx服务,但又希望它处理完当前请求后再停止工作进程,可以在终端向nginx发送正常关机的信号,命令为:

$ nginx -s quit

当nginx的配置被更改或者添加新的辅助配置文件时,它们不会立即生效。如果想让新的配置生效,就必须重新启动nginx或者进行配置重载。假如我们希望nginx在不影响当前任务处理的情况下重载配置,可以通过终端向nginx发送重新加载配置文件的信号,命令为:

$ nginx -s reload

一旦主进程收到配置重载信号,它将检查新配置文件的语法有效性,并尝试应用其中的配置。如果成功,主进程将启动新的工作进程并向旧工作进程发送关闭请求,否则主进程将回滚更改,并继续使用旧配置。旧工作进程在接收关闭请求后会停止接受新连接,并且继续为当前请求提供服务,直到当前请求处理完毕才关闭。

更多nginx信号的知识可前往nginx官方文档查看,详见http://nginx.org/en/docs/control.html

2.1.2 nginx配置文件

nginx由模块组成,而这些模块由配置文件中特定的指令控制,也就是说nginx的配置文件决定了nginx及其模块的工作方式。nginx的配置文件分为主配置文件和辅助配置文件:主配置文件名为nginx.conf,默认存放在 /etc/nginx目录中;辅助配置文件要求以 .conf作为文件后缀,并且默认存放在/etc/nginx/conf.d目录中。要注意的是,nginx允许同时存在多个辅助配置文件。

nginx的指令分为简单指令和块指令。一个简单的指令由指令名称和参数组成,它们以空格作为分隔符,并以分号结尾,如:

error_page 404 /404.html;

其中error_page是指令名称,404和 /404.html共同组成参数,作用是指定404错误显示的HTML文件。

块指令与简单指令具有相同的结构,但它不是以分号结尾,而是以花括号包围的一组附加指令结束,如:

location /404.html {
    root   /home/async/www/error_page;
}

如果块指令内包含其他指令,则该块指令称为上下文。常见的上下文有events、http、server和location。要注意的是,这里还有一个隐藏的main上下文,它并非实际存在,类似于层级的根目录,即所有的指令的最外层都是main。main上下文作为其他上下文的参考对象,例如events和http,必须写在main上下文中,server必须写在http中,而location则必须写在server中。对于它们的关系,我们可以通过一段简单的配置来理解:

http {
    server {
        location / {
            root /www/index index.html;
        }
        location /images/ {
            # ...
        }
    }
}

配置文件的注释符为#。以上配置默认监听80端口,当我们在本地访问http://localhost/时,服务器将根据配置文件设定的资源路径寻找资源,并将符合条件的资源发送给客户端,如果资源不存在,则发送404错误。我们并没有在配置中添加任何有关main的文字,但http上下文确实包含在main中。

nginx提供了一个默认的辅助配置文件default.conf,存放在 /etc/nginx/conf.d目录中,里面包含了若干server块指令示例。我们可以在终端使用如下命令查看default.conf文件的内容:

$sudo cat /etc/nginx/conf.d/default.conf

命令执行后,终端会输出如下内容(以 ... 代替部分被注释的内容):

server {
    listen         80;
    server_name   localhost;
    #charset koi8-r;
    #access_log   /var/log/nginx/host.access.log   main;

    location / {
        root    /usr/share/nginx/html;
        index   index.html index.htm;
    }

    #error_page   404                  /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page    500 502 503 504   /50x.html;
    location = /50x.html {
        root    /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    # ...
    #}
}

2.1.3 简单的代理服务

提供资源是服务器的功能之一,接下来我们就来学习如何通过nginx实现简单的代理服务。本次任务要求服务器根据用户的请求URL,响应服务器本地目录中的资源(如 /data/www目录中包含的HTML文件和 /data/images目录中包含的图片文件)。我们可以通过编辑nginx的配置文件,将URL和本地目录中的资源进行映射,从而实现这个需求。

首先,我们需要准备本地目录的资源。在用户目录(如 /home/async)下创建www文件夹,将包含任意内容的index.html文件放到www目录中。在www文件夹中创建images文件夹,并在里面放置一张名为example.png的图片。

然后使用编辑器打开nginx默认的辅助配置文件default.conf,将其中的所有代码注释后,编写新的配置:

http {
    server {
    }
}

在通常情况下,server需要确定监听的端口和服务器名称, nginx一旦决定处理请求,就会根据块指令中定义的指令参数测试请求头中指定的URI。我们将下方的location添加到server中:

location / {
    root /home/async/www;
}

配置中的location指定“/”与请求中的URI进行比较。对于匹配的请求,URI将指向root指令中指定的路径,即 /home/async/www,以便将本地资源与请求对应。如果存在多个匹配的location块指令,那么nginx会选择具有最长前缀的块指令。当无法匹配到其他前缀时,就会匹配“/”。

接下来添加第二个location块指令:

location /images/ {
    root /home/async/www;
}

它将匹配以“/images/”开头的URI(“/”也匹配此类请求,但由于“/”的前缀长度比“/images/”短,所以优先匹配“/images/”)。完整的辅助配置文件内容如下:

server {
location / {
root /home/async/www;
}

location /images/ {
root /home/async/www;
}
}

如果想让刚才的配置生效,我们需要给nginx发送重载配置的信号:

$ nginx -s reload

配置生效后,nginx就会监听80端口(80是默认值),我们可以在浏览器中访问http://localhost/。服务器为响应这次请求,会根据配置文件中指定的文件路径搜索HTML文件,并优先选择名为index.html的文件作为响应内容。如果在浏览器中访问http://localhost/images/example.png,那么nginx会根据配置文件,从指定的路径中搜索example.png文件,如果存在,则返回文件资源,否则触发404错误。

在真正访问http://localhost/之前,还有两件事要做。第一件事是编辑nginx主配置文件,将配置中User指定的用户从nginx改为你的操作系统的用户名(比如我的操作系统的用户名为async)。打开主配置文件的命令为:

$sudo nano /etc/nginx/nginx.conf

主配置文件的第一行即是User设置。

第二件事是关闭SELinux。SELinux是美国国家安全局(NSA)对于强制访问控制的实现,默认安装在版本较新的Linux系统中。如果当前操作系统中没有SELinux,则无须关闭。打开SELinux配置文件的命令为:

$sudo nano /etc/selinux/config

注意将配置文件中的SELINUX=enforcing改为SELINUX=disabled,保存改动后重新启动操作系统。

接着我们用浏览器访问http://localhost/,此时显示的内容如图2-2所示。

图2-2 浏览器显示的内容

再试一试访问http://localhost/images/example.png,浏览器显示的内容如图2-3所示。

图2-3 浏览器中显示的图片

当我们使用浏览器访问指定的URL后,如果浏览器中能够正确显示我们准备的HTML内容和图片,就说明nginx辅助配置已生效。

2.1.4 nginx模块与指令

nginx内置了很多模块,可以从nginx文档中的Modules reference部分查看。其中比较常用的模块是ngx_http_rewrite_module(详见http://nginx.org/en/docs/http/ngx_http_rewrite_module.html),我们可以通过学习该模块来熟悉nginx模块的语法。

ngx_http_rewrite_module模块的主要作用是重定向,它通过正则表达式或判断语句来更改请求的URI。该模块有一些主要的指令,这些指令分别是if、set、break、return和rewrite。if指令的语法和语境如表2-1所示。

表2-1 if指令的语法和语境

我们可以看到if指令的语法是:

if 条件 {
    ...
}

这与其他编程语言的语法非常相似,很容易理解。if指令没有默认值,使用范围限制在server块和location块内。它的条件有以下几种情况。

变量名称:如果变量的值是空字符串或0,则条件的布尔值为false;在nginx 1.0.1版本之前,任何以0开头的字符串都被认为是错误的值。

=或!=:比较变量和字符串。

~或~*:将变量与正则表达式匹配,区分大小写。如果正则表达式包含}或;字符,则整个表达式应该用单引号或双引号括起来。

-f或!-f:检查文件是否存在。

-d或!-d:检查目录是否存在。

-e或!-e:检查文件、目录或符号链接是否存在。

-x或!-x:检查可执行文件。

我们可以通过一些例子来理解这些条件判断:

# 当请求头中User-Agent 头域的值包含MSIE 字符串,则重定向到指定URI
if ($http_user_agent ~ MSIE) {
    rewrite ^(.*)$ /msie/$1 break;
}
# 当请求头中Cookie 头域的值满足条件,则设定$id 变量值为正则部分
if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
    set $id $1;
}
# 如果请求方式是 POST,则返回 405
if ($request_method = POST) {
    return 405;
}
# 限制下载速度为10k,$slow 可以通过set 指令设置
if ($slow) {
    limit_rate 10k;
}
#当请求头中Referer 头域的值为空或www.example.com 时,允许访问,否则返回403
valid_referers none www.example.com;
if ($invalid_referer) {
    return 403;
}

我们可以从其中选出一个作为验证对象。打开nginx辅助配置文件default.conf,并在文件中添加对Referer的判断语句:

server {
    location / {
        # 对请求的Referer 进行验证,如果没有Referer 头域
        # 或者头域值为www.example.com,则允许访问
        valid_referers none www.example.com;
        if ($invalid_referer) {
            return 403;
        }
        root /home/async/www;
    }
    location /images/ {
        root /home/async/www;
    }
}

为了让nginx启用新配置,我们需要给它发送reload信号:

$ nginx -s reload

为了验证请求的过滤效果,我们可以使用Postman进行测试。首先测试没有Referer头域的请求,得到的结果如图2-4所示。

图2-4 Postman请求结果1

本次请求的响应状态码为200,说明服务器可以正常响应客户端的请求。然后测试Referer头域的值不满足条件的情况,结果如图2-5所示。

图2-5 Postman请求结果2

本次请求的响应状态码为403,说明服务器对Referer头域的值进行判断后,发现它并不符合要求。除了以上给出的$invalid_referer和$request_method变量之外,还有哪些变量呢?在介绍ngx_http_core_module时,我们给出了nginx支持的嵌入式变量,其中常用的变量及含义如表2-2所示。

表2-2 nginx中常用的变量及其含义

nginx所支持的嵌入变量详见http://nginx.org/en/docs/http/ngx_http_core_module.html#variables

nginx指令列表详见http://nginx.org/en/docs/http/ngx_http_core_module.html#Directives。以limit_rate指令为例,其语法和语境如表2-3所示。

表2-3 limit_rate指令的语法和语境

limit_rate的作用是限制客户端的传输速度,单位为字节/秒,默认值为0,即不限速。limit_rate是对单个请求设置限制的,意味着如果客户端同时打开两个连接,则总速率将是指定限制的两倍。除了全局限速以外,还可以根据条件设置限速:

server {
    if (condition) {
        set $limit_rate 100k;
    }
}

ngx_http_rewrite_module模块的语法和语境如表2-4所示。

表2-4 ngx_http_rewrite_module模块的语法和语境

如果指定的正则表达式与请求URI匹配,则URI将根据replacement字符串的指定进行更改。该rewrite指令按照其在配置文件中出现的顺序依次执行,可以使用标志终止对指令的进一步处理。如果替换字符串以http://、https://或$scheme开头,则处理停止并将重定向返回给客户端。

flag参数可以是以下值之一。

last:停止处理当前的ngx_http_rewrite_module指令集,并开始搜索与更改后的URI匹配的新位置。

break:ngx_http_rewrite_module与break指令一样,可以停止处理当前的指令集。

redirect:返回带有302代码的临时重定向。如果替换字符串不以http://、https://或$scheme开头,则使用它。

permanent:返回301代码的永久重定向。

官方给出的重定向示例代码如下:

server {
    ...
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 last;
    rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra   last;
    return   403;
    ...
}

但是如果将这些指令放在/download/位置内,则该last标志应替换为break,否则nginx将进行10次循环并返回500错误:

location /download/ {
    rewrite ^(/download/.*)/media/(.*)\..*$ $1/mp3/$2.mp3 break;
    rewrite ^(/download/.*)/audio/(.*)\..*$ $1/mp3/$2.ra   break;
    return   403;
}

2.1.5 nginx日志

日志是nginx的重要组成部分,记录着每一次请求的相关信息,是开发者了解客户端请求和服务器端响应状态的好帮手。nginx的日志分为访问日志和错误日志,存储路径可以在nginx主配置文件中查看,设置访问日志存放路径的指令名为access_log,而设置错误日志存放路径的指令名为error_log,示例命令如下:

access_log   /var/log/nginx/access.log   main;
error_log /var/log/nginx/error.log;

1. 访问日志

访问日志主要记录客户端访问nginx的请求信息,如客户端的IP地址、请求的URI、响应状态、Referer头域的值等。以下记录为本机nginx访问记录中的一条:

127.0.0.1 - - [11/Mar/2019:08:42:43 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11;
Fedora; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0" "-"

nginx中与访问日志相关的指令是log_format和access_log。log_format用来设置访问日志的格式,也就是日志文件中每条日志记录的格式,它在主配置文件中的设置如图2-6所示。

图2-6 访问日志格式

访问日志默认使用main格式,并且里面记录了很多的信息,这些信息的含义如表2-5所示。

表2-5 log_format支持的变量及其释义

2. 错误日志

错误日志主要记录客户端访问nginx错误时的请求信息,不支持自定义格式。错误日志有多种等级,如debug、info、notice、warn、error和crit,从左到右日志级别逐步递增。nginx的主配置文件中将错误日志的级别设为warn。以下记录为本机nginx错误记录中的一条:

2019/03/10 20:30:44 [error] 11160#11160: *1 "/home/async/www/index.html" is forbidden
(13: Permission denied), client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host:
"localhost"

我们可以在错误记录中看到错误发生的具体时间、原因、客户端的IP地址、请求方式和协议版本等信息,这对我们排查错误和测试有很大的帮助。

2.1.6 小结

nginx是一款轻量、高性能的服务器应用,配置灵活、功能强大,深受开发者喜爱。nginx具有条件判断、连接限制和客户端信息获取等功能,这些功能为开发者限制爬虫程序提供了条件。