OpenResty使用Lua大全(十二)实战: 动手实现一个网关框架

news2024/11/27 14:45:14

文章目录

  • 系列文章索引
  • 一、网关基本介绍
    • 1、网关常用功能
    • 2、本次实战目标
    • 3、orange介绍
    • 4、安装orange
  • 二、动手实现网关
    • 1、主入口
      • (1)nginx的conf配置
      • (2)网关lua:gateway.lua
      • (3)启动
    • 2、gateway网关的插件管理
      • (1)实现思路
      • (2)基础父类
      • (3)工具类
      • (4)基础插件类
      • (5)实战:签名验证的插件
      • (6)设置配置文件
    • 3、根据配置文件加载插件
      • (1)编写读取配置文件内容的类库
      • (2)针对字符串的工具类
      • (3)字符串与json对象编码的工具类
      • (4)编写配置文件加载类库
      • (5)改造入口gateway.lua
      • (6)更改nginx.conf
      • (7)重启nginx,查看nginx日志
    • 4、签名验证插件完善
      • (1)handler.lua进行改造
      • (2)gateway.lua中access阶段进行改造
      • (3)在utils工具类加入对table的操作
      • (4)启动nginx,利用之前编写的java代码,模拟请求
    • 5、实现黑名单插件
      • (1)使用封装的redis库
      • (2)创建插件
      • (3)完善工具类
      • (4)配置文件加入插件
      • (4)测试
    • 6、实现限流插件
      • (1)新建插件
      • (2)配置文件加入插件
      • (3)测试
  • 三、总结

系列文章索引

OpenResty使用Lua大全(一)Lua语法入门实战
OpenResty使用Lua大全(二)在OpenResty中使用Lua
OpenResty使用Lua大全(三)OpenResty使用Json模块解析json
OpenResty使用Lua大全(四)OpenResty中使用Redis
OpenResty使用Lua大全(五)OpenResty中使用MySQL
OpenResty使用Lua大全(六)OpenResty发送http请求
OpenResty使用Lua大全(七)OpenResty使用全局缓存
OpenResty使用Lua大全(八)OpenResty执行流程与阶段详解
OpenResty使用Lua大全(九)实战:nginx-lua-redis实现访问频率控制
OpenResty使用Lua大全(十)实战: Lua + Redis 实现动态封禁 IP
OpenResty使用Lua大全(十一)实战: nginx实现接口签名安全认证
OpenResty使用Lua大全(十二)实战: 动手实现一个网关框架

一、网关基本介绍

1、网关常用功能

统一入口;
安全:黑名单、权限身份认证;
限流:实现微服务访问流量计算,基于流量计算分析进行限流,可以定义多种限流规则;
缓存:数据缓存;
日志:记录日志;
监控:记录请求响应数据,api耗时分析,性能监控;
重试:异常重试;
熔断:降级。

2、本次实战目标

利用openresty+lua实现网关的部分需求;主要搭建好框架,可以在此基础上扩展。

自己了解深入网关
基于openresty开发小型网关框架,主要介绍逻辑,和实现方式;有些生产环境上面去使用,还是需要大家进行改造。
在这里插入图片描述

3、orange介绍

官网:http://orange.sumory.com

安装依赖
1)OpenResty: 版本应在1.9.7.3+
需要编译OpenResty时添加--with-http_stub_status_module

2)lor框架

git clone https://github.com/sumory/lor
cd lor
make install

3)MySQL
数据库名:orange
安装mysql数据库
执行相应的版本的数据库脚本

4、安装orange

git clone https://github.com/sumory/orange.git
使用的是v0.5.0及以上的版本, 可以通过make install将Orange安装到系统中。
执行此命令后, 以下两部分将被安装:
/usr/local/orange #orange运行时需要的文件
/usr/local/bin/orange #orange命令行工具

注意: 报错/usr/bin/env: ‘resty’: No such file or directory
在/usr/bin/目录下创建resty的软链接
#sudo ln -s /usr/local/openresty/bin/resty /usr/bin/resty

执行 orange help

Orange v0.6.4, OpenResty/Nginx API Gateway.

Usage: orange COMMAND [OPTIONS]

The commands are:

stop Stop current Orange
version Show the version of Orange
restart Restart Orange
reload Reload the config of Orange
store Init/Update/Backup Orange store
help Show help tips
start Start the Orange Gateway

根据以上两种方式选择使用sh start.sh或orange start来启动Orange。
Orange启动成功后, Dashboard和API server也随之启动:

内置的Dashboard可通过http://localhost:9999访问
API Server默认在7777端口监听,如不需要API Server可删除nginx.conf里对应的配置

二、动手实现网关

1、主入口

(1)nginx的conf配置

#user  nobody;
worker_processes  1;

