跳到主要内容

Linux 修行之路 · Blog

Linux修行之路 - 技术博客

分享Kubernetes、Linux、Python、网络安全等技术文章

文章数量169
技术分类9
查看分类
18

nginx + lua 实现 waf

· 阅读需 14 分钟

Lua脚本基础语法

lua是一个简介、轻量、可扩展的脚本语言

nginx + lua的优势:

- 充分的结合 Nginx 的并发处理epoll优势和Lua的轻量实现简单的功能且高并发的场景
//epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
- 场景:
- 统计IP
- 统计用户信息
- 安全WAF

安装lua

~]# yum install lua -y

lua的运行

~]# lua
> print("hello world")

~]# which lua
/usr/bin/lua

~]# vim test.lua
#!/usr/bin/lua
print("hello world")
print("test:",a)
~]# lua ./test.lua

Lua的注释

行注释:
--print("hi,boy")

多行注释:
--[[
xxxxxxx
]]

Lua的基础语法

变量定义
a = 123
~]# vim test.lua
#!/usr/bin/lua
print("test:",a) #不加$

~]# lua ./test.lua

while循环

~]# cat while.lua
#!/usr/bin/lua
sum =0
num =1
while num <= 100 do
sum = sum + num
num = num + 1
end
print("sum=",sum)
//执行结果
~]# lua while.lua
sum = 5050

for循环

~]# cat for.lua
#!/usr/bin/lua
sum = 0
for i = 1,100 do
sum = sum + 1
end
print("sum=",sum)
//执行结果
~]# lua for.lua
sum = 100

if 判断

~]# cat if.lua
#!/usr/bin/lua
if age == 40 and sex == "Man" then
print("男人大于40")
elseif age > 60 and sex ~= "Women" then
print("非女人而且大于60")
else
local age = io.read()
print("Your age is"..age)
end

// ~= 是不等于
// 字符串的拼接操作符 ".."
// io库的分别从stdin和stdout读写,read和write函数

Nginx加载Lua环境

默认情况下Nginx不支持Lua模块,需要安装luaJIT解释器,并且需要重新便宜Nginx,可选择使用openrestry

LuaJIT
Ngx_devel_kit 和 lua-nginx-module

环境准备

~]# yum -y install gcc gcc-c++ make pcre-devel zlib-devel openssl-devel

下载最新工具包

下载最新的luajit 和 ngx_devel_kit 以及 lua-nginx-module:

~]# mkdir -p /soft/src && cd /soft/src
~]# wget http://luajit.org/download/LuaJIT-2.0.4.tar.gz
~]# wget https://github.com/simpl/ngx_devel_kit/archive/v0.2.19.tar.gz
~]# wget https://github.com/openresty/lua-nginx-module/archive/v0.9.16.tar.gz ##视频中后又修改为v0.10.13.tar.gz

解压

解压 ngx_devel_kit 和 lua-nginx-module

//解压后为 ngx_devel_kit-0.2.19
~]# tar xf v0.2.19.tar.gz
//解压后为 lua-nginx-module-0.9.16
~]# tar xf v0.9.16.tar.gz

安装LuaJIT

安装LuaJIT ,Luajit是Lua即时编译器

~]# tar zxvf LuaJIT-2.0.3.tar.gz
~]# cd LuaJIT-2.0.3
~]# make && make install

安装Nginx并加载模块

~]# cd /soft/src
~]# wget http://nginx.org/download/nginx-1.12.2.tar.gz
~]# tar xf nginx-1.12.2.tar.gz
~]# cd nginx-1.12.2

#### 编译参数:
./configure --prefix=/soft/nginx --with-http_ssl_module --with-http_stub_status_module --with-file-aio --with-http_dav_module --add-module=../ngx_devel_kit-0.2.19/ --add-module=../lua-nginx-module-0.9.16/
//lua-nginx-module-xxxx 这个版本如果换了就要重新再编译一次

make <-j2> && make install

//如果不建立软链接,会出现share object 错误
ln -s /usr/local/lib/libluajit-5.1.so.2 /lib64/libluajit-5.1.so.2

// 测试 openresty 安装
vim nginx.conf
location /test {
default_type text/html;
content_by_lua_block {
ngx.say("Hello world")
}
}

/soft/nginx/sbin/nginx -t //相当于nginx -t
/soft/nginx/sbin/nginx //启动nginx

使用浏览器访问:
例如:39.112.53.22/test

#//4.加载lua库,加入ld.so.conf文件
#echo "/usr/local/LuaJIT/lib" >> /etc/ld.so.conf
#ldconfig

也可以直接部署春哥的开源项目 OpenResty

安装依赖包

~]# yum install -y readline-devel pcre-devel openssl-devel
~]# cd /soft/src

下载编译 openresty

wget https://openresty.org/download/ngx_openresty-1.9.3.2.tar.gz
tar zxf ngx_openresty-1.9.3.2.tar.gz
cd ngx_openresty-1.9.3.2
./configure --prefix=/soft/openresty-1.9.3.2 \
--with-luajit --with-http_stub_status_module \
--with-pcre --with-pcre-jit

gmake && gmake install
ln -s /soft/openresty-1.9.3.2/ /soft/openresty

启动 openresty

echo "PATH=/soft/openresty/nginx/sbin:$PATH" >> ~/.bashrc
source ~/.bashrc

nginx
ps aux | grep nginx

测试openresty安装

vim /soft/openresty/nginx/conf/nginx.conf
location /test {
default_type text/html;
content_by_lua_block {
ngx.say("Hello world")
}
}

ps : 实际上openresty就是嵌套了nginx实现的

Nginx调用Lua指令

  • Nginx 调用 Lua 模块指令,Nginx的可插拔模块加载执行,共11个处理阶段
