现在CDN应用越来越广泛,但是这样也相应的产生一些防御上的问题,比如我们使用CLOUDFLARE的CDN做防御,但是CLOUDFLARE抗DDOS能力是很强,但是面对模拟真实访客的CC攻击就没有什么特别的办法,只要攻击者给你的网页带上?参数,CF就会回源去读取你服务器上数据,同时有个几千几万请求的时候,就会有很多CF的服务器回源到你服务器上,造成拥堵,网站就无法访问了,在你服务器上查看连接的IP也都是CF的IP,这种情况下也不可能去通过封禁CF IP的方式来达到防攻击的目的,因为你把CDN的IP都给封杀了,正常访客通过这个CDN就访问不到你的网站,相当于变相宕机了。

所以问题点就在于,在我们的后端服务器(源服务器)上一定要有个判断方法,能识别到每个回源访问的真实IP地址是什么,幸好现在大部分CDN,包括国内的阿里云,腾讯云,百度云都是支持携带X-Forwarded-For头的,这个识别头会带上访客的真实IP来传给服务器,因此可以通过这个来识别到每个访问请求的真实IP,从而通过封禁访问量过大的IP的方法达到防CC的目的,一般不会误杀,正常访客不大可能达到非常高频率的访问,高频访问要嘛是攻击者要嘛就是采集者。

先来看看两种方式访问路径

普通用户IE浏览器 ——-> 你的服务器 由于普通用户直接访问你的服务器,所以服务器端可以直接得到用户的 IP 地址, 而我们的限制就是基于对 来源IP 地址的访问限制

普通用户浏览器 —–> CDN或者反代服务器(我们自己建的反代服务器,CloudFlare,阿里云等) —-> 源服务器(PHP 程序部署在这里,iptables, nginx 安全配置等)

第一种方式可以直接看到访客IP,因此封禁比较简单,只要看看日志,挑出量大的直接封杀就行了

用户的 IP 地址 $binary_remote_addr 作为 Key,每个 IP 地址最多有 50 个并发连接

你想开 几千个连接 刷死我? 超过 50 个连接,直接返回 503 错误给你,根本不处理你的请求了

在 Nginx 的 http 模块内加入如下配置

limit_conn_zone $binary_remote_addr zone=TotalConnLimitZone:10m ;
limit_conn  TotalConnLimitZone  50;
limit_conn_log_level notice;
 
## 用户的 IP 地址 $binary_remote_addr 作为 Key,每个 IP 地址每秒处理 10 个请求
## 你想用程序每秒几百次的刷我,没戏,再快了就不处理了,直接返回 503 错误给你
limit_req_zone $binary_remote_addr zone=ConnLimitZone:10m  rate=10r/s;
limit_req_log_level notice;
 
## 具体服务器配置
server {
    listen   80;
    location ~ .php$ {
                ## 最多 5 个排队, 由于每秒处理 10 个请求 + 5个排队,你一秒最多发送 15 个请求过来,再多就直接返回 503 错误给你了
        limit_req zone=ConnLimitZone burst=5 nodelay;
 
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include    fastcgi_params;
    }    
 
}

第二种方式因为日志中记录到的访客IP其实都是CDN的IP地址,如果用第一种的NGINX配置脚本,最终封杀的是CDN 的IP而不是攻击者,因此需要迂回获取访客真实IP,下面就讲讲怎么获取访客真实IP

X-Forwarded-For : 用户IP, 代理服务器IP

如果中间经历了不止一个 代理服务器,像访问www.bnxb.com 中间建立多层代理之后,这个 记录会是这样

X-Forwarded-For : 用户IP, 代理服务器1-IP, 代理服务器2-IP, 代理服务器3-IP, ….

可以看到经过好多层代理之后, 用户的真实IP 在第一个位置, 后面会跟一串 中间代理服务器的IP地址,从这里取到用户真实的IP地址,针对这个 IP 地址做限制就可以了,

nginx 配置 取得用户的原始地址,在 Nginx 的 http 模块内加入如下配置

map $http_x_forwarded_for  $clientRealIp
{
        ## 没有通过代理,直接用 remote_addr
    ""    $remote_addr;  
        ## 用正则匹配,从 x_forwarded_for 中取得用户的原始IP
        ## 例如   X-Forwarded-For: 202.123.123.11, 208.22.22.234, 192.168.2.100,...
        ## 这里第一个 202.123.123.11 是用户的真实 IP,后面其它都是经过的 CDN 服务器
    ~^(?P<firstAddr>[0-9.]+),?.*$    $firstAddr;
}

通过 map 指令,我们为 nginx 创建了一个变量 $clientRealIp ,这个就是 原始用户的真实 IP 地址,

不论用户是直接访问,还是通过一串 CDN 之后的访问,我们都能取得正确的原始IP地址

这里$clientRealIP 就是用户真实 IP,而且代码中还配合使用了 $remote_addr,因此$clientRealIP 还能兼容上文中第一种直接访问模式,不像 $http_x_forwarded_for 在直接访问模式中将会是空值!所以 $clientRealIP 还能配置到 Nginx 日志格式中,替代传统的 $remote_addr 使用,这样日志显示也能正常显示真实访客IP

下面是修改之后的 Nginx 配置: CDN环境下 Nginx 的安全配置

## 这里取得原始用户的IP地址
map $http_x_forwarded_for  $clientRealIp {
    ""    $remote_addr;
    ~^(?P<firstAddr>[0-9.]+),?.*$    $firstAddr;
}
 