error_log  logs/error.log;
error_log  logs/debug.log  debug;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  text/html;
	charset utf-8;
    sendfile        on;
    keepalive_timeout  65;
	
	resolver 8.8.8.8;
	
	upstream default_upstream {
        server localhost:8080;
    }
	
	#----------------------------nginx gateway configuration-----------------------------
	# lua文件地址
    lua_package_path '/usr/local/lua/?.lua;;';
    lua_code_cache on;
		
	lua_shared_dict shared_ip_blacklist 1m; #定义ip_blacklist 本地缓存变量
		
	# 引用我们的 网关lua文件
	init_by_lua_block {
		
        local gateway = require("gateway.gateway")

        context = {
            gateway = gateway
        }
    }

    init_worker_by_lua_block {
        local gateway = context.gateway
        gateway.init_worker()
    }
	
    server {
        listen       80;
        #server_name  www.server1.com;
		
		location = /favicon.ico {
            log_not_found off;
            access_log off;
        }
		
		location / {
            set $upstream_host $host;
            set $upstream_url 'http://default_upstream';

            rewrite_by_lua_block {
                local gateway = context.gateway
                gateway.redirect()
                gateway.rewrite()
            }

            access_by_lua_block {
                local gateway = context.gateway
                gateway.access()
            }

            # proxy
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Scheme $scheme;
            proxy_set_header Host $upstream_host;
            proxy_pass $upstream_url;


            header_filter_by_lua_block {
                local gateway = context.gateway
                gateway.header_filter()
            }

            body_filter_by_lua_block {
                local gateway = context.gateway
                gateway.body_filter()
            }

            log_by_lua_block {
                local gateway = context.gateway
                gateway.log()
            }
        }
    }
	
	server {  
        listen 8080;  
  
        location /world {  
            echo "hello world";  
        }  
    }  
}

(2)网关lua:gateway.lua

创建/usr/local/lua/gateway/gateway.lua文件

mkdir -p /usr/local/lua/gateway
vi /usr/local/lua/gateway/gateway.lua
local gateway = {}

function gateway.init()
	ngx.log(ngx.DEBUG, "===========gateway.init============");
end

function gateway.init_worker()
	ngx.log(ngx.DEBUG, "===========gateway.init_worker============");
end

function gateway.redirect()
	ngx.log(ngx.DEBUG, "===========gateway.redirect============");
end

function gateway.rewrite()
	ngx.log(ngx.DEBUG, "===========gateway.rewrite============");
end

function gateway.access()
	ngx.log(ngx.DEBUG, "===========gateway.access============");
end

function gateway.header_filter()
	ngx.log(ngx.DEBUG, "===========gateway.header_filter============");
end

function gateway.body_filter()
	ngx.log(ngx.DEBUG, "===========gateway.body_filter============");
end

function gateway.log()
	ngx.log(ngx.DEBUG, "===========gateway.log============");
end

return gateway;

(3)启动

启动nginx,访问
在这里插入图片描述
看debug日志:
在这里插入图片描述

这样,我们从nginx的入口,就调用到了我们的lua脚本。
后续我们开发网关的功能,只需要扩展gateway.lua就可以了。

2、gateway网关的插件管理

(1)实现思路

我们需要设计一个方案,可以动态的插拔插件,每个插件代表不同的功能

设计一个方便可插拔的插件,在文档目录结构方面进行这样的设计

–gateway #网关gateway主目录
—lib #存放第三方类库
—common #存放一些工具类库
—conf #存放配置文件
-----gateway.conf #定义gateway网关的一些相关配置
—plugins #存放所有插件的目录
-----base_plugin.lua #插件基类
-----XXXX #某个插件的目录名
-------handler.lua #某个插件的实现类,统一命名为handler,这个是用目录名区分插件,这样的设计有特别用途

mkdir -p /usr/local/lua/gateway/plugins
mkdir -p /usr/local/lua/gateway/lib
mkdir -p /usr/local/lua/gateway/common
mkdir -p /usr/local/lua/gateway/conf

(2)基础父类

存放了公共的类库
classic脚本使用了object的基类,类似java的object类;
以后我们所设计的类,只要在此类上面进行扩展即可,此基类object,大家不需要完全了解,只需要知道此脚本类似于object基类就行。

vi /usr/local/lua/gateway/lib/classic.lua
local Object = {}
Object.__index = Object


function Object:new()
end


function Object:extend()
  local cls = {}
  for k, v in pairs(self) do
    if k:find("__") == 1 then
      cls[k] = v
    end
  end
  cls.__index = cls
  cls.super = self
  setmetatable(cls, self)
  return cls
end


function Object:implement(...)
  for _, cls in pairs({...}) do
    for k, v in pairs(cls) do
      if self[k] == nil and type(v) == "function" then
        self[k] = v
      end
    end
  end
end


function Object:is(T)
  local mt = getmetatable(self)
  while mt do
    if mt == T then
      return true
    end
    mt = getmetatable(mt)
  end
  return false
end


function Object:__tostring()
  return "Object"
end


function Object:__call(...)
  local obj = setmetatable({}, self)
  obj:new(...)
  return obj
end


return Object

(3)工具类

在common目录下创建的是一些公共方法脚本类

vi /usr/local/lua/gateway/common/utils.lua
local string_find = string.find

local _M = {}

function _M.debug_log(msg)
    ngx.log(ngx.DEBUG, msg);
end

function _M.warn_log(msg)
    ngx.log(ngx.WARN, msg);
end

function _M.error_log(msg)
    ngx.log(ngx.ERR, msg);
end

function _M.load_module_if_exists(module_name)
    local status, res = pcall(require, module_name)
    if status then
        return true, res
        -- Here we match any character because if a module has a dash '-' in its name, we would need to escape it.
    elseif type(res) == "string" and string_find(res, "module '"..module_name.."' not found", nil, true) then
        return false
    else
        error(res)
    end
end

return _M

(4)基础插件类

vi /usr/local/lua/gateway/plugins/base_plugin.lua
local utils = require("gateway.common.utils")
local Object = require("gateway.lib.classic")

local BasePlugin = Object:extend()

function BasePlugin:new(name)
    self._name = name
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": new")
end

function BasePlugin:get_name()
    return self._name
end

function BasePlugin:init_worker()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": init_worker")
end

function BasePlugin:redirect()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": redirect")
end