语法
set_by_lua
set_by_lua_file
设置Nginx变量,可以实现负载的赋值逻辑
access_by_lua
access_by_lua_file
请求访问阶段处理,用于访问控制
content_by_lua
content_by_lua_file
内容处理器,接受请求处理并输出响应
  • Nginx 调用 Lua API
变量
ngx.varnginx变量
ngx.req.get_headers获取请求头
ngx.req.get_url_args获取url请求参数
ngx.redirect重定向
ngx.print输出响应内容体
ngx.say输出响应内容体,最后输出一个换行符
ngx.header输出响应头

Nginx + Lua 实现代码灰度发布

使用Nginx结合lua 实现代码灰度发布,灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

按照一定的关系区别,分不分的代码进行上线,使代码的发布能平滑过渡上线

  1. 用户的信息cookie等信息区别
  2. 根据用户的ip地址,颗粒度更广
nginx+lua  通过Memcached校验IP。存在IP则成功-->访问成功即访问java_test,不成功即访问java_prod

执行过程L:

  • 1.用户请求到达前端代理Nginx,内嵌的lua模块会解析Nginx配置文件中Lua脚本
  • 2.Lua脚本会获取客户端IP地址,查看Memcached缓存中是否存在该键值
  • 3.如果存在则执行@java_test,否则执行@java_prod
  • 4.如果使@java_test,那么location会将请求转发至新版代码的集群组
  • 5.如果使@java_prod,那么location会将请求转发至原始版代码集群组
  • 6.最后整个过程执行后结束

实践环境准备:

系统服务地址
centos7Nginx+Lua+Memcached192.168.29.19
centos7Tomcat集群8080_prod192.168.29.20
centos7Tomcat集群9090_test192.168.29.21

1.安装两台服务器 Tomcat,分别启动8080 和9090 端口

## 两台服务器都需要配置java环境 //yum install -y java
20 ~]# cd /soft
20 ~]# ./tomcat-8080/bin/startup.sh

## 可以在这个环境中改一下那个端口,scp到另一个服务器中,然后直接启动

21 ~]# sh /soft/tomcat-9090/bin/startup.sh
21 ~]# netstat -lntp //端口已经起来了
21 ~]# cd /soft/tomcat-9090/webapps/ROOT
21 ~]# ll //有一个test.jsp

##
20 ~]# cd /soft/tomcat-9090/webapps/ROOT
20 ~]# rm -rf ./*
20 ~]# vim test.jsp
## 里面的内容从21的服务器上的test.jsp里复制过来,记得改一下`<TITLE>`内端口号: JSP 8080
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
`<HTML>`
`<HEAD>`
`<TITLE>`JSP 9090</TITLE>
</HEAD>
`<BODY>`
<h1> Jsp 9090-test </h1>
<%
Random rand = new Random();
out.println("<h1>Random number:</h1>");
out.println(rand.nextInt(99)+100);
%>
% </BODY>
% </HTML>

#######################################
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
`<HTML>`
`<HEAD>`
`<TITLE>`JSP 8080-prod</TITLE>
</HEAD>
`<BODY>`
<h1> Jsp 8080-prod </h1>
<%
Random rand = new Random();
out.println("<h1>Random number:</h1>");
out.println(rand.nextInt(99)+100);
%>
% </BODY>
% </HTML>

2.配置Memcached并让其支持Lua调用

//安装memcached服务
19 ~]# yum install memcached -y

//配置memcached支持lua
19 ~]# cd /soft/src
19 ~]# wget https://github.com/agentzh/lua-resty-memcached/archive/v0.11.tar.gz
19 ~]# tar xf v0.11.tar.gz
19 ~]# cp -r /soft/src/lua-resty-memcached-0.11/lib/resty/memcached.lua /soft/nginx/conf/lua
19 ~]# ll /soft/nginx/conf/lua ## 有一个memcached.lua这个文件


//启动memcached
19 ~]# systemctl start memcached
19 ~]# systemctl enable memcached

3.配置负载均衡调度

# 必须在http层 加一个include 包含conf。在conf中加入下面内容并且,删掉server 和 error的内容(不然会报错)
# 在conf目录里加上 lua.conf:
lua_package_path "/soft/nginx/conf/lua/memcached.lua";

upstream java_prod {
server ip:8080;
}

upstream java_test {
server ip:9090;
}

server {
listen 80;
#server_name 39.22.145.18;

location /test { #视频写的hello
default_type 'text/plain';
content_by_lua ngx.say("hello , lua scripts");
}

location /myip {
default_type 'test/plain';
content_by_lua '
clientIP = ngx.req.get_headers()["x_forwarded_for"]
ngx.say("Forwarded_IP:",clientIP)
if clientIP == nil then #视频中写的是nli
clientIP = ngx.var.remote_addr
ngx.say("Remote_IP:",clientIP)
end
';
}

location / {
default_type 'text/plain';
content_by_lua_file /soft/nginx/conf/lua/dep.lua; #这个dep.lua很重要
}
location @java_prod {
proxy_pass http://java_prod;
include proxy_params; #访问浏览器400需要加上这条信息(因为代理Tomcat必须加上这个,这个文件有一些设置的一些头部信息)
}
location @java_test {
proxy_pass http://java_test;
include proxy_params; #访问浏览器400需要加上这条信息
}
}

4.编写Nginx调用灰度发布Lua脚本

~]# cat /soft/nginx/conf/lua/dep.lua
-- 获取x-real-ip
clientIP = ngx.req.get_headers()["X-Real-IP"]

-- 如果IP为空 , 取x-forwarded-for
if clientIP == nil then
clientIP = ngx.req.get_headers()["x_forwarded_for"]
end