## 针对原始用户 IP 地址做限制
limit_conn_zone $clientRealIp zone=TotalConnLimitZone:20m ;
limit_conn  TotalConnLimitZone  50;
limit_conn_log_level notice;
 
## 针对原始用户 IP 地址做限制
limit_req_zone $clientRealIp zone=ConnLimitZone:20m  rate=10r/s;
#limit_req zone=ConnLimitZone burst=10 nodelay;
limit_req_log_level notice;
 
## 具体服务器配置
server {
    listen   80;
    location ~ .php$ {
                ## 最多 5 个排队, 由于每秒处理 10 个请求 + 5个排队,你一秒最多发送 15 个请求过来,再多就直接返回 503 错误给你了
        limit_req zone=ConnLimitZone burst=5 nodelay;
 
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include    fastcgi_params;
    }    
 
}

到这里你的网站就能配合任何CDN或者云加速之类的平台完成防御作业了

中文乱码问题
  1、下载类库语言安装脚本
    https://github.com/dompdf/utils.git

  2、将里面的load_font.php文件拷贝到dompdf根目录,和autoload.inc.php同级
3、下载一个宋体字体 simsun.ttf,也放到项目根目录,这时load_font.php和simsun.ttf应该在同一级目录
4、在根目录执行命令安装字体

php load_font.php simsun.ttf simsun

 命令说明 php load_font.php 调用这个文件,并传入字体路径,字体名字
  注意这一步,最后的simsun最好不要添加引号,看有一些例子写了引号,导致后面声明字体使用不上。
进入到这个目录:C:\wamp64\www\tp5\vendor\dompdf\dompdf\lib\fonts
    找到这个文件:dompdf_font_family_cache.php
    看下是否有以下这段,没有就添加上,有就最好。

中文换行问题

1、寻找到Text.php文件
    我的目录:C:\wamp64\www\tp5\vendor\dompdf\dompdf\src\FrameReflower\Text.php

  2、找到相关代码

// split the text into words
    $words = preg_split('/([\s-]+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    $wc = count($words);

  3、替换以上代码,或者注释掉以上代码,添加我们的代码。

    preg_match_all("/./u", $text, $array);
    $words = array(0);
    $wc = count($words);

  4、小提示,可能因为版本的变迁,文件、代码位置不一样,大家最好使用目录全文搜索那一段代码,而不是直接去找那个文件。

页面设置问题

 1、设置页面

  //网上你可能看到很多这样的例子,我要竖着的看啊,坑。
    $dompdf->setPaper('A4', 'landscape');
//找了类库说明,发现还有一个参数,这下终于可以正常显示了。
    $dompdf->setPaper('A4', 'portrait');

  2、为了更好将HTML转换成PDF,建议将HTML宽高设置成和A4大小接近,避免不必要的麻烦。个人还是要根据实际pdf显示情况来调节。

body {
    margin: 0;
    width: 820px;
       height: 1160px;
}

表格table向下合并td后,再分页情况下第2页有接着分页就会乱了
这是由于使用了rowspan>1 的话,那下边的相应的行就必然少了一些td,如果分页dompdf就没有加以处理,导致下一页少了td造成,那我们就应该换一种思路,不要使用rowspan,可以通过边框border样式来达到合并格子的需求,只不过无法实现垂直居中了,不过效果还成:

.pure-table {
    border-collapse: collapse;
    border-spacing: 0;
    empty-cells: show;
    border-right: 1px solid #cbcbcb;
    border-bottom: 1px solid #cbcbcb;
}
.pure-table td,.pure-table th{
    border-left: 1px solid #cbcbcb;
    border-top: 1px solid #cbcbcb;
}


        <table class="pure-table pure-table-bordered pact-but" style="width: 800px;">
            <thead>
            <tr style="background-color: #ffff00;">
                <th>分区</th>
                <th>名称</th>
                <th>数量</th>
                <th>单位</th>
                <th>备注</th>
            </tr>
            </thead>
            <tbody>
            <?php foreach ($hetong->fields['supplies'] as $zone => $items) :
                foreach ($items as $k => $item) :
                    $_desc = zmf::mbChunkString($item['desc'], 20);
                    ?>
                    <tr>

                        <?php if ($k == 0) : ?>
                        <td>
                            <span><?= $item['zone'] ?></span>
                        </td>
                        <?php else: ?>
                        <td style="border-top: 0;">&nbsp;</td>
                        <?php endif ?>
                        <td><?= $item['title'] ?></td>
                        <td><?= $item['num'] ?></td>
                        <td><?= $item['unit'] ?></td>
                        <td>
                            <?php foreach ($_desc as $v):?>
                                <span style="display: block;"><?= $v ?></span>
                            <?php endforeach; ?>
                        </td>
                    </tr>
                <?php endforeach; ?>
            <?php endforeach; ?>
            </tbody>
        </table>

第一步打开chrome控制台,可以使用鼠标右键,点击 检查 菜单打开Elements界面或者使用快捷键,window平台F12, Mac平台 command+option+i
第二步使用快捷键 windows平台 ctrl+shift+p, Mac平台 Maccommand+shift+p,,调出命令行,输入screen,出现以下命令,选中即可生成图片并自动保存:

Capture full size screenshot 截取整个网页,body部分

Capture node screenshot 截取选中的Dom节点