function BasePlugin:rewrite()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": rewrite")
end

function BasePlugin:access()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": access")
end

function BasePlugin:header_filter()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": header_filter")
end

function BasePlugin:body_filter()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": body_filter")
end

function BasePlugin:log()
    utils.debug_log("BasePlugin executing plugin \""..self._name.."\": log")
end

return BasePlugin

(5)实战:签名验证的插件

在plugins目录下新建sign_auth目录
在sign_auth目录下新建handler.lua

mkdir -p /usr/local/lua/gateway/plugins/sign_auth
vi /usr/local/lua/gateway/plugins/sign_auth/handler.lua
local utils = require("gateway.common.utils")
local BasePlugin = require("gateway.plugins.base_plugin")

local SignAuthHandler = BasePlugin:extend()
SignAuthHandler.PRIORITY = 0

function SignAuthHandler:new()
    SignAuthHandler.super.new(self, "sign_auth-plugin")
    utils.debug_log("===========SignAuthHandler.new============");
end

function SignAuthHandler:access()
    SignAuthHandler.super.access(self)
    utils.debug_log("===========SignAuthHandler.access============");
end

return SignAuthHandler;

(6)设置配置文件

到此插件已经设计完成,现在我们需要设计配置文件,有哪些可以配置

vi /usr/local/lua/gateway/conf/gateway.conf
{
    "plugins": [       #插件属性,拥有哪些插件
        "sign_auth"    #此名称就是 插件的目录名
    ]
}

注意,不能带注释!!!

设计到这里,我们基本雏形已经完成

3、根据配置文件加载插件

(1)编写读取配置文件内容的类库

vi /usr/local/lua/gateway/common/io.lua
--- 
-- :P some origin code is from https://github.com/Mashape/kong/blob/master/kong/tools/io.lua
-- modified by sumory.wu

local stringy = require("gateway.common.stringy")

local _M = {}

---
-- Checks existence of a file.
-- @param path path/file to check
-- @return `true` if found, `false` + error message otherwise
function _M.file_exists(path)
    local f, err = io.open(path, "r")
    if f ~= nil then
        io.close(f)
        return true
    else
        return false, err
    end
end

---
-- Execute an OS command and catch the output.
-- @param command OS command to execute
-- @return string containing command output (both stdout and stderr)
-- @return exitcode
function _M.os_execute(command, preserve_output)
    local n = os.tmpname() -- get a temporary file name to store output
    local f = os.tmpname() -- get a temporary file name to store script
    _M.write_to_file(f, command)
    local exit_code = os.execute("/bin/bash "..f.." > "..n.." 2>&1")
    local result = _M.read_file(n)
    os.remove(n)
    os.remove(f)
    return preserve_output and result or string.gsub(string.gsub(result, "^"..f..":[%s%w]+:%s*", ""), "[%\r%\n]", ""), exit_code / 256
end

---
-- Check existence of a command.
-- @param cmd command being searched for
-- @return `true` of found, `false` otherwise
function _M.cmd_exists(cmd)
    local _, code = _M.os_execute("hash "..cmd)
    return code == 0
end

--- Kill a process by PID.
-- Kills the process and waits until it's terminated
-- @param pid_file the file containing the pid to kill
-- @param signal the signal to use
-- @return `os_execute` results, see os_execute.
function _M.kill_process_by_pid_file(pid_file, signal)
    if _M.file_exists(pid_file) then
        local pid = stringy.strip(_M.read_file(pid_file))
        return _M.os_execute("while kill -0 "..pid.." >/dev/null 2>&1; do kill "..(signal and "-"..tostring(signal).." " or "")..pid.."; sleep 0.1; done")
    end
end

--- Read file contents.
-- @param path filepath to read
-- @return file contents as string, or `nil` if not succesful
function _M.read_file(path)
    local contents
    local file = io.open(path, "rb")
    if file then
        contents = file:read("*all")
        file:close()
    end
    return contents
end

--- Write file contents.
-- @param path filepath to write to
-- @return `true` upon success, or `false` + error message on failure
function _M.write_to_file(path, value)
    local file, err = io.open(path, "w")
    if err then
        return false, err
    end

    file:write(value)
    file:close()
    return true
end


--- Get the filesize.
-- @param path path to file to check
-- @return size of file, or `nil` on failure
function _M.file_size(path)
    local size
    local file = io.open(path, "rb")
    if file then
        size = file:seek("end")
        file:close()
    end
    return size
end

return _M

(2)针对字符串的工具类

vi /usr/local/lua/gateway/common/stringy.lua
local string_gsub = string.gsub
local string_find = string.find
local table_insert = table.insert

local _M = {}

function _M.trim_all(str)
    if not str or str == "" then return "" end
    local result = string_gsub(str, " ", "")
    return result
end

function _M.strip(str)
    if not str or str == "" then return "" end
    local result = string_gsub(str, "^ *", "")
    result = string_gsub(result, "( *)$", "")
    return result
end


function _M.split(str, delimiter)
    if not str or str == "" then return {} end
    if not delimiter or delimiter == "" then return { str } end

    local result = {}
    for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do
        table_insert(result, match)
    end
    return result
end

function _M.startswith(str, substr)
    if str == nil or substr == nil then
        return false
    end
    if string_find(str, substr) ~= 1 then
        return false
    else
        return true
    end
end

function _M.endswith(str, substr)
    if str == nil or substr == nil then
        return false
    end
    local str_reverse = string.reverse(str)
    local substr_reverse = string.reverse(substr)
    if string.find(str_reverse, substr_reverse) ~= 1 then
        return false
    else
        return true
    end