-- 如果IP为空-取remote_addr
if clientIP == nil then
clientIP = ngx.var.remote_addr
end

-- 定义本地,加载memcached
local memcached = require "resty.memcached"
-- 实例化对象
local memc, err = memcached:new()
-- 判断连接是否存在错误
if not memc then
ngx.say("failed to instantiate memc: ", err)
return
end
-- 建立memcache连接
local ok, err = memc:connect("127.0.0.1",11211)
-- 无法连接往前端抛出错误信息
if not ok then
ngx.say("failed to connect: ",err)
return
end
----------------------
--获取对象中的ip-存在值赋给res
local res, flags,err = memc:get(clientIP)
--
-- ngx.say("value key: ",res.clientIP)
if err then
ngx.say("failed to get clientIP ", err)
return
end
-- 如果值为1 则调用local-@java_rest
if res == "1" then
ngx.exec("@java_test")
retuen
end
-- 否则调用 local-@java_prod
ngx.exec("@java_prod")
return
  • 验证
~]# /soft/nginx/sbin/nginx -t
~]# /soft/nginx/sbin/nginx -s reload

# 使用浏览器访问nginx 的 ip/(加/)
# 如果有问题 tail -f /soft/nginx/logs/error.log 查看报错信息

#########################################

访问浏览器nginx ip/myip<路径名(nginx上写的)>
想要访问新的代码 (tomcat 9090),则在nginx主机上修改一下memcached:
telnet 127.0.0.1 11211
set 211.161.160.201 0 0 1
1
#此页面变成了9090;如果delete 此ip
#会在此换成8080的页面
# 当然生产环境肯定不会是我们这样修改,会在memcache的后台管理界面直接怼ip就可以

Nginx+Lua实现WAF

  • 1.常见的恶意行为
    • 爬虫行为和恶意抓取,资源盗取
    • 防护手段
      • 1.基础防盗链功能不让恶意用户能够轻易的爬取网站对外数据
      • access_module -> 对后台,部分用户服务的数据提供IP防护

防护代码demo:

set ip = 0
if ($http_x_forward_for ^~ 211.161.160.201) {
set ip = 1
}
localtion ^~ /admin {
if ($ip ~ 0){
return 403
}
}
  • 常见的攻击手段
    • 后台密码撞库,通过猜测密码字典不断对后台系统登录性尝试,获取后台登录密码
    • 防护手段
      • 1.后台登录密码复杂性
      • 2.使用access_module 对后台提供IP防控
      • 3.预警机制
    • 文件上传漏洞,利用上传接口将恶意代码植入到服务器中,再通过url去访问执行代码
    • 执行方式xxx.com/1.jpg/1.php

解决方法:

location ^~ /upload {
root /soft/code/upload;
if ($request_filename ~* (.*)\.php){
return 403;
}
}
  • 3.常见的攻击手段
    • 利用未过滤/未审核的用户输入进行SQL注入的攻击方法,让应用运行本不应该运行的SQL代码
    • 防护手段
      • 1.php配置开启安全相关限制
      • 2.开发人员对sql提交进行审核,屏蔽常见的注入手段
      • 3.Nginx+Lua构建WAF应用层防火墙,防止SQL注入

编写WAF拦截sql注入

1.快速安装lnmp架构

# 在有waf的服务器上,yum快速安装下面软件及插件
~]# yum install mariadb mariadb-server php php-fpm php-mysql -y

2.配置Nginx + php

conf.d]# cat phpserver.conf
server {
server_name 47.104.250.169;
root /soft/code;
index index.html index.php;

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /soft/code/$fastcgi_script_name;
include fastcgi_params;
}
}

~]# systemctl start php-fpm
~]# systemctl start mariadb

3.配置MYSQL

# 创建数据库
MariaDB [(node)]> create database info;
MariaDB [(node)]> usr info;
MariaDB [info]> create table user(id int(11),username varchar(64),password varchar(64),email varchar(64));
MariaDB [info]> desc user;
xxxxxx
xxxxxx

# 插入数据
MariaDB [info]> insert into user (id,username,password,email) values(1,'xiaoyuan',('123'),'xiaoyuan@123456.com');
MariaDB [info]> select * from info.user;
xxxxxx
xxxxxx
exit

4.配置php代码

//配置html文件:

conf.d]# cat /soft/code/login.html
<html>
<head>
<title> Sql注入演示场景 </title>
<meta http-equiv="content-type"content="text/html;charset=utf-8">
</ead>
<body>
<form action='sql.php' method="post">
<table>
<tr>
<td> 用户: </td>
<td><input type="text" name="username"></td>
</tr>

<tr>
<td> 密码: </td>
<td><input type="test" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="提交"></td>
<td><input type="reset" value="重置"></td>
</tr>
</table>
</form>
</body>
</html>

//被html调用的sql.php文件
conf.d]# cat /soft/code/sql.php
<?php
$conn = mysql_connect("localhost",'root','') or die("数据库连接失败");
mysql_select_db("info",$conn) or die("您选择的数据库不存在");
$name=$_POST['username'];
$pwd=$_POST['password'];
$sql="select * from user where username='$name' and password='$pwd'";
echo $sql."<br />";
$query=mysql_query($sql);
$arr=mysql_fetch_array($query);
if($arr){
echo "login success!<br />";
echo $arr[1];
echo $arr[3]."<br /><br />";
}else{
echo "login failed!";
}
\?>
  • 测试:
浏览器访问47.104.250.169/login.html
显示404打不开,先把lua.conf 改个名; mv lua.conf lua.conf.bak 继续访问

# 登录页面显示; (上面插入的数据用户账号密码为 xiaoyuan 123,此账号密码为正确的)
# 然后再用户框输入 ' or 1=1#' 直接提交;显示登陆成功

