Skip to content

04 插件开发

jinhongyang edited this page Apr 4, 2019 · 1 revision

1. 基础知识

1.1 执行阶段概念

nginx

  • 这几个阶段的存在,应该是 OpenResty 不同于其他多数 Web 平台编程的最明显特征了。由于 Nginx 把一个请求分成了很多阶段,这样第三方模块就可以根据自己行为,挂载到不同阶段进行处理达到目的。OpenResty 也应用了同样的特性。所不同的是,OpenResty 挂载的是我们编写的 Lua 代码。

1.2 插件执行流程

-- 加载的插件
local plugins = {}

-- 定义执行流对象
local _M = {}

-- 初始化项目配置
function _M.init(options)
    options = options or {}
    singletons.config = env
    plugins = loading_plugins(enable_plugins)
end

-- 初始化插件配置
function _M.init_worker()
    for _, plugin in ipairs(plugins) do
        plugin.handler:init_worker()
    end
end

-- 请求转发、重定向等操作
function _M.rewrite()
    for _, plugin in ipairs(plugins) do
        plugin.handler:rewrite()
    end
end

-- 请求转发、重定向等操作
function _M.access()
    for _, plugin in ipairs(plugins) do
        plugin.handler:access()
    end
end

-- 响应头部过滤处理
function _M.header_filter()
    local ctx = ngx.ctx
    for _, plugin in ipairs(plugins) do
        plugin.handler:header_filter(ctx)
    end
end

-- 响应体过滤处理
function _M.body_filter()
    local ctx = ngx.ctx
    for _, plugin in ipairs(plugins) do
        plugin.handler:body_filter(ctx)
    end
end

-- 会话完成后本地异步完成日志处理
function _M.log()
    local ctx = ngx.ctx
    for _, plugin in ipairs(plugins) do
        plugin.handler:log(ctx)
    end
end

return _M
  • 通过OpenResty的执行阶段,我们在代码中对每个阶段一一映射,这样我们就可以根据我们的需要,在不同的阶段直接完成大部分典型处理了。

2. 插件开发

2.1 插件目录

  • 进入插件目录plugins
  • 根据插件名称建立目录(例如:plugins/jwt-auth)

2.2 插件结构

  • 插件目录中一般包含两个文件

handler.lua(jwt-auth)

  • 插件主处理程序该模块声明需要继承plugins/base_plugin.lua
  • 因每个模块的功能可能只在某个或某几个阶段中执行,继承base_plugin后在模块中仅需要声明该模块中需要执行的阶段即可,其余阶段会调用base_plugin中的空阶段
  1. init_worker 阶段一般用于初始化插件配置
  2. rewrite 阶段一般用于转发、重定向、缓存等作用
  3. access 阶段一般用于IP 准入、接口权限等情况处理
  4. header_filter 阶段一般用于响应头部的过滤处理
  5. body_filter 阶段一般用于响应体的过滤处理
  6. log 阶段一般用于会话完成后本地异步完成日志记录
local JwtAuthService = jwt_auth:new()
local JwtAuthHandler = plugin:extend()

function JwtAuthHandler:new()
    JwtAuthHandler.super.new(self, "jwt-auth")
end

function JwtAuthHandler:init_worker()
    if ngx.worker.id() == 0 then
        local ok, err = ngx_timer_at(0, function(premature)
            -- 初始化插件配置
            JwtAuthService:init_config()
        end)
        if not ok then
            ngx_log(ngx_ERR, "failed to create the timer: ", err)
            return
        end
    end
end

function JwtAuthHandler:access(ctx)
    if ctx.api.is_auth == 1 then
        local cjson = require "cjson"
        local headers = ngx.req.get_headers()
        -- 获取项目私钥
        local jwtsecret = JwtAuthService:get_config_by_backendname(ctx.backend_name)
        if not jwtsecret then
            return response:error(401, "Project Secret Undefined"):response()
        end

        -- 获取令牌信息
        local authorization_header = headers["authorization"] or nil
        if not authorization_header then
            return response:error(401, "Header Token Undefined"):response()
        end

        -- 获取Header中Token信息
        local jwttoken = ngx_re_match(authorization_header, "\\s*[Bb]earer\\s+(.+)")
        if not jwttoken then
            return response:error(401, "Header Token Format Error"):response()
        end

        -- 校验签名
        local verifyinfo = jwt:verify(tostring(jwtsecret.secret_key), tostring(jwttoken[1]))
        if not verifyinfo["verified"] then
            return response:error(401, "Unauthorized"):response()
        end

        -- 负载数据不能为空否则认证失败
        local payload = verifyinfo["payload"]
        if not payload or not next(payload) then
            return response:error(401, "Unauthorized"):response()
        end

        -- 把负载数据写入Header,方便业务层应用
        for key, value in pairs(payload) do
            local header_key = string_upper("JWT-" .. string_gsub(key, "_", "-"))
            ngx.req.set_header(header_key, value)
        end
    end
end

return JwtAuthHandler

service.lua(jwt-auth)

  • 插件服务模块,主要处理插件中在DB和缓存上的读写操作
local _M = {}

function _M:new()
    local instance = {}
    instance.db = pool:new()
    instance.cachekey = "jwt-auth"
    setmetatable(instance, {
        __index = self
    })
    return instance
end

-- 初始化插件配置到缓存中
function _M:init_config()
    --读取系统配置规则
    local secrets = self.db:query("select project_name, secret_key, secret_alg from jwts")
    if secrets and next(secrets) ~= nil then
        for _, secret in pairs(secrets) do
            local success, error = ngx_cache:set(self.cachekey, secret.project_name, secret)
            ngx_log(ngx_DEBUG, string_format("CREATE JWT SECRET [%s] status:%s error:%s", secret.project_name,
                success, error))
        end
    end
end

-- 根据项目标识更新JWT配置
function _M:update_config_by_backendname(backendname)
    local status = false
    if not backendname then
        return status
    end
    local secrets = self.db:query("select project_name, secret_key, secret_alg from jwts where project_name = ? limit 1",
        { tostring(backendname) })
    if secrets and next(secrets) then
        local succ, err = ngx_cache:set(self.cachekey, backendname, secrets[1])
        if succ then
            status = true
        end
        ngx_log(ngx_DEBUG, string_format("UPDATE JWT SECRET [%s] status:%s error:%s", backendname, succ, err))
    end
    return status
end

-- 根据项目标识获取JWT配置
function _M:get_config_by_backendname(backendname)
    if not backendname or type(backendname) ~= "string" or string_len(backendname) <= 0 then
        return nil
    end
    local jwtconf = ngx_cache:get(self.cachekey, backendname)
    if not jwtconf then
        local succ = self:update_config_by_backendname(backendname)
        if succ then
            jwtconf = ngx_cache:get(self.cachekey, backendname)
        end
    end
    return jwtconf
end

return _M