end

return _M

(3)字符串与json对象编码的工具类

vi /usr/local/lua/gateway/common/json.lua
local cjson = require("cjson.safe")

local _M = {}

function _M.encode(data, empty_table_as_object)
    if not data then return nil end

    if cjson.encode_empty_table_as_object then
        -- empty table default is arrya
        cjson.encode_empty_table_as_object(empty_table_as_object or false)
    end

    if require("ffi").os ~= "Windows" then
        cjson.encode_sparse_array(true)
    end

    return cjson.encode(data)
end


function _M.decode(data)
    if not data then return nil end

    return cjson.decode(data)
end


return _M

(4)编写配置文件加载类库

vi /usr/local/lua/gateway/common/config_loader.lua
local json = require("gateway.common.json")
local IO = require("gateway.common.io")

local _M = {}

function _M.load(config_path)
    config_path = config_path or "/usr/local/lua/gateway/conf/gateway.conf"
    local config_contents = IO.read_file(config_path)

    if not config_contents then
        ngx.log(ngx.ERR, "No configuration file at: ", config_path)
        os.exit(1)
    end

    local config = json.decode(config_contents)
    return config, config_path
end

return _M

(5)改造入口gateway.lua

vi /usr/local/lua/gateway/gateway.lua
local utils = require("gateway.common.utils")
local config_loader = require("gateway.common.config_loader")

local function load_node_plugins(config)
  utils.debug_log("===========load_node_plugins============");
  local plugins = config.plugins --插件列表
  local sorted_plugins = {} --按照优先级的插件集合
  for _, v in ipairs(plugins) do
    local loaded, plugin_handler = utils.load_module_if_exists("gateway.plugins." .. v .. ".handler")
    if not loaded then
            utils.warn_log("The following plugin is not installed or has no handler: " .. v)
        else
            utils.debug_log("Loading plugin: " .. v)
            table.insert(sorted_plugins, {
                name = v,
                handler = plugin_handler(), --插件
            })
        end
  end
  --表按照优先级排序
  table.sort(sorted_plugins, function(a, b)
        local priority_a = a.handler.PRIORITY or 0
        local priority_b = b.handler.PRIORITY or 0
        return priority_a > priority_b
    end)
  
  return sorted_plugins
end


local gateway= {}

function gateway.init(options)
  options = options or {}
  local config
  local status, err = pcall(function()
    --gateway的配置文件路径
        local conf_file_path = options.config
    utils.debug_log("Loading gateway conf : " .. conf_file_path)
        config = config_loader.load(conf_file_path)
    --加载配置的插件
        loaded_plugins = load_node_plugins(config)
    end)
  
  if not status or err then
        utils.error_log("Startup error: " .. err)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)  
    end

  ngx.log(ngx.DEBUG, "===========gateway.init============");
end

function gateway.init_worker()
        ngx.log(ngx.DEBUG, "===========gateway.init_worker============");
end

function gateway.redirect()
        ngx.log(ngx.DEBUG, "===========gateway.redirect============");
end

function gateway.rewrite()
        ngx.log(ngx.DEBUG, "===========gateway.rewrite============");
end

function gateway.access()
        ngx.log(ngx.DEBUG, "===========gateway.access============");
end

function gateway.header_filter()
        ngx.log(ngx.DEBUG, "===========gateway.header_filter============");
end

function gateway.body_filter()
        ngx.log(ngx.DEBUG, "===========gateway.body_filter============");
end

function gateway.log()
        ngx.log(ngx.DEBUG, "===========gateway.log============");
end

return gateway;

(6)更改nginx.conf

init_by_lua_block {

    local gateway = require("gateway.gateway")

    local config_file = "/usr/local/lua/gateway/conf/gateway.conf"

    gateway.init({
      config = config_file
    })

    context = {
        gateway = gateway
    }
}

(7)重启nginx,查看nginx日志

nginx -s reload

访问一下:
在这里插入图片描述

在这里插入图片描述

4、签名验证插件完善

(1)handler.lua进行改造

vi /usr/local/lua/gateway/plugins/sign_auth/handler.lua
local utils = require("gateway.common.utils")
local BasePlugin = require("gateway.plugins.base_plugin")
local redis = require "resty.redis"  --引入redis模块

local function close_redis(red)  
    if not red then  
        return
    end  
    --释放连接(连接池实现)  
    local pool_max_idle_time = 10000 --毫秒  
    local pool_size = 100 --连接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
    if not ok then  
        utils.error_log("set keepalive error : "..err)  
    end  
end