5.使用lua解决此类安全问题

nginx+lua ->拦截cookie类型工具;拦截异常post请求;拦截CC攻击;拦截URL;拦截arg -> java\PHP\Python

6.部署Waf相关防护代码

相关项目:https://github.com/loveshell/ngx_lua_waf

~]# cd /soft/src
~]# git clone https://github.com/loveshell/ngx_lua_waf.git

//把ngx_lua_waf 复制到nginx的目录下,解压命名为waf
~]# cp -r ngx_lua_waf /soft/nginx/conf/waf

//在conf.d的 phpserver.conf 最上面添加
lua_package_path "/soft/nginx/conf/waf/\?.lua"; #视频中为/etc/waf/\?.lua
lua_shared_dict limit 10m;
init_by_lua_file /soft/nginx/conf/waf/init.lua; #/etc/waf/init.lua
access_by_lua_file /soft/nginx/conf/waf/waf.lua; #/etc/waf/waf.lua

~]# /soft/nginx/sbin/nginx -t
~]# /soft/nginx/sbin/nginx -s reload

//配置config.lua里的waf规则目录(一般在waf/conf/目录下)
RulePath = "/soft/nginx/conf/waf/wafconf/"

# 绝对路径如有变动,需对应修改,然后重启nginx即可
  • 测试:
浏览器访问47.104.250.169/login.html

# 登录页面显示; (上面插入的数据用户账号密码为 xiaoyuan 123,此账号密码为正确的)
# 输入正确的和错误的账号密码尝试一下。
# 然后再用户框输入 ' or 1=1#' 直接提交;显示登陆成功。所以说需要配置wafconf/目录下的定义规则:

修改post文件(正则匹配):
首行添加:
\sor\s+

~]# /soft/nginx/sbin/nginx -t
~]# /soft/nginx/sbin/nginx -s reload

再次测试在用户框输入 ' or 1=1#' 直接提交; 显示被拦截

编写WAF拦截CC攻击

~]# cd /soft/nginx/conf/waf
~]# vim config.lua
CCDeny="on"
CCrate="600/60" #每分钟只能有600次请求,如果超过就会被拒绝掉

~]# /soft/nginx/sbin/nginx -t
~]# /soft/nginx/sbin/nginx -s reload

本地压测 ab -n 2000 -c 200 http://47.104.250.169/login.html //请求2000次,并发200
再次访问浏览器http://47.104.250.169/login.html ;显示503

openvpn_server

· 阅读需 10 分钟

搭建vpn实现两台不同局域网内的在同一虚拟局域网内....

1.安装 openvpn、easy-rsa、iptables-services

yum -y install epel-release
yum -y install openvpn easy-rsa iptables-services

2. 使用 easy-rsa 生成需要的证书及相关文件,在这个阶段会产生一些 key 和证书

CA 根证书
OpenVPN 服务器 ssl 证书
Diffie-Hellman 算法用到的 key

2.1 将 easy-rsa 脚本复制到 /etc/openvpn/,该脚本主要用来方便地生成 CA 证书和各种 key

cp -r /usr/share/easy-rsa/ /etc/openvpn/

2.2 跳到 easy-rsa 目录并编辑 vars 文件,添加一些生成证书时用到的变量

cd /etc/openvpn/easy-rsa/<easy-rsa 版本号>/  # 查看 easy-rsa 版本号:yum info easy-rsa
vim vars # 没这个文件的话新建,填写如下内容(变量值根据实际情况随便填写):
export KEY_COUNTRY="China"
export KEY_PROVINCE="Shenzhen"
export KEY_CITY="Shenzhen"
export KEY_ORG="Google"
export KEY_EMAIL="admin@gmail.com"
source ./vars # 使变量生效

2.3 生成 CA 根证书

./easyrsa init-pki    #初始化 pki 相关目录
./easyrsa build-ca nopass #生成 CA 根证书, 输入 Common Name,名字随便起。
### 我起的名字是 server

2.4 生成 OpenVPN 服务器证书和密钥

第一个参数 server 为证书名称,可以随便起,比如 ./easyrsa build-server-full openvpn nopass

./easyrsa build-server-full server nopass

2.5 生成 Diffie-Hellman 算法需要的密钥文件

./easyrsa gen-dh

2.6 生成 tls-auth key,这个 key 主要用于防止 DoS 和 TLS 攻击,这一步其实是可选的,但为了安全还是生成一下,该文件在后面配置 open VPN 时会用到

openvpn --genkey --secret ta.key

2.7 将上面生成的相关证书文件整理到 /etc/openvpn/server/certs (这一步完全是为了维护方便)

mkdir /etc/openvpn/server/certs && cd /etc/openvpn/server/certs/
cp /etc/openvpn/easy-rsa/3/pki/dh.pem ./ # SSL 协商时 Diffie-Hellman 算法需要的 key
cp /etc/openvpn/easy-rsa/3/pki/ca.crt ./ # CA 根证书
cp /etc/openvpn/easy-rsa/3/pki/issued/server.crt ./ # open VPN 服务器证书
cp /etc/openvpn/easy-rsa/3/pki/private/server.key ./ # open VPN 服务器证书 key
cp /etc/openvpn/easy-rsa/3/ta.key ./ # tls-auth key

2.8 创建 open VPN 日志目录

mkdir -p /var/log/openvpn/
chown openvpn:openvpn /var/log/openvpn

3. 配置 OpenVPN

可以从 /usr/share/doc/openvpn-/sample/sample-config-files 复制一份 demo 到 /etc/openvpn/(openvpn 版本号查看:yum info openvpn)然后改改,或者从头开始创建一个新的配置文件。

我选择新建配置:

cd /etc/openvpn/
vim server.conf