--检验请求的sign签名是否正确
--params:传入的参数值组成的table
--secret:项目secret,根据appid找到secret
local function signcheck(params,secret)
    --判断参数是否为空,为空报异常
    if utils.isTableEmpty(params) then
        local mess="params table is empty"
        utils.error_log(mess)
        return false,mess
    end
    
    --判断是否有签名参数
    local sign = params["sign"]
    if sign == nil then
        local mess="params sign is nil"
        utils.error_log(mess)
        return false,mess
    end
    
    --是否存在时间戳的参数
    local timestamp = params["time"]
    if timestamp == nil then
        local mess="params timestamp is nil"
        utils.error_log(mess)
        return false,mess
    end
    --时间戳有没有过期,10秒过期
    local now_mill = ngx.now() * 1000 --毫秒级
    if now_mill - timestamp > 10000 then
        local mess="params timestamp is 过期"
        utils.error_log(mess)
        return false,mess
    end
    
    local keys, tmp = {}, {}

    --提出所有的键名并按字符顺序排序
    for k, _ in pairs(params) do 
        if k ~= "sign" then --去除掉
            keys[#keys+1]= k
        end
    end
    table.sort(keys)
    --根据排序好的键名依次读取值并拼接字符串成key=value&key=value
    for _, k in pairs(keys) do
        if type(params[k]) == "string" or type(params[k]) == "number" then 
            tmp[#tmp+1] = k .. "=" .. tostring(params[k])
        end
    end
    --将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比,
    local signchar = table.concat(tmp, "&") .."&"..secret
    local rightsign = ngx.md5(signchar);
    if sign ~= rightsign then
        --如果签名错误返回错误信息并记录日志,
        local mess="sign error: sign,"..sign .. " right sign:" ..rightsign.. " sign_char:" .. signchar
        utils.error_log(mess)
        return false,mess
    end
    return true
end

local SignAuthHandler = BasePlugin:extend()
SignAuthHandler.PRIORITY = 0

function SignAuthHandler:new()
    SignAuthHandler.super.new(self, "sign_auth-plugin")
    utils.debug_log("===========SignAuthHandler.new============");
end

function SignAuthHandler:access()
    SignAuthHandler.super.access(self)
    utils.debug_log("===========SignAuthHandler.access============");
    local params = {}

    local get_args = ngx.req.get_uri_args();
    
    local appid = get_args["appid"];
    
    if appid == nil then
        ngx.say("appid is empty,非法请求");
        return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
    end
    
    ngx.req.read_body()
    local post_args = ngx.req.get_post_args();

    utils.union(params,get_args)
    params = utils.union(params,post_args)
    
    local red = redis:new()  --创建一个对象,注意是用冒号调用的
    
    --设置超时(毫秒)  
    red:set_timeout(1000) 
    --建立连接  
    local host = "127.0.0.1"  
    local port = 6379
    local ok, err = red:connect(host, port)
    if not ok then  
        close_redis(red)
        utils.error_log("Cannot connect");
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)   
    end  
    
    --得到此appid对应的secret
    local resp, err = red:hget("apphash",appid)
    if not resp or (resp == ngx.null) then  
        close_redis(red)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败
    end 
    --resp存放着就是appid对应的secret       
    local checkResult,mess = signcheck(params,resp)

    if not checkResult then
        ngx.say(mess);
        return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
    end
end

return SignAuthHandler;

(2)gateway.lua中access阶段进行改造

vi /usr/local/lua/gateway/gateway.lua
function gateway.access()
    ngx.log(ngx.DEBUG, "===========gateway.access============");
    for _, plugin in ipairs(loaded_plugins) do
        ngx.log(ngx.DEBUG, "==gateway.access name==" .. plugin.name);
        plugin.handler:access()
    end
end

(3)在utils工具类加入对table的操作

vi /usr/local/lua/gateway/common/utils.lua
--判断table是否为空
function _M.isTableEmpty(t)
    return t == nil or next(t) == nil
end

--两个table合并
function _M.union(table1,table2)
    for k, v in pairs(table2) do
        table1[k] = v
    end
    return table1
end

(4)启动nginx,利用之前编写的java代码,模拟请求

5、实现黑名单插件

(1)使用封装的redis库

vi /usr/local/lua/gateway/lib/redis.lua
local redis_c = require "resty.redis"

local ok, new_tab = pcall(require, "table.new")
if not ok or type(new_tab) ~= "function" then
    new_tab = function (narr, nrec) return {} end
end

local _M = new_tab(0, 155)
_M._VERSION = '0.01'

local commands = {
    "append",            "auth",              "bgrewriteaof",
    "bgsave",            "bitcount",          "bitop",
    "blpop",             "brpop",
    "brpoplpush",        "client",            "config",
    "dbsize",
    "debug",             "decr",              "decrby",
    "del",               "discard",           "dump",
    "echo",
    "eval",              "exec",              "exists",
    "expire",            "expireat",          "flushall",
    "flushdb",           "get",               "getbit",
    "getrange",          "getset",            "hdel",
    "hexists",           "hget",              "hgetall",
    "hincrby",           "hincrbyfloat",      "hkeys",
    "hlen",
    "hmget",              "hmset",      "hscan",
    "hset",
    "hsetnx",            "hvals",             "incr",
    "incrby",            "incrbyfloat",       "info",
    "keys",
    "lastsave",          "lindex",            "linsert",
    "llen",              "lpop",              "lpush",
    "lpushx",            "lrange",            "lrem",
    "lset",              "ltrim",             "mget",
    "migrate",
    "monitor",           "move",              "mset",
    "msetnx",            "multi",             "object",
    "persist",           "pexpire",           "pexpireat",
    "ping",              "psetex",            "psubscribe",
    "pttl",
    "publish",      --[[ "punsubscribe", ]]   "pubsub",
    "quit",
    "randomkey",         "rename",            "renamenx",
    "restore",
    "rpop",              "rpoplpush",         "rpush",
    "rpushx",            "sadd",              "save",
    "scan",              "scard",             "script",
    "sdiff",             "sdiffstore",
    "select",            "set",               "setbit",
    "setex",             "setnx",             "setrange",
    "shutdown",          "sinter",            "sinterstore",
    "sismember",         "slaveof",           "slowlog",
    "smembers",          "smove",             "sort",
    "spop",              "srandmember",       "srem",
    "sscan",
    "strlen",       --[[ "subscribe",  ]]     "sunion",
    "sunionstore",       "sync",              "time",
    "ttl",
    "type",         --[[ "unsubscribe", ]]    "unwatch",
    "watch",             "zadd",              "zcard",
    "zcount",            "zincrby",           "zinterstore",
    "zrange",            "zrangebyscore",     "zrank",
    "zrem",              "zremrangebyrank",   "zremrangebyscore",
    "zrevrange",         "zrevrangebyscore",  "zrevrank",
    "zscan",
    "zscore",            "zunionstore",       "evalsha"
}

local mt = { __index = _M }

local function is_redis_null( res )
    if type(res) == "table" then
        for k,v in pairs(res) do
            if v ~= ngx.null then
                return false
            end
        end
        return true
    elseif res == ngx.null then
        return true
    elseif res == nil then
        return true
    end

    return false
end

function _M.close_redis(self, redis)  
    if not redis then  
        return  
    end  
    --释放连接(连接池实现)
    local pool_max_idle_time = self.pool_max_idle_time --最大空闲时间 毫秒  
    local pool_size = self.pool_size --连接池大小  
    
    local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size)  
    if not ok then  
        ngx.say("set keepalive error : ", err)  
    end  