port 8866 # 监听的端口号,记得云主机安全组放通改协议端口
proto udp # 服务端用的协议,udp 能快点,所以我选择 udp
dev tun
ca /etc/openvpn/server/certs/ca.crt # CA 根证书路径
cert /etc/openvpn/server/certs/server.crt # open VPN 服务器证书路径
key /etc/openvpn/server/certs/server.key # open VPN 服务器密钥路径,This file should be kept secret
dh /etc/openvpn/server/certs/dh.pem # Diffie-Hellman 算法密钥文件路径
tls-auth /etc/openvpn/server/certs/ta.key 0 # tls-auth key,参数 0 可以省略,如果不省略,那么客户端
# 配置相应的参数该配成 1。如果省略,那么客户端不需要 tls-auth 配置
server 10.128.0.0 255.255.255.0 # 该网段为 open VPN 虚拟网卡网段,不要和内网网段冲突即可。open VPN 默认为 10.8.0.0/24
push "route 192.168.31.0 255.255.255.0" # 设置将所有的client转发到此server的网段,打通局域网
push "dhcp-option DNS 223.5.5.5" # DNS 服务器配置,可以根据需要指定其他 ns
push "dhcp-option DNS 8.8.8.8"
push "redirect-gateway def1" # 客户端所有流量都通过 open VPN 转发,类似于代理开全局
compress lzo
duplicate-cn # 允许一个用户多个终端连接
keepalive 10 120
comp-lzo
persist-key
persist-tun
user openvpn # open VPN 进程启动用户,openvpn 用户在安装完 openvpn 后就自动生成了
group openvpn
log /var/log/openvpn/server.log # 指定 log 文件位置
log-append /var/log/openvpn/server.log
status /var/log/openvpn/status.log
verb 3
explicit-exit-notify 1

4.防火墙相关配置(使用 iptables 添加 snat 规则)

4.1防火墙相关配置(使用 iptables 添加 snat 规则)

systemctl stop firewalld
systemctl mask firewalld

4.2 禁用 SELinux

马上关闭:setenforce 0 | 马上生效

永久关闭:sed -i ‘s/SELINUX=enforcing/SELINUX=disabled/g’ /etc/selinux/config | 需要重启服务器生效

4.3 启用iptables

systemctl enable iptables
systemctl start iptables
iptables -F # 清理所有防火墙规则

4.4 添加防火墙规则,将 openvpn 的网络流量转发到公网:snat 规则

iptables -t nat -A POSTROUTING -s 10.128.0.0/24 -j MASQUERADE
iptables-save > /etc/sysconfig/iptables # iptables 规则持久化保存

4.5 Linux 服务器启用地址转发

echo net.ipv4.ip_forward = 1 >> /etc/sysctl.conf
sysctl -p # 这一步一定得执行,否则不会立即生效。

5.启动 open VPN

systemctl start openvpn@server  # 启动
systemctl enable openvpn@server # 开机自启动
systemctl status openvpn@server # 查看服务状态

添加一个 OpenVPN 用户

OpenVPN 服务端搭建完了,但是我们该如何使用呢?