end  

-- change connect address as you need
function _M.connect_mod( self, redis )
    redis:set_timeout(self.timeout)
        
    local ok, err = redis:connect(self.ip, self.port)
    if not ok then  
        ngx.say("connect to redis error : ", err)  
        return self:close_redis(redis)  
    end

    if self.password ~= "" then ----密码认证
    
        local count, err = redis:get_reused_times()
        if 0 == count then ----新建连接,需要认证密码
            ok, err = redis:auth(self.password)
            if not ok then
                ngx.say("failed to auth: ", err)
                return
            end
        elseif err then  ----从连接池中获取连接,无需再次认证密码
            ngx.say("failed to get reused times: ", err)
            return
        end
    end

    return ok,err;
end

function _M.init_pipeline( self )
    self._reqs = {}
end

function _M.commit_pipeline( self )
    local reqs = self._reqs

    if nil == reqs or 0 == #reqs then
        return {}, "no pipeline"
    else
        self._reqs = nil
    end

    local redis, err = redis_c:new()
    if not redis then
        return nil, err
    end

    local ok, err = self:connect_mod(redis)
    if not ok then
        return {}, err
    end

    redis:init_pipeline()
    for _, vals in ipairs(reqs) do
        local fun = redis[vals[1]]
        table.remove(vals , 1)

        fun(redis, unpack(vals))
    end

    local results, err = redis:commit_pipeline()
    if not results or err then
        return {}, err
    end

    if is_redis_null(results) then
        results = {}
        ngx.log(ngx.WARN, "is null")
    end
    -- table.remove (results , 1)

    --self.set_keepalive_mod(redis)
    self:close_redis(redis)  

    for i,value in ipairs(results) do
        if is_redis_null(value) then
            results[i] = nil
        end
    end

    return results, err
end

local function do_command(self, cmd, ... )
    if self._reqs then
        table.insert(self._reqs, {cmd, ...})
        return
    end

    local redis, err = redis_c:new()
    if not redis then
        return nil, err
    end

    local ok, err = self:connect_mod(redis)
    if not ok or err then
        return nil, err
    end

    redis:select(self.db_index)
    
    local fun = redis[cmd]
    local result, err = fun(redis, ...)
    if not result or err then
        -- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err)
        return nil, err
    end

    if is_redis_null(result) then
        result = nil
    end

    --self.set_keepalive_mod(redis)
    self:close_redis(redis)  

    return result, err
end

for i = 1, #commands do
    local cmd = commands[i]
    _M[cmd] =
            function (self, ...)
                return do_command(self, cmd, ...)
            end
end

function _M.new(self, opts)
    opts = opts or {}
    local timeout = (opts.timeout and opts.timeout * 1000) or 1000
    local db_index= opts.db_index or 0
    local ip = opts.ip or '127.0.0.1'
    local port = opts.port or 6379
    local password = opts.password or ""
    local pool_max_idle_time = opts.pool_max_idle_time or 60000
    local pool_size = opts.pool_size or 100

    return setmetatable({
            timeout = timeout,
            db_index = db_index,
            ip = ip,
            port = port,
            password = password,
            pool_max_idle_time = pool_max_idle_time,
            pool_size = pool_size,
            _reqs = nil }, mt)
end

return _M

(2)创建插件

mkdir -p /usr/local/lua/gateway/plugins/limit_ip
vi /usr/local/lua/gateway/plugins/limit_ip/handler.lua
local utils = require("gateway.common.utils")
local redis = require("gateway.lib.redis")  --引入redis模块
local BasePlugin = require("gateway.plugins.base_plugin")

local opts = {
    ip = "127.0.0.1",
    port = "6379",
    -- password = "123456",
    nil,
    db_index = 0
}

local LimitIpHandler = BasePlugin:extend()
LimitIpHandler.PRIORITY = 2

function LimitIpHandler:new()
    LimitIpHandler.super.new(self, "limit_ip-plugin")
    utils.debug_log("===========LimitIpHandler.new============");
end

function LimitIpHandler:access()
    LimitIpHandler.super.access(self)
    utils.debug_log("===========LimitIpHandler.access============");
    
    local key = "limit:ip:blacklist";
    local user_ip = utils.get_ip();
    local shared_ip_blacklist = ngx.shared.shared_ip_blacklist;
    
    --获得本地缓存的最新刷新时间
    local last_update_time = shared_ip_blacklist:get("last_update_time");
    
    if last_update_time ~= nil then 
        local dif_time = ngx.now() - last_update_time 
        if dif_time < 60 then --缓存1分钟,没有过期
            if shared_ip_blacklist:get(user_ip) then
                return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
            end
        end
    end
    
    local red = redis:new(opts)  --创建一个对象,注意是用冒号调用的
    
    local ip_blacklist, err = red:smembers(key);
    if err then
        utils.error_log("limit ip smembers");
    else
        --刷新本地缓存,重新设置
        shared_ip_blacklist:flush_all();
        
        if ip_blacklist ~= nil then            
            for i,bip in ipairs(ip_blacklist) do
                --本地缓存redis中的黑名单
                shared_ip_blacklist:set(bip,true);
            end
        end
        
        --设置本地缓存的最新更新时间
        shared_ip_blacklist:set("last_update_time",ngx.now());
    end
        
    if shared_ip_blacklist:get(ip) then
        return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
    end
    
end

return LimitIpHandler;

(3)完善工具类

vi /usr/local/lua/gateway/common/utils.lua
function _M.get_ip()
    local myIP = ngx.req.get_headers()["X-Real-IP"]
    if myIP == nil then
        myIP = ngx.req.get_headers()["x_forwarded_for"]
    end
    if myIP == nil then
        myIP = ngx.var.remote_addr
    end
    return myIP;
end

(4)配置文件加入插件

vi /usr/local/lua/gateway/conf/gateway.conf
{
    "plugins": [
        "limit_ip"
    ]
}

(4)测试

访问:
在这里插入图片描述

加入黑名单:

sadd limit:ip:blacklist 192.168.56.1

过一段时间,生效,发现被限制了:
在这里插入图片描述

6、实现限流插件

(1)新建插件

mkdir -p /usr/local/lua/gateway/plugins/limit_frequency
vi /usr/local/lua/gateway/plugins/limit_frequency/handler.lua
local utils = require("gateway.common.utils")
local redis = require("gateway.lib.redis")  --引入redis模块
local BasePlugin = require("gateway.plugins.base_plugin")

local opts = {
    ip = "127.0.0.1",
    port = "6379",
    db_index = 0
}

local LimitFrequencyHandler = BasePlugin:extend()
LimitFrequencyHandler.PRIORITY = 1

function LimitFrequencyHandler:new()
    LimitFrequencyHandler.super.new(self, "LimitFrequency-plugin")
    utils.debug_log("===========LimitFrequencyHandler.new============");
end


function LimitFrequencyHandler:access()
    LimitFrequencyHandler.super.access(self)
    utils.debug_log("===========LimitFrequencyHandler.access============");
    
    local user_ip = utils.get_ip();
    
    local key = "limit:frequency:"..user_ip;

    local red = redis:new(opts)  --创建一个对象,注意是用冒号调用的
    
    --得到此客户端IP的频次
    local resp, err = red:get(key)
    
    if err then
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败
    end 

    if resp == nil then   
        utils.debug_log("===========key set ============");
        local result,err = red:set(key, 1) -- 单位时间 第一次访问
        utils.debug_log("===========key expire ============");
        result,err = red:expire(key, 10) --10秒时间 过期
    end  

    if type(resp) == "string" then 
        if tonumber(resp) > 10 then -- 超过10次
            return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
        end
    end

    --调用API设置key  
    local ok, err = red:incr(key)  
    if err then
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 报错 
    end 

end

return LimitFrequencyHandler;

(2)配置文件加入插件

vi /usr/local/lua/gateway/conf/gateway.conf
{
    "plugins": [
        "limit_ip",
        "limit_frequency"
    ]
}

(3)测试

重启nginx:

10秒内访问超过10次,就会出现403 Forbidden。

三、总结

其实没必要自己手动实现一个网关,因为现有的开源网关功能很强大也很好用,比如kong,orange开源网关框架。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1530521.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

camunda 与 pycamunda学习

camunda 与 pycamunda 相关链接&#xff1a; camunda 官方社区&#xff1a;https://docs.camunda.org/manual/7.17/ 官方社区提供的REST_API:https://docs.camunda.org/manual/7.17/reference/rest/ GITHUB 社区&#xff1a;https://github.com/camunda-community-hub Git…

【机器学习】机器学习实验方法与原则(统计有效性检验详解)

统计有效性检验 假设的评估检验&#xff1a;问题1 • 效果估计 • 给定一个假设 在有限量数据 上的准确率 • 该准确率是否能准确估计 在其它未见数据上 的效果&#xff1f; 假设的评估检验&#xff1a;问题2 • h 1 在数据的一个样本集上表现优于 h 2 • h 1 总体…

ubuntu下samba匿名读写服务器

目的&#xff1a; 局域网内&#xff0c;ubuntu下&#xff0c;创建SAMBA文件共享服务器。匿名读写权限。为了开发项目组文件共享传输方便。 环境&#xff1a; X86_64电脑一台。 操作系统&#xff1a; Ubuntu 20.04 LTS 64位。 安装samba $ sudo apt-get install samba创建…

缅甸的大开发时代即将到来 缅文wordpress主题模板

Simplify WordPress外贸网站模板 Simplify WordPress外贸网站模板&#xff0c;简洁实用的外贸公司wordpress外贸建站模板。 https://www.jianzhanpress.com/?p4565

爬虫(七)