要连接到 open VPN 服务端首先得需要一个客户端软件,(在 Mac 下推荐使用 Tunnelblick,下载地址:https://tunnelblick.net/downloads.html) Tunnelblick 是一个开源、免费的 Mac 版 open VPN 客户端软件。(Windows平台下载地址:https://openvpn.net/community-downloads/)

接下来在服务端创建一个 open VPN 用户:其实创建用户的过程就是生成客户端 SSL 证书的过程,然后将其他相关的证书文件、key、.ovpn 文件(客户端配置文件)打包到一起供客户端使用。由于创建一个用户的过程比较繁琐,所以在此将整个过程写成了一个脚本 add_ovpn_user.sh,脚本内容比较简单,一看就懂:

首先创建一个客户端配置模板文件 sample.ovpn,该文件在脚本中会用到,放到 /etc/openvpn/client/ 目录,内容如下:

sample.ovpn:

client
proto udp
dev tun
remote [open VPN服务端公网 ip,根据实际情况填写] 8866
ca ca.crt
cert admin.crt
key admin.key
tls-auth ta.key 1
remote-cert-tls server
persist-tun
persist-key
comp-lzo
verb 3
mute-replay-warnings
auth-nocache

下面为创建 open VPN 用户脚本:

PS: 此脚本创建用户极其方便

./add_ovpn_user.sh:

# ! /bin/bash

set -e

OVPN_USER_KEYS_DIR=/etc/openvpn/client/keys
EASY_RSA_VERSION=3
EASY_RSA_DIR=/etc/openvpn/easy-rsa/
PKI_DIR=$EASY_RSA_DIR/$EASY_RSA_VERSION/pki

for user in "$@"
do
if [ -d "$OVPN_USER_KEYS_DIR/$user" ]; then
rm -rf $OVPN_USER_KEYS_DIR/$user
rm -rf $PKI_DIR/reqs/$user.req
sed -i '/'"$user"'/d' $PKI_DIR/index.txt
fi
cd $EASY_RSA_DIR/$EASY_RSA_VERSION
# 生成客户端 ssl 证书文件
./easyrsa build-client-full $user nopass
# 整理下生成的文件
mkdir -p $OVPN_USER_KEYS_DIR/$user
cp $PKI_DIR/ca.crt $OVPN_USER_KEYS_DIR/$user/ # CA 根证书
cp $PKI_DIR/issued/$user.crt $OVPN_USER_KEYS_DIR/$user/ # 客户端证书
cp $PKI_DIR/private/$user.key $OVPN_USER_KEYS_DIR/$user/ # 客户端证书密钥
cp /etc/openvpn/client/sample.ovpn $OVPN_USER_KEYS_DIR/$user/$user.ovpn # 客户端配置文件
sed -i 's/admin/'"$user"'/g' $OVPN_USER_KEYS_DIR/$user/$user.ovpn
cp /etc/openvpn/server/certs/ta.key $OVPN_USER_KEYS_DIR/$user/ta.key # auth-tls 文件
cd $OVPN_USER_KEYS_DIR
zip -r $user.zip $user
done
exit 0

执行上面脚本创建一个用户:sh add_ovpn_user.sh sen ,会在 /etc/openvpn/client/keys 目录下生成以用户名 sen 命名的 zip 打包文件,将该压缩包下载到本地解压,然后将里面的 .ovpn 文件拖拽到 Tunnelblick 客户端软件即可使用。 在这里插入图片描述

如上三个。

删除一个 OpenVPN 用户

 上面我们知道了如何添加一个用户,那么如果公司员工离职了或者其他原因,想删除对应用户 OpenVPN 的使用权,该如何操作呢?其实很简单,OpenVPN 的客户端和服务端的认证主要通过 SSL 证书进行双向认证,所以只要吊销对应用户的 SSL 证书即可。
  1. 编辑 OpenVPN 服务端配置 server.conf 添加如下配置:
crl-verify /etc/openvpn/easy-rsa/3/pki/crl.pem
  1. 吊销用户证书,假设要吊销的用户名为 username
cd /etc/openvpn/easy-rsa/3/
./easyrsa revoke username
./easyrsa gen-crl
  1. 重启 OpenVPN 服务端使其生效
systemctl start openvpn@server

为了方便,也将上面步骤整理成了一个脚本,可以一键删除用户。

PS : 极其方便!!

del_ovpn_user.sh:

# ! /bin/bash
set -e
OVPN_USER_KEYS_DIR=/etc/openvpn/client/keys
EASY_RSA_VERSION=3
EASY_RSA_DIR=/etc/openvpn/easy-rsa/
for user in "$@"
do
cd $EASY_RSA_DIR/$EASY_RSA_VERSION
echo -e 'yes\n' | ./easyrsa revoke $user
./easyrsa gen-crl
# 吊销掉证书后清理客户端相关文件
if [ -d "$OVPN_USER_KEYS_DIR/$user" ]; then
rm -rf $OVPN_USER_KEYS_DIR/${user}*
fi
systemctl restart openvpn@server
done
exit 0

安装过程中遇到的问题及解决方法

问题 1

open VPN 客户端可以正常连接到服务端,但是无法上网,ping 任何地址都不通,只有服务端公网 ip 可以 ping 通

问题原因及解决方法:主要原因是服务的地址转发功能没打开,其实我前面配置
了 echo net.ipv4.ip_forward = 1 >> /etc/sysctl.conf,但是没有执行 sysctl -p 使其
立即生效,所以才导致出现问题。因此一定要记得两条命令都要执行。

问题 2

open VPN 可以正常使用,但是看客户端日志却有如下错误:

2019-06-15 02:39:03.957926 AEAD Decrypt error: bad packet ID (may be a replay): [ #6361 ] -- see the man page entry for --no-replay and --replay-window for more info or silence this warning with --mute-replay-warnings
2019-06-15 02:39:23.413750 AEAD Decrypt error: bad packet ID (may be a replay): [ #6508 ] -- see the man page entry for --no-replay and --replay-window for more info or silence this warning with --mute-replay-warnings

问题原因及解决方法:
其实这个问题一般在 open VPN 是 UDP 服务的情况下出现,主要原因是 UDP 数据包重复发送导致,在 Wi-Fi 网络下经常出现,这并不影响使用,但是我们可以选择禁止掉该错误:根据错误提示可知使用 -mute-replay-warnings 参数可以消除该警告,我们使用的 open VPN 是 GUI 的,所以修改客户端 .ovpn 配置文件,末尾添加:mute-replay-warnings 即可解决。

该问题在这里有讨论:
https://sourceforge.net/p/openvpn/mailman/message/10655695/

linux系统中的openvpn_client

这里开始虚拟机nat不共享是ssh链接不到的,我们先给他桥接网卡做好设置之后再改回来。
yum -y install epel-release
yum -y install openvpn
vim /etc/sysctl.conf 修改网络配置文件
net.ipv4.ip_forward = 1
sysctl -p 使修改配置生效
rpm -ql openvpn | grep client.conf
cp /usr/share/doc/openvpn-2.4.9/sample/sample-config-files/client.conf /etc/openvpn 复制客户端配置文件并编辑:

client
proto udp
dev tun
remote 120.79.102.7 8866
resolv-retry infinite #
ca /etc/openvpn/client/ca.crt
cert /etc/openvpn/client/sen.crt
key /etc/openvpn/client/sen.key
tls-auth ta.key 1
remote-cert-tls server
persist-tun
persist-key
comp-lzo
verb 3

连接openvpn

openvpn /etc/openvpn/client.conf  或者 openvpn /etc/***.ovpn > /dev/null & #后台连接

显示报错了!

在这里插入图片描述

然后把 ta.key 往server端创建的 sen 用户的目录里传一份就可以了,变成了如下图所示:

在这里插入图片描述

再次执行:

在这里插入图片描述

连接完毕

测试:

连接client,ifconfig 查看tun0. server 端 ping ip 显示有了流量

在这里插入图片描述

【置顶】requests 常用方法

· 阅读需 12 分钟

【置顶】requests 常用方法

url = '要爬取的网址'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
response = requests(url=url, headers=headers)
response.encoding = 'utf-8'
page_text = response.text # 获取返回的解码后的字符串数据
json_data = response.json() # 获取返回的 json 解析后的数据
img_data = response.content # 获取二进制的返回内容

requests 模块初步使用

requests 是爬虫中一个基于网络请求的模块,安装方式:

pip install requests

不过如果你是用的是 Anaconda 环境,就不需要安装了,Anaconda 默认继承了 requests 模块。

requests 模块作用是模拟浏览器发起请求。

使用 requests 模块获取响应数据的代码编写流程:

  1. 指定 url
  2. 发起请求
  3. 获取响应数据(爬取到的页面源码数据)
  4. 持久化存储

实例:爬取搜狗首页的页面源码数据

import requests
# 1 指定url
url = 'https://www.sogou.com/'
# 2 发起请求get方法的返回值为响应对象
response = requests.get(url=url)
# 3 获取响应数据
#.text:返回的是字符串形式的响应数据
page_text = response.text
# 4 持久化存储
with open('./sougou.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

运行代码后,会在当前目录下新生成一个 sougo.html 文件。用浏览器打开后如下图所示,只有普通文本,样式都不见了。不过这没关系,因为爬虫往往只在意数据,不计较样式。

![img](/img/requests 模块应对 UA 检测和爬取动态网页/1.png)

参数动态化、UA 检测和 UA 伪装

光拿到搜狗的首页是没有用的 -- 这里什么都没有。如果我们想要拿到指定文本的搜索数据,该怎么办呢?

首先,要了解搜狗等搜索引擎的机制。一般情况下,搜索引擎的搜索请求都是 GET 请求。搜索的关键字放在路径中的查询字符串种传进去。搜狗也是如此。

比如,要搜索 jay,只需在浏览器中输入 https://www.sogou.com/web?query=jay 即可:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/2.png)

要搜索的内容是什么,就把路径中的 jay 改成什么就好了。

现在,让我们来实现一个简易网页采集器,基于搜狗针对指定不同的关键字将其对应的页面数据进行爬取。

这个需求,就是实现参数动态化

如果请求的 url 携带参数,且我们想要将携带的参数进行动态化操作那么我们必须:

  1. 将携带的动态参数以键值对的形式封装到一个字典中
  2. 将该字典作用到 get 方法的 params 参数中即可
  3. 需要将原始携带参数的 url 中将携带的参数删除

写成代码就是:

keyword = input('enter a keyword:')

# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query': keyword
}
url = 'https://www.sogou.com/web'
#params参数(字典):保存请求时url携带的参数
response = requests.get(url=url, params=params)

page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

比如输入 jay,打开新生成的文件,它长这个样子:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/3.png)

我们发现上面建议的采集器代码会产生两个问题:

  1. 页面乱码了
  2. 页面中的数据明显太少了,我们丢失了数据

首先,让我们解决乱码问题。这很容易,只需要加一行代码,使用一个 encoding 命令即可实现:

keyword = input('enter a keyword: ')

# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query': keyword
}
url = 'https://www.sogou.com/web'
# params参数(字典):保存请求时url携带的参数
response = requests.get(url=url, params=params)
# 修改响应数据的编码格式
# encoding返回的是响应数据的原始的编码格式,如果给其赋值则表示修改了响应数据的编码格式
response.encoding = 'utf-8'
page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

从结果来看,乱码问题是解决了。而且我们从中也了解到,数据量变少的原因:我们被搜狗的反爬策略限制了。

![img](/img/requests 模块应对 UA 检测和爬取动态网页/4.png)

处理乱码后,页面显示 异常访问请求 导致请求数据的缺失。这是因为网站后台已经检测出该次请求不是通过浏览器发起的请求而是通过爬虫程序发起的请求(不是通过浏览器发起的请求都是异常请求)。

网站的后台主要是通过查看请求的请求头中的 user-agent 判定请求是不是通过浏览器发起的。

什么是 User-Agent

  • 请求载体的身份标识,告诉服务器,使用的是什么工具(浏览器种类,操作系统类型,手机还是电脑,等)
  • 请求载体有且只有两种:
    • 浏览器
      • 浏览器的身份标识是统一固定,身份标识可以从抓包工具中获取。
    • 爬虫程序
      • 身份标识各自不同

这里就涉及到我们的第二种反爬机制,UA 检测:网站后台会检测请求对应的 User-Agent,以判定当前请求是否为异常请求。

UA 检测对应的反反爬策略是 UA 伪装:我们使用一个浏览器的 User-Agent,而不是爬虫的,去访问网页。

伪装流程:

  • 使用抓包工具捕获到某一个基于浏览器请求的 User-Agent 的值,将其伪装作用到一个字典中,将该字典作用到请求方法(get,post)的 headers 参数中即可。
  • 因为 UA 检测机制很多网站都会有,所以一般我们写爬虫代码的时候,都会加上 User-Agent 请求头,有备无患

使用代码表示就是:

keyword = input('enter a keyword:')
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 携带了请求参数的url,如果想要爬取不同关键字对应的页面,我们需要将url携带的参数进行动态化
# 实现参数动态化:
params = {
'query':keyword
}
url = 'https://www.sogou.com/web'
# params参数(字典):保存请求时url携带的参数
# 实现了UA伪装
response = requests.get(url=url, params=params, headers=headers)
# 修改响应数据的编码格式
# encoding返回的是响应数据的原始的编码格式,如果给其赋值则表示修改了响应数据的编码格式
response.encoding = 'utf-8'
page_text = response.text
fileName = keyWord + '.html'
with open(fileName, 'w', encoding='utf-8') as fp:
fp.write(page_text)
print(fileName, '爬取完毕!!!')

这样,我们就成功拿到搜索页面:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/5.png)

爬取动态页面

现在我们要爬取豆瓣电影中的电影详情数据

url 地址:[https://movie.douban.com/typerank?type_name=%E5%8A%A8%E4%BD%9C&type=5&interval_id=100:90&action=](https://movie.douban.com/typerank?type_name=动作&type=5&interval_id=100:90&action=)

我们想要页面中的电影信息:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/6.png)

如果我们还像之前那样,直接爬取这个网址,把代码写成这样:

url = 'https://movie.douban.com/typerank?type_name=%E5%8A%A8%E4%BD%9C&type=5&interval_id=100:90&action='
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
response = requests.get(url=url, headers=headers)
page_text = response.text
with open('movie.html', 'w', encoding='utf-8') as fp:
fp.write(page_text)

执行完上面的代码,我们再打开生成的 movie.html 文件,却发现,里面并没有我们想要的电影详情信息:

![img](/img/requests 模块应对 UA 检测和爬取动态网页/7.png)

这是因为这个网页的数据是动态加载的。

什么是动态加载的数据?

  • 我们通过 requests 模块进行数据爬取无法每次都实现可见即可得。
  • 有些数据是通过非浏览器地址栏中的 url 请求到的数据,而是通过其他请求方式(比如 ajax)请求到的数据。对于这些通过其他请求请求到的数据就是动态加载的数据。

那么该如何检测网页中是否存在动态加载数据呢?

我们当然可以想上面那样,先直接爬取页面看看,如果不能爬取到我们想要的数据,则说明网页很可能是动态加载的。

但是这个办法住农家有点蠢,我们更常用的检测网页是否是动态加载的方式是使用浏览器的抓包工具进行检测。

首先,基于抓包工具进行局部搜索。在当前网页中打开抓包工具,捕获到地址栏的 url 对应的数据包,在该数据包的 response 选项卡搜索我们想要爬取的数据,如果搜索到了结果则表示数据不是动态加载的,否则表示数据为动态加载的。

![browsercapturetoollocal](/img/requests 模块应对 UA 检测和爬取动态网页/8.png)

如果已经确定数据为动态加载,我们该如何捕获到动态加载的数据?

这就要基于抓包工具进行全局搜索。

定位到动态加载数据对应的数据包,从该数据包中就可以提取出

  • 请求的 url
  • 请求方式
  • 请求携带的参数
  • 看到响应数据

![browsercapturetoolglobal](/img/requests 模块应对 UA 检测和爬取动态网页/9.png)

我们找到这个网址的动态请求的链接和各种请求参数,并且知道了请求的方法是 get。有了这些参数,我们就可以实现我们的数据请求:

url = 'https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=0&limit=20'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 注意data的键值都写成字符串形式,没有值的话,就写成空字符串
data = {
'type': '5',
'interval_id': '100:90',
'action': '',
'start': '0',
'limit': '20',
}
response = requests.get(url=url, headers=headers, data=data)
# .json()将获取的字符串形式的json数据序列化成字典或者列表对象
data_list = response.json()
#解析出电影的名称+评分
for movie in data_list:
title = movie['title']
score = movie['score']
print(title, score)

可以通过修改 data 中的 start 和 limit 等数据,获取不同范围不同数目的结果。

基于抓包工具进行全局搜索不一定可以每次都能定位到动态加载数据对应的数据包,因为有可能动态加载的数据是经过加密的密文数据。这种情况我们后面会有所提及。

分页数据的爬取

需求:爬取肯德基的餐厅位置数据

url:http://www.kfc.com.cn/kfccda/storelist/index.aspx

分析:

  1. 在录入关键字的文本框中录入关键字按下搜索按钮,发起的是一个 ajax 请求。当前页面刷新出来的位置信息一定是通过 ajax 请求请求到的数据
  2. 基于抓包工具定位到该 ajax 请求的数据包,从该数据包中捕获到:
    • 请求的 url
    • 请求方式
    • 请求携带的参数
    • 看到响应数据

首先,我们先爬取第一的内容,注意这次的请求方法是 post,而不是 get 了:

# 爬取第一页的数据
url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
data = {
'cname': '',
'pid': '',
'keyword': '北京',
'pageIndex': '1',
'pageSize': '10',
}
# data参数是post方法中处理参数动态化的参数
response = requests.post(url=url, headers=headers, data=data)
data_list = response.json()
for store in data_list['Table1']:
store_name = store['storeName']
store_addr = store['addressDetail']
print(store_name, store_addr)

很显然,要爬取其他页码的数据,我们只需要讲 pageIndex 的参数修改成需要的页码即可:

url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=keyword'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
for page in range(1, 11):
data = {
'cname': '',
'pid': '',
'keyword': '北京',
'pageIndex': f'{page}',
'pageSize': '10',
}
response = requests.post(url=url, headers=headers, data=data)
data_list = response.json()
for store in data_list['Table1']:
store_name = store['storeName']
store_addr = store['addressDetail']
print(store_name, store_addr)

练习题

任务:爬取药监总局中的企业详情数据

url:http://125.35.6.84:81/xk/

需求:

  • 将首页中每一家企业的详情数据进行爬取。
    • 每一家企业详情页对应的数据
  • 将前 5 页企业的数据爬取即可。

难点:

  • 用不到数据解析
  • 所有的数据都是动态加载出来

提示:先试着将一家企业的详情页的详情数据爬取出来,然后再去爬取多家企业的数据。

完成代码如下:

import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
# 爬取前五页,每一家企业的详情
for page in range(1, 6):
url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsList'
data = {
'on': True,
'page': f'{page}',
'pageSize': '15',
'productName': '',
'conditionType': '1',
'applyname': '',
}
response = requests.post(url=url, headers=headers, data=data)
response_data = response.json()
for enterprise in response_data.get('list'):
url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsById'
data = {
'id': enterprise.get('ID')
}
enterprise_response = requests.post(url=url, headers=headers, data=data)
enterprise_response_data = enterprise_response.json()
print(enterprise_response_data)