1.批量爬取知网数据 lxml:是 Python 的一个功能强大且易用的 XML 和 HTML 处理库。它提供了简单又轻巧的 API,使得解析、构建和操作 XML 和 HTML 文档变得非常方便。lxml 库通常用于处理 XML 和 HTML 文档,例如解析网页、处理配置文件等。openpyxl:是 Python 中用于操作 Ex…

Basic RNN

文章目录 回顾RNNRNN CellRNNCell的使用RNN的使用 RNN例子使用RNN Cell实现使用RNN实现 嵌入层 Embedding独热向量的缺点Embedding LSTMGRU(门控循环单元)练习 回顾 DNN&#xff08;全连接&#xff09;&#xff1a;和CNN相比&#xff0c;拥有巨大的参数量&#xff0c;CNN权重共…

哔哩哔哩秋招Java二面

前言 作者&#xff1a;晓宜 个人简介&#xff1a;互联网大厂Java准入职&#xff0c;阿里云专家博主&#xff0c;csdn后端优质创作者&#xff0c;算法爱好者 一面过后面试官叫我别走&#xff0c;然后就直接二面&#xff0c;二面比较简短&#xff0c;记录一下&#xff0c;希望可以…

charles使用教程 ---- 抓取https请求 修改请求

目录 简介 将 Charles 设置成系统代理 截取 Https 通讯信息 配置想要抓取数据的地址 在电脑上安装证书 设置HTTP和HTTPS代理 抓包工具charles修改请求和返回数据 简介 Charles 是在 Mac 下常用的网络封包截取工具&#xff0c;在做 移动开发时&#xff0c;我们为了调试与…

MNN createSession 之创建流水线后端(四)

系列文章目录 MNN createFromBuffer&#xff08;一&#xff09; MNN createRuntime&#xff08;二&#xff09; MNN createSession 之 Schedule&#xff08;三&#xff09; MNN createSession 之创建流水线后端&#xff08;四&#xff09; MNN Session::resize 之流水线编码&am…

【刷题】滑动窗口入门

送给大家一句话&#xff1a; 那脑袋里的智慧&#xff0c;就像打火石里的火花一样&#xff0c;不去打它是不肯出来的。——莎士比亚 滑动窗口入门 认识滑动窗口Leetcode 209. 长度最小的子数组题目描述算法思路 Leetcode 3. 无重复字符的最长子串题目描述算法思路 Leetcode 1004…

Invicti v24.3.0 for Windows - Web 应用程序安全测试

Invicti v24.3.0 for Windows - Web 应用程序安全测试 Invicti Standard 12 Mar 2024 v24.3.0 请访问原文链接&#xff1a;https://sysin.org/blog/invicti/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org Invicti 是一种自动…

什么是 JVM 双亲委派机制?

什么是 JVM 双亲委派机制&#xff1f; 题目 什么是 JVM 双亲委派机制&#xff1f; 推荐解析 通俗的说&#xff0c;双亲委派模型&#xff0c;就是加载类的时候&#xff0c;先请求其父类加载器去加载&#xff0c;如果父类加载器无法加载类&#xff0c;再尝试自己去加载类。如…

npm install不成功

解决办法&#xff1a; $env:NODE_OPTIONS"--openssl-legacy-provider" npm run dev

HarmonyOS应用开发实战 - Api9 拍照、拍视频、选择图片、选择视频、选择文件工具类

鸿蒙开发过程中&#xff0c;经常会进行系统调用&#xff0c;拍照、拍视频、选择图库图片、选择图库视频、选择文件。今天就给大家分享一个工具类。 1.话不多说&#xff0c;先展示样式 2.设计思路 根据官方提供的指南开发工具类&#xff0c;基础的拍照、拍视频、图库选照片、选…

【mybatis】objectwrapper解读

简介 在 MyBatis 中&#xff0c;ObjectWrapper 是一个关键的接口&#xff0c;用于详细封装了对象的属性信息。ObjectWrapper 主要用于内部操作&#xff0c;它抽象了对象的属性操作&#xff0c;使得 MyBatis 能够统一处理原生类型、Bean 对象以及 Map 集合等。 类图展示 主要功…

1、初识JVM

一、JVM是什么&#xff1f; JVM的英文全称是 Java Virtual Machine&#xff0c;其中文译名为Java虚拟机。它在本质上就是是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 JVM执行流程如下 二、JVM有哪些功能&#xff1f; 2.1 解释和运行 对字节码文…

vue:功能【xlsx】动态行内合并

场景&#xff1a;纯前端导出excel数据&#xff0c;涉及到列合并、行合并。 注&#xff09;当前数据表头固定&#xff0c;行内数据不固定。以第一列WM为判断条件&#xff0c;相同名字的那几行数据合并单元格。合并的那几行数据&#xff0c;后面的列按需求进行合并。 注&#x…

SpringBoot + Vue项目(显示+删除+回显家居)

文章目录 1.显示家居信息1.com/sun/furn/controller/FurnController.java 添加方法2.postman测试3.src/views/HomeView.vue 修改el-table 并清空数据池tableData4.src/views/HomeView.vue 发送请求并取出数据1.方法池2.created阶段调用list方法3.结果展示 5.src/utils/request.…

【python】flask服务端响应与重定向处理

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

C语言经典算法-8

文章目录 其他经典例题跳转链接41.基数排序法42.循序搜寻法&#xff08;使用卫兵&#xff09;43.二分搜寻法&#xff08;搜寻原则的代表&#xff09;44.插补搜寻法45.费氏搜寻法 其他经典例题跳转链接 C语言经典算法-1 1.汉若塔 2. 费式数列 3. 巴斯卡三角形 4. 三色棋 5. 老鼠…