【openresty】利用 JWT 实现令牌校验 | 模拟注册登录流程 | RunnerGo 自动化测试

一、什么是 JWT

JWT(JSON Web Token)是一种基于 JSON 的开放标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递 json 对象,常用来实现分布式用户认证。

它通常用于用户的登录鉴权,进行身份验证和信息交换。大致流程:

  1. 用户使用账号、密码登录,请求发送到 Authentication Server。
  2. Authentication Server 进行用户验证(查询数据库等),创建 JWT 字符串返回给客户端。
  3. 客户端请求接口访问资源时,请求头携带 JWT。
  4. Application Server 验证 JWT 合法性,做相应处理。

JWT 组成部分

JWT主要由三部分组成,通过点号分隔:头部(Header)、载荷(Payload)和签名(Signature)。

  • Header
{ "typ": "JWT", "alg": "HS256" }

JWT 头部主要由两部分组成:令牌的类型(typ)、加密签名的哈希算法(alg)。这部分内容用 Base64 编码后构成 JWT 的第一部分。

  • Payload

JWT 第二部分是 Payload,也是一个 Json 对象,除了包含自定义的数据外,JWT规范还定义了一些预定义的字段,用于存储有关令牌的标准信息。这些预定义字段通常称为 Claims,分为三个类别:注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。

以下是JWT Payload中的七个默认的注册声明字段(Registered Claims):

  • iss (issuer):发布令牌的实体,即签发者

  • sub (subject):令牌的主题

  • aud (audience):令牌接收者

  • exp (expiration time):令牌的过期时间,UNIX 时间戳

  • nbf (Not Before):令牌的生效时间,UNIX 时间戳

  • iat (Issued At):令牌的发布时间,UNIX 时间戳

  • jti (JWT ID):JWT的唯一标识符,用于标识该 JWT

还可以添加公共声明和私有声明来存储其他自定义的数据:

{
  "iss": "app",
  "sub": "user123",
  "exp": 1679680000,
  "roles": ["user", "admin"],
  "device": "mobile",
  
  "custom_data": {
    "key1": "value1",
    "key2": "value2"
  },
  
  "user_preferences": {
    "theme": "dark",
    "language": "en"
  }
}

载荷的内容也会被 Base64 编码,构成JWT的第二部分。这部分内容是可以反向反编码回原样的,所以不要在 JWT 中放敏感数据,以防信息泄露。

  • Signature

签名是 JWT的第三部分,通过将编码后的头部、编码后的载荷与一个密钥结合在一起,然后通过所选的哈希算法生成的。

-- 伪代码:
base64URL(hmac_sha256(base64URL(header).base64URL(payload), secret))

这个 secret 是私密密钥,保存在服务端,客户端无法获取。并且签名的加密算法要和 JWT 头部定义的加密算法一致,否则无效。

Application Server 进行验证,利用 JWT 前两段,用同一套哈希加密算法,和同一个 secret 计算一个签名值,与第三段比较,相同即认证成功。


JWT 优缺点

  1. 轻量且自包含:JWT 是一种紧凑的数据格式,可以轻松地在 HTTP 头部、URL 参数或 Cookie 中传输。由于它自包含,无需在服务器端存储会话信息,减轻了服务器的负担。
  2. 无状态性:由于 JWT 包含了所有必要的信息,服务器不需要在后台存储会话状态。这使得应用程序更容易扩展,因为服务器不需要在多个实例之间共享会话信息。
  3. 跨平台和语言、便于传输、可扩展、安全等。
  4. 劣势:由于有效期存储在 Token 中,JWT Token 一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁用用户,单纯使用 JWT 就无法做到。

二、JWT 安装&使用

基于 openresty 实现简单的用户注册登录身份鉴权,安装 lua-resty-jwt 库,可以使用 opm 包管理工具快捷安装 JWT 库。

  1. 执行 opm search lua-resty-jwt

在这里插入图片描述
安装 github 上 star 较高的 jwt 库 SkyLothar/lua-resty-jwt

  1. opm get SkyLothar/lua-resty-jwt

在这里插入图片描述

  1. cd /usr/local/openresty/site/lualib/resty/ && ls:查看是否安装成功,jwt.lua 文件是否存在。

在这里插入图片描述

添加 JWT 库路径

在 nginx 启动的配置文件 nginx.conf 中,添加上 JWT 库的路径。

lua_package_path "/usr/local/openresty/site/lualib/resty/?.lua;;";

导入 JWT 库使用:

local jwt = require "resty.jwt"

三、安装 http 客户端

lua-resty-http 库在本次实验中没有用到,但这是 openresty 中一个重要的 Lua 库,用于执行 HTTP 请求和处理响应。

安装过程很简单,直接执行下面三行即可,安装在 /usr/local/openresty/lualib/resty/

sudo wget https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http.lua /usr/local/openresty/lualib/resty/
sudo wget https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_headers.lua /usr/local/openresty/lualib/resty/
sudo wget https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_connect.lua /usr/local/openresty/lualib/resty/

这个路径是 openresty 默认查询 lua 库的路径,无需手动添加进配置文件中。

如果拉取比较慢,手动创建这三个文件即可(/usr/local/openresty/lualib/resty/):

  1. http.lua
local http_headers = require "resty.http_headers"

local ngx = ngx
local ngx_socket_tcp = ngx.socket.tcp
local ngx_req = ngx.req
local ngx_req_socket = ngx_req.socket
local ngx_req_get_headers = ngx_req.get_headers
local ngx_req_get_method = ngx_req.get_method
local str_lower = string.lower
local str_upper = string.upper
local str_find = string.find
local str_sub = string.sub
local tbl_concat = table.concat
local tbl_insert = table.insert
local ngx_encode_args = ngx.encode_args
local ngx_re_match = ngx.re.match
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_gsub = ngx.re.gsub
local ngx_re_find = ngx.re.find
local ngx_log = ngx.log
local ngx_DEBUG = ngx.DEBUG
local ngx_ERR = ngx.ERR
local ngx_var = ngx.var
local ngx_print = ngx.print
local ngx_header = ngx.header
local co_yield = coroutine.yield
local co_create = coroutine.create
local co_status = coroutine.status
local co_resume = coroutine.resume
local setmetatable = setmetatable
local tonumber = tonumber
local tostring = tostring
local unpack = unpack
local rawget = rawget
local select = select
local ipairs = ipairs
local pairs = pairs
local pcall = pcall
local type = type


-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
local HOP_BY_HOP_HEADERS = {
    ["connection"]          = true,
    ["keep-alive"]          = true,
    ["proxy-authenticate"]  = true,
    ["proxy-authorization"] = true,
    ["te"]                  = true,
    ["trailers"]            = true,
    ["transfer-encoding"]   = true,
    ["upgrade"]             = true,
    ["content-length"]      = true, -- Not strictly hop-by-hop, but Nginx will deal
                                    -- with this (may send chunked for example).
}


local EXPECTING_BODY = {
    POST  = true,
    PUT   = true,
    PATCH = true,
}


-- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot
-- be resumed. This protects user code from infinite loops when doing things like
-- repeat
--   local chunk, err = res.body_reader()
--   if chunk then -- <-- This could be a string msg in the core wrap function.
--     ...
--   end
-- until not chunk
local co_wrap = function(func)
    local co = co_create(func)
    if not co then
        return nil, "could not create coroutine"
    else
        return function(...)
            if co_status(co) == "suspended" then
                return select(2, co_resume(co, ...))
            else
                return nil, "can't resume a " .. co_status(co) .. " coroutine"
            end
        end
    end
end


-- Returns a new table, recursively copied from the one given.
--
-- @param   table   table to be copied
-- @return  table
local function tbl_copy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == "table" then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[tbl_copy(orig_key)] = tbl_copy(orig_value)
        end
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end


local _M = {
    _VERSION = '0.17.1',
}
_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version

local mt = { __index = _M }


local HTTP = {
    [1.0] = " HTTP/1.0\r\n",
    [1.1] = " HTTP/1.1\r\n",
}


local DEFAULT_PARAMS = {
    method = "GET",
    path = "/",
    version = 1.1,
}


local DEBUG = false


function _M.new(_)
    local sock, err = ngx_socket_tcp()
    if not sock then
        return nil, err
    end
    return setmetatable({ sock = sock, keepalive = true }, mt)
end


function _M.debug(d)
    DEBUG = (d == true)
end


function _M.set_timeout(self, timeout)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:settimeout(timeout)
end


function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:settimeouts(connect_timeout, send_timeout, read_timeout)
end

do
    local aio_connect = require "resty.http_connect"
    -- Function signatures to support:
    -- ok, err, ssl_session = httpc:connect(options_table)
    -- ok, err = httpc:connect(host, port, options_table?)
    -- ok, err = httpc:connect("unix:/path/to/unix.sock", options_table?)
    function _M.connect(self, options, ...)
        if type(options) == "table" then
            -- all-in-one interface
            return aio_connect(self, options)
        else
            -- backward compatible
            return self:tcp_only_connect(options, ...)
        end
    end
end

function _M.tcp_only_connect(self, ...)
    ngx_log(ngx_DEBUG, "Use of deprecated `connect` method signature")

    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    self.host = select(1, ...)
    self.port = select(2, ...)

    -- If port is not a number, this is likely a unix domain socket connection.
    if type(self.port) ~= "number" then
        self.port = nil
    end

    self.keepalive = true
    self.ssl = false

    return sock:connect(...)
end


function _M.set_keepalive(self, ...)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    if self.keepalive == true then
        return sock:setkeepalive(...)
    else
        -- The server said we must close the connection, so we cannot setkeepalive.
        -- If close() succeeds we return 2 instead of 1, to differentiate between
        -- a normal setkeepalive() failure and an intentional close().
        local res, err = sock:close()
        if res then
            return 2, "connection must be closed"
        else
            return res, err
        end
    end
end


function _M.get_reused_times(self)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:getreusedtimes()
end


function _M.close(self)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:close()
end


local function _should_receive_body(method, code)
    if method == "HEAD" then return nil end
    if code == 204 or code == 304 then return nil end
    if code >= 100 and code < 200 then return nil end
    return true
end


function _M.parse_uri(_, uri, query_in_path)
    if query_in_path == nil then query_in_path = true end

    local m, err = ngx_re_match(
        uri,
        [[^(?:(http[s]?):)?//((?:[^\[\]:/\?]+)|(?:\[.+\]))(?::(\d+))?([^\?]*)\??(.*)]],
        "jo"
    )

    if not m then
        if err then
            return nil, "failed to match the uri: " .. uri .. ", " .. err
        end

        return nil, "bad uri: " .. uri
    else
        -- If the URI is schemaless (i.e. //example.com) try to use our current
        -- request scheme.
        if not m[1] then
            -- Schema-less URIs can occur in client side code, implying "inherit
            -- the schema from the current request". We support it for a fairly
            -- specific case; if for example you are using the ESI parser in
            -- ledge (https://github.com/ledgetech/ledge) to perform in-flight
            -- sub requests on the edge based on instructions found in markup,
            -- those URIs may also be schemaless with the intention that the
            -- subrequest would inherit the schema just like JavaScript would.
            local scheme = ngx_var.scheme
            if scheme == "http" or scheme == "https" then
                m[1] = scheme
            else
                return nil, "schemaless URIs require a request context: " .. uri
            end
        end

        if m[3] then
            m[3] = tonumber(m[3])
        else
            if m[1] == "https" then
                m[3] = 443
            else
                m[3] = 80
            end
        end
        if not m[4] or "" == m[4] then m[4] = "/" end

        if query_in_path and m[5] and m[5] ~= "" then
            m[4] = m[4] .. "?" .. m[5]
            m[5] = nil
        end

        return m, nil
    end
end


local function _format_request(self, params)
    local version = params.version
    local headers = params.headers or {}

    local query = params.query or ""
    if type(query) == "table" then
        query = ngx_encode_args(query)
    end

    if query ~= "" and str_sub(query, 1, 1) ~= "?" then
        query = "?" .. query
    end

    -- Initialize request
    local req = {
        str_upper(params.method),
        " ",
        self.path_prefix or "",
        params.path,
        query,
        HTTP[version],
        -- Pre-allocate slots for minimum headers and carriage return.
        true,
        true,
        true,
    }
    local c = 7 -- req table index it's faster to do this inline vs table.insert

    -- Append headers
    for key, values in pairs(headers) do
        key = tostring(key)

        if type(values) == "table" then
            for _, value in pairs(values) do
                req[c] = key .. ": " .. tostring(value) .. "\r\n"
                c = c + 1
            end

        else
            req[c] = key .. ": " .. tostring(values) .. "\r\n"
            c = c + 1
        end
    end

    -- Close headers
    req[c] = "\r\n"

    return tbl_concat(req)
end


local function _receive_status(sock)
    local line, err = sock:receive("*l")
    if not line then
        return nil, nil, nil, err
    end

    local version = tonumber(str_sub(line, 6, 8))
    if not version then
        return nil, nil, nil,
               "couldn't parse HTTP version from response status line: " .. line
    end

    local status = tonumber(str_sub(line, 10, 12))
    if not status then
        return nil, nil, nil,
               "couldn't parse status code from response status line: " .. line
    end

    local reason = str_sub(line, 14)

    return status, version, reason
end


local function _receive_headers(sock)
    local headers = http_headers.new()

    repeat
        local line, err = sock:receive("*l")
        if not line then
            return nil, err
        end

        local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.*)", "jo")
        if err then ngx_log(ngx_ERR, err) end

        if not m then
            break
        end

        local key = m[1]
        local val = m[2]
        if headers[key] then
            if type(headers[key]) ~= "table" then
                headers[key] = { headers[key] }
            end
            tbl_insert(headers[key], tostring(val))
        else
            headers[key] = tostring(val)
        end
    until ngx_re_find(line, "^\\s*$", "jo")

    return headers, nil
end


local function transfer_encoding_is_chunked(headers)
    local te = headers["Transfer-Encoding"]
    if not te then
        return false
    end

    -- Handle duplicate headers
    -- This shouldn't happen but can in the real world
    if type(te) ~= "string" then
        te = tbl_concat(te, ",")
    end

    return str_find(str_lower(te), "chunked", 1, true) ~= nil
end
_M.transfer_encoding_is_chunked = transfer_encoding_is_chunked


local function _chunked_body_reader(sock, default_chunk_size)
    return co_wrap(function(max_chunk_size)
        local remaining = 0
        local length
        max_chunk_size = max_chunk_size or default_chunk_size

        repeat
            -- If we still have data on this chunk
            if max_chunk_size and remaining > 0 then

                if remaining > max_chunk_size then
                    -- Consume up to max_chunk_size
                    length = max_chunk_size
                    remaining = remaining - max_chunk_size
                else
                    -- Consume all remaining
                    length = remaining
                    remaining = 0
                end
            else -- This is a fresh chunk

                -- Receive the chunk size
                local str, err = sock:receive("*l")
                if not str then
                    co_yield(nil, err)
                end

                length = tonumber(str, 16)

                if not length then
                    co_yield(nil, "unable to read chunksize")
                end

                if max_chunk_size and length > max_chunk_size then
                    -- Consume up to max_chunk_size
                    remaining = length - max_chunk_size
                    length = max_chunk_size
                end
            end

            if length > 0 then
                local str, err = sock:receive(length)
                if not str then
                    co_yield(nil, err)
                end

                max_chunk_size = co_yield(str) or default_chunk_size

                -- If we're finished with this chunk, read the carriage return.
                if remaining == 0 then
                    sock:receive(2) -- read \r\n
                end
            else
                -- Read the last (zero length) chunk's carriage return
                sock:receive(2) -- read \r\n
            end

        until length == 0
    end)
end


local function _body_reader(sock, content_length, default_chunk_size)
    return co_wrap(function(max_chunk_size)
        max_chunk_size = max_chunk_size or default_chunk_size

        if not content_length and max_chunk_size then
            -- We have no length, but wish to stream.
            -- HTTP 1.0 with no length will close connection, so read chunks to the end.
            repeat
                local str, err, partial = sock:receive(max_chunk_size)
                if not str and err == "closed" then
                    co_yield(partial, err)
                end

                max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
                if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end

                if not max_chunk_size then
                    ngx_log(ngx_ERR, "Buffer size not specified, bailing")
                    break
                end
            until not str

        elseif not content_length then
            -- We have no length but don't wish to stream.
            -- HTTP 1.0 with no length will close connection, so read to the end.
            co_yield(sock:receive("*a"))

        elseif not max_chunk_size then
            -- We have a length and potentially keep-alive, but want everything.
            co_yield(sock:receive(content_length))

        else
            -- We have a length and potentially a keep-alive, and wish to stream
            -- the response.
            local received = 0
            repeat
                local length = max_chunk_size
                if received + length > content_length then
                    length = content_length - received
                end

                if length > 0 then
                    local str, err = sock:receive(length)
                    if not str then
                        co_yield(nil, err)
                    end
                    received = received + length

                    max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
                    if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end

                    if not max_chunk_size then
                        ngx_log(ngx_ERR, "Buffer size not specified, bailing")
                        break
                    end
                end

            until length == 0
        end
    end)
end


local function _no_body_reader()
    return nil
end


local function _read_body(res)
    local reader = res.body_reader

    if not reader then
        -- Most likely HEAD or 304 etc.
        return nil, "no body to be read"
    end

    local chunks = {}
    local c = 1

    local chunk, err
    repeat
        chunk, err = reader()

        if err then
            return nil, err, tbl_concat(chunks) -- Return any data so far.
        end
        if chunk then
            chunks[c] = chunk
            c = c + 1
        end
    until not chunk

    return tbl_concat(chunks)
end


local function _trailer_reader(sock)
    return co_wrap(function()
        co_yield(_receive_headers(sock))
    end)
end


local function _read_trailers(res)
    local reader = res.trailer_reader
    if not reader then
        return nil, "no trailers"
    end

    local trailers = reader()
    setmetatable(res.headers, { __index = trailers })
end


local function _send_body(sock, body)
    if type(body) == "function" then
        repeat
            local chunk, err, partial = body()

            if chunk then
                local ok, err = sock:send(chunk)

                if not ok then
                    return nil, err
                end
            elseif err ~= nil then
                return nil, err, partial
            end

        until chunk == nil
    elseif body ~= nil then
        local bytes, err = sock:send(body)

        if not bytes then
            return nil, err
        end
    end
    return true, nil
end


local function _handle_continue(sock, body)
    local status, version, reason, err = _receive_status(sock) --luacheck: no unused
    if not status then
        return nil, nil, nil, err
    end

    -- Only send body if we receive a 100 Continue
    if status == 100 then
        -- Read headers
        local headers, err = _receive_headers(sock)
        if not headers then
            return nil, nil, nil, err
        end

        local ok, err = _send_body(sock, body)
        if not ok then
            return nil, nil, nil, err
        end
    end
    return status, version, reason, err
end


function _M.send_request(self, params)
    -- Apply defaults
    setmetatable(params, { __index = DEFAULT_PARAMS })

    local sock = self.sock
    local body = params.body
    local headers = http_headers.new()

    -- We assign one-by-one so that the metatable can handle case insensitivity
    -- for us. You can blame the spec for this inefficiency.
    local params_headers = params.headers or {}
    for k, v in pairs(params_headers) do
        headers[k] = v
    end

    if not headers["Proxy-Authorization"] then
        -- TODO: next major, change this to always override the provided
        -- header. Can't do that yet because it would be breaking.
        -- The connect method uses self.http_proxy_auth in the poolname so
        -- that should be leading.
        headers["Proxy-Authorization"] = self.http_proxy_auth
    end

    -- Ensure we have appropriate message length or encoding.
    do
        local is_chunked = transfer_encoding_is_chunked(headers)

        if is_chunked then
            -- If we have both Transfer-Encoding and Content-Length we MUST
            -- drop the Content-Length, to help prevent request smuggling.
            -- https://tools.ietf.org/html/rfc7230#section-3.3.3
            headers["Content-Length"] = nil

        elseif not headers["Content-Length"] then
            -- A length was not given, try to calculate one.

            local body_type = type(body)

            if body_type == "function" then
                return nil, "Request body is a function but a length or chunked encoding is not specified"

            elseif body_type == "table" then
                local length = 0
                for _, v in ipairs(body) do
                    length = length + #tostring(v)
                end
                headers["Content-Length"] = length

            elseif body == nil and EXPECTING_BODY[str_upper(params.method)] then
                headers["Content-Length"] = 0

            elseif body ~= nil then
                headers["Content-Length"] = #tostring(body)
            end
        end
    end

    if not headers["Host"] then
        if (str_sub(self.host, 1, 5) == "unix:") then
            return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one."
        end
        -- If we have a port (i.e. not connected to a unix domain socket), and this
        -- port is non-standard, append it to the Host header.
        if self.port then
            if self.ssl and self.port ~= 443 then
                headers["Host"] = self.host .. ":" .. self.port
            elseif not self.ssl and self.port ~= 80 then
                headers["Host"] = self.host .. ":" .. self.port
            else
                headers["Host"] = self.host
            end
        else
            headers["Host"] = self.host
        end
    end
    if not headers["User-Agent"] then
        headers["User-Agent"] = _M._USER_AGENT
    end
    if params.version == 1.0 and not headers["Connection"] then
        headers["Connection"] = "Keep-Alive"
    end

    params.headers = headers

    -- Format and send request
    local req = _format_request(self, params)
    if DEBUG then ngx_log(ngx_DEBUG, "\n", req) end
    local bytes, err = sock:send(req)

    if not bytes then
        return nil, err
    end

    -- Send the request body, unless we expect: continue, in which case
    -- we handle this as part of reading the response.
    if headers["Expect"] ~= "100-continue" then
        local ok, err, partial = _send_body(sock, body)
        if not ok then
            return nil, err, partial
        end
    end

    return true
end


function _M.read_response(self, params)
    local sock = self.sock

    local status, version, reason, err

    -- If we expect: continue, we need to handle this, sending the body if allowed.
    -- If we don't get 100 back, then status is the actual status.
    if params.headers["Expect"] == "100-continue" then
        local _status, _version, _reason, _err = _handle_continue(sock, params.body)
        if not _status then
            return nil, _err
        elseif _status ~= 100 then
            status, version, reason, err = _status, _version, _reason, _err -- luacheck: no unused
        end
    end

    -- Just read the status as normal.
    if not status then
        status, version, reason, err = _receive_status(sock)
        if not status then
            return nil, err
        end
    end


    local res_headers, err = _receive_headers(sock)
    if not res_headers then
        return nil, err
    end

    -- keepalive is true by default. Determine if this is correct or not.
    local ok, connection = pcall(str_lower, res_headers["Connection"])
    if ok then
        if (version == 1.1 and str_find(connection, "close", 1, true)) or
           (version == 1.0 and not str_find(connection, "keep-alive", 1, true)) then
            self.keepalive = false
        end
    else
        -- no connection header
        if version == 1.0 then
            self.keepalive = false
        end
    end

    local body_reader = _no_body_reader
    local trailer_reader, err
    local has_body = false

    -- Receive the body_reader
    if _should_receive_body(params.method, status) then
        has_body = true

        if version == 1.1 and transfer_encoding_is_chunked(res_headers) then
            body_reader, err = _chunked_body_reader(sock)
        else
            local ok, length = pcall(tonumber, res_headers["Content-Length"])
            if not ok then
                -- No content-length header, read until connection is closed by server
                length = nil
            end

            body_reader, err = _body_reader(sock, length)
        end
    end

    if res_headers["Trailer"] then
        trailer_reader, err = _trailer_reader(sock)
    end

    if err then
        return nil, err
    else
        return {
            status = status,
            reason = reason,
            headers = res_headers,
            has_body = has_body,
            body_reader = body_reader,
            read_body = _read_body,
            trailer_reader = trailer_reader,
            read_trailers = _read_trailers,
        }
    end
end


function _M.request(self, params)
    params = tbl_copy(params) -- Take by value
    local res, err = self:send_request(params)
    if not res then
        return res, err
    else
        return self:read_response(params)
    end
end


function _M.request_pipeline(self, requests)
    requests = tbl_copy(requests) -- Take by value

    for _, params in ipairs(requests) do
        if params.headers and params.headers["Expect"] == "100-continue" then
            return nil, "Cannot pipeline request specifying Expect: 100-continue"
        end

        local res, err = self:send_request(params)
        if not res then
            return res, err
        end
    end

    local responses = {}
    for i, params in ipairs(requests) do
        responses[i] = setmetatable({
            params = params,
            response_read = false,
        }, {
            -- Read each actual response lazily, at the point the user tries
            -- to access any of the fields.
            __index = function(t, k)
                local res, err
                if t.response_read == false then
                    res, err = _M.read_response(self, t.params)
                    t.response_read = true

                    if not res then
                        ngx_log(ngx_ERR, err)
                    else
                        for rk, rv in pairs(res) do
                            t[rk] = rv
                        end
                    end
                end
                return rawget(t, k)
            end,
        })
    end
    return responses
end


function _M.request_uri(self, uri, params)
    params = tbl_copy(params or {}) -- Take by value
    if self.proxy_opts then
        params.proxy_opts = tbl_copy(self.proxy_opts or {})
    end

    do
        local parsed_uri, err = self:parse_uri(uri, false)
        if not parsed_uri then
            return nil, err
        end

        local path, query
        params.scheme, params.host, params.port, path, query = unpack(parsed_uri)
        params.path = params.path or path
        params.query = params.query or query
        params.ssl_server_name = params.ssl_server_name or params.host
    end

    do
        local proxy_auth = (params.headers or {})["Proxy-Authorization"]
        if proxy_auth and params.proxy_opts then
            params.proxy_opts.https_proxy_authorization = proxy_auth
            params.proxy_opts.http_proxy_authorization = proxy_auth
        end
    end

    local ok, err = self:connect(params)
    if not ok then
        return nil, err
    end

    local res, err = self:request(params)
    if not res then
        self:close()
        return nil, err
    end

    local body, err = res:read_body()
    if not body then
        self:close()
        return nil, err
    end

    res.body = body

    if params.keepalive == false then
        local ok, err = self:close()
        if not ok then
            ngx_log(ngx_ERR, err)
        end

    else
        local ok, err = self:set_keepalive(params.keepalive_timeout, params.keepalive_pool)
        if not ok then
            ngx_log(ngx_ERR, err)
        end

    end

    return res, nil
end


function _M.get_client_body_reader(_, chunksize, sock)
    chunksize = chunksize or 65536

    if not sock then
        local ok, err
        ok, sock, err = pcall(ngx_req_socket)

        if not ok then
            return nil, sock -- pcall err
        end

        if not sock then
            if err == "no body" then
                return nil
            else
                return nil, err
            end
        end
    end

    local headers = ngx_req_get_headers()
    local length = headers.content_length
    if length then
        return _body_reader(sock, tonumber(length), chunksize)
    elseif transfer_encoding_is_chunked(headers) then
        -- Not yet supported by ngx_lua but should just work...
        return _chunked_body_reader(sock, chunksize)
    else
        return nil
    end
end


function _M.set_proxy_options(self, opts)
    -- TODO: parse and cache these options, instead of parsing them
    -- on each request over and over again (lru-cache on module level)
    self.proxy_opts = tbl_copy(opts) -- Take by value
end


function _M.get_proxy_uri(self, scheme, host)
    if not self.proxy_opts then
        return nil
    end

    -- Check if the no_proxy option matches this host. Implementation adapted
    -- from lua-http library (https://github.com/daurnimator/lua-http)
    if self.proxy_opts.no_proxy then
        if self.proxy_opts.no_proxy == "*" then
            -- all hosts are excluded
            return nil
        end

        local no_proxy_set = {}
        -- wget allows domains in no_proxy list to be prefixed by "."
        -- e.g. no_proxy=.mit.edu
        for host_suffix in ngx_re_gmatch(self.proxy_opts.no_proxy, "\\.?([^,]+)", "jo") do
            no_proxy_set[host_suffix[1]] = true
        end

        -- From curl docs:
        -- matched as either a domain which contains the hostname, or the
        -- hostname itself. For example local.com would match local.com,
        -- local.com:80, and www.local.com, but not www.notlocal.com.
        --
        -- Therefore, we keep stripping subdomains from the host, compare
        -- them to the ones in the no_proxy list and continue until we find
        -- a match or until there's only the TLD left
        repeat
            if no_proxy_set[host] then
                return nil
            end

            -- Strip the next level from the domain and check if that one
            -- is on the list
            host = ngx_re_sub(host, "^[^.]+\\.", "", "jo")
        until not ngx_re_find(host, "\\.", "jo")
    end

    if scheme == "http" and self.proxy_opts.http_proxy then
        return self.proxy_opts.http_proxy
    end

    if scheme == "https" and self.proxy_opts.https_proxy then
        return self.proxy_opts.https_proxy
    end

    return nil
end


-- ----------------------------------------------------------------------------
-- The following functions are considered DEPRECATED and may be REMOVED in
-- future releases. Please see the notes in `README.md`.
-- ----------------------------------------------------------------------------

function _M.ssl_handshake(self, ...)
    ngx_log(ngx_DEBUG, "Use of deprecated function `ssl_handshake`")

    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    self.ssl = true

    return sock:sslhandshake(...)
end


function _M.connect_proxy(self, proxy_uri, scheme, host, port, proxy_authorization)
    ngx_log(ngx_DEBUG, "Use of deprecated function `connect_proxy`")

    -- Parse the provided proxy URI
    local parsed_proxy_uri, err = self:parse_uri(proxy_uri, false)
    if not parsed_proxy_uri then
        return nil, err
    end

    -- Check that the scheme is http (https is not supported for
    -- connections between the client and the proxy)
    local proxy_scheme = parsed_proxy_uri[1]
    if proxy_scheme ~= "http" then
        return nil, "protocol " .. proxy_scheme .. " not supported for proxy connections"
    end

    -- Make the connection to the given proxy
    local proxy_host, proxy_port = parsed_proxy_uri[2], parsed_proxy_uri[3]
    local c, err = self:tcp_only_connect(proxy_host, proxy_port)
    if not c then
        return nil, err
    end

    if scheme == "https" then
        -- Make a CONNECT request to create a tunnel to the destination through
        -- the proxy. The request-target and the Host header must be in the
        -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section
        -- 4.3.6 for more details about the CONNECT request
        local destination = host .. ":" .. port
        local res, err = self:request({
            method = "CONNECT",
            path = destination,
            headers = {
                ["Host"] = destination,
                ["Proxy-Authorization"] = proxy_authorization,
            }
        })

        if not res then
            return nil, err
        end

        if res.status < 200 or res.status > 299 then
            return nil, "failed to establish a tunnel through a proxy: " .. res.status
        end
    end

    return c, nil
end


function _M.proxy_request(self, chunksize)
    ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_request`")

    return self:request({
        method = ngx_req_get_method(),
        path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""),
        body = self:get_client_body_reader(chunksize),
        headers = ngx_req_get_headers(),
    })
end


function _M.proxy_response(_, response, chunksize)
    ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_response`")

    if not response then
        ngx_log(ngx_ERR, "no response provided")
        return
    end

    ngx.status = response.status

    -- Filter out hop-by-hop headeres
    for k, v in pairs(response.headers) do
        if not HOP_BY_HOP_HEADERS[str_lower(k)] then
            ngx_header[k] = v
        end
    end

    local reader = response.body_reader

    repeat
        local chunk, ok, read_err, print_err

        chunk, read_err = reader(chunksize)
        if read_err then
            ngx_log(ngx_ERR, read_err)
        end

        if chunk then
            ok, print_err = ngx_print(chunk)
            if not ok then
                ngx_log(ngx_ERR, print_err)
            end
        end

        if read_err or print_err then
            break
        end
    until not chunk
end


return _M

  1. http_connect.lua
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_sub = ngx.re.sub
local ngx_re_find = ngx.re.find
local ngx_log = ngx.log
local ngx_WARN = ngx.WARN

--[[
A connection function that incorporates:
  - tcp connect
  - ssl handshake
  - http proxy
Due to this it will be better at setting up a socket pool where connections can
be kept alive.


Call it with a single options table as follows:

client:connect {
    scheme = "https"        -- scheme to use, or nil for unix domain socket
    host = "myhost.com",    -- target machine, or a unix domain socket
    port = nil,             -- port on target machine, will default to 80/443 based on scheme
    pool = nil,             -- connection pool name, leave blank! this function knows best!
    pool_size = nil,        -- options as per: https://github.com/openresty/lua-nginx-module#tcpsockconnect
    backlog = nil,

    -- ssl options as per: https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake
    ssl_reused_session = nil
    ssl_server_name = nil,
    ssl_send_status_req = nil,
    ssl_verify = true,      -- NOTE: defaults to true
    ctx = nil,              -- NOTE: not supported

    -- mTLS options: These require support for mTLS in cosockets, which first
    -- appeared in `ngx_http_lua_module` v0.10.23.
    ssl_client_cert = nil,
    ssl_client_priv_key = nil,

    proxy_opts,             -- proxy opts, defaults to global proxy options
}
]]
local function connect(self, options)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    local ok, err

    local request_scheme = options.scheme
    local request_host = options.host
    local request_port = options.port

    local poolname = options.pool
    local pool_size = options.pool_size
    local backlog = options.backlog

    if request_scheme and not request_port then
        request_port = (request_scheme == "https" and 443 or 80)
    elseif request_port and not request_scheme then
        return nil, "'scheme' is required when providing a port"
    end

    -- ssl settings
    local ssl, ssl_reused_session, ssl_server_name
    local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key
    if request_scheme == "https" then
        ssl = true
        ssl_reused_session = options.ssl_reused_session
        ssl_server_name = options.ssl_server_name
        ssl_send_status_req = options.ssl_send_status_req
        ssl_verify = true -- default
        if options.ssl_verify == false then
            ssl_verify = false
        end
        ssl_client_cert = options.ssl_client_cert
        ssl_client_priv_key = options.ssl_client_priv_key
    end

    -- proxy related settings
    local proxy, proxy_uri, proxy_authorization, proxy_host, proxy_port, path_prefix
    proxy = options.proxy_opts or self.proxy_opts

    if proxy then
        if request_scheme == "https" then
            proxy_uri = proxy.https_proxy
            proxy_authorization = proxy.https_proxy_authorization
        else
            proxy_uri = proxy.http_proxy
            proxy_authorization = proxy.http_proxy_authorization
            -- When a proxy is used, the target URI must be in absolute-form
            -- (RFC 7230, Section 5.3.2.). That is, it must be an absolute URI
            -- to the remote resource with the scheme, host and an optional port
            -- in place.
            --
            -- Since _format_request() constructs the request line by concatenating
            -- params.path and params.query together, we need to modify the path
            -- to also include the scheme, host and port so that the final form
            -- in conformant to RFC 7230.
            path_prefix = "http://" .. request_host .. (request_port == 80 and "" or (":" .. request_port))
        end
        if not proxy_uri then
            proxy = nil
            proxy_authorization = nil
            path_prefix = nil
        end
    end

    if proxy and proxy.no_proxy then
        -- Check if the no_proxy option matches this host. Implementation adapted
        -- from lua-http library (https://github.com/daurnimator/lua-http)
        if proxy.no_proxy == "*" then
            -- all hosts are excluded
            proxy = nil

        else
            local host = request_host
            local no_proxy_set = {}
            -- wget allows domains in no_proxy list to be prefixed by "."
            -- e.g. no_proxy=.mit.edu
            for host_suffix in ngx_re_gmatch(proxy.no_proxy, "\\.?([^,]+)") do
                no_proxy_set[host_suffix[1]] = true
            end

            -- From curl docs:
            -- matched as either a domain which contains the hostname, or the
            -- hostname itself. For example local.com would match local.com,
            -- local.com:80, and www.local.com, but not www.notlocal.com.
            --
            -- Therefore, we keep stripping subdomains from the host, compare
            -- them to the ones in the no_proxy list and continue until we find
            -- a match or until there's only the TLD left
            repeat
                if no_proxy_set[host] then
                    proxy = nil
                    proxy_uri = nil
                    proxy_authorization = nil
                    break
                end

                -- Strip the next level from the domain and check if that one
                -- is on the list
                host = ngx_re_sub(host, "^[^.]+\\.", "")
            until not ngx_re_find(host, "\\.")
        end
    end

    if proxy then
        local proxy_uri_t
        proxy_uri_t, err = self:parse_uri(proxy_uri)
        if not proxy_uri_t then
            return nil, "uri parse error: ", err
        end

        local proxy_scheme = proxy_uri_t[1]
        if proxy_scheme ~= "http" then
            return nil, "protocol " .. tostring(proxy_scheme) ..
                        " not supported for proxy connections"
        end
        proxy_host = proxy_uri_t[2]
        proxy_port = proxy_uri_t[3]
    end

    -- construct a poolname unique within proxy and ssl info
    if not poolname then
        poolname = (request_scheme or "")
                   .. ":" .. request_host
                   .. ":" .. tostring(request_port)
                   .. ":" .. tostring(ssl)
                   .. ":" .. (ssl_server_name or "")
                   .. ":" .. tostring(ssl_verify)
                   .. ":" .. (proxy_uri or "")
                   .. ":" .. (request_scheme == "https" and proxy_authorization or "")
        -- in the above we only add the 'proxy_authorization' as part of the poolname
        -- when the request is https. Because in that case the CONNECT request (which
        -- carries the authorization header) is part of the connect procedure, whereas
        -- with a plain http request the authorization is part of the actual request.
    end

    -- do TCP level connection
    local tcp_opts = { pool = poolname, pool_size = pool_size, backlog = backlog }
    if proxy then
        -- proxy based connection
        ok, err = sock:connect(proxy_host, proxy_port, tcp_opts)
        if not ok then
            return nil, "failed to connect to: " .. (proxy_host or "") ..
                        ":" .. (proxy_port or "") ..
                        ": ", err
        end

        if ssl and sock:getreusedtimes() == 0 then
            -- Make a CONNECT request to create a tunnel to the destination through
            -- the proxy. The request-target and the Host header must be in the
            -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section
            -- 4.3.6 for more details about the CONNECT request
            local destination = request_host .. ":" .. request_port
            local res
            res, err = self:request({
                method = "CONNECT",
                path = destination,
                headers = {
                    ["Host"] = destination,
                    ["Proxy-Authorization"] = proxy_authorization,
                }
            })

            if not res then
                return nil, "failed to issue CONNECT to proxy:", err
            end

            if res.status < 200 or res.status > 299 then
                return nil, "failed to establish a tunnel through a proxy: " .. res.status
            end
        end

    elseif not request_port then
        -- non-proxy, without port -> unix domain socket
        ok, err = sock:connect(request_host, tcp_opts)
        if not ok then
            return nil, err
        end

    else
        -- non-proxy, regular network tcp
        ok, err = sock:connect(request_host, request_port, tcp_opts)
        if not ok then
            return nil, err
        end
    end

    local ssl_session
    -- Now do the ssl handshake
    if ssl and sock:getreusedtimes() == 0 then

        -- Experimental mTLS support
        if ssl_client_cert and ssl_client_priv_key then
          if type(sock.setclientcert) ~= "function" then
            ngx_log(ngx_WARN, "cannot use SSL client cert and key without mTLS support")

          else
            ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key)
            if not ok then
              ngx_log(ngx_WARN, "could not set client certificate: ", err)
            end
          end
        end

        ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req)
        if not ssl_session then
            self:close()
            return nil, err
        end
    end

    self.host = request_host
    self.port = request_port
    self.keepalive = true
    self.ssl = ssl
    -- set only for http, https has already been handled
    self.http_proxy_auth = request_scheme ~= "https" and proxy_authorization or nil
    self.path_prefix = path_prefix

    return true, nil, ssl_session
end

return connect

  1. http_headers.lua
local rawget, rawset, setmetatable =
    rawget, rawset, setmetatable

local str_lower = string.lower

local _M = {
    _VERSION = '0.17.1',
}


-- Returns an empty headers table with internalised case normalisation.
function _M.new()
    local mt = {
        normalised = {},
    }

    mt.__index = function(t, k)
        return rawget(t, mt.normalised[str_lower(k)])
    end

    mt.__newindex = function(t, k, v)
        local k_normalised = str_lower(k)

        -- First time seeing this header field?
        if not mt.normalised[k_normalised] then
            -- Create a lowercased entry in the metatable proxy, with the value
            -- of the given field case
            mt.normalised[k_normalised] = k

            -- Set the header using the given field case
            rawset(t, k, v)
        else
            -- We're being updated just with a different field case. Use the
            -- normalised metatable proxy to give us the original key case, and
            -- perorm a rawset() to update the value.
            rawset(t, mt.normalised[k_normalised], v)
        end
    end

    return setmetatable({}, mt)
end


return _M

四、项目代码

  1. nginx.conf
user cauchy;

pid /home/cauchy/openresty/logs/nginx.pid;

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    lua_package_path "/home/cauchy/openresty/modules/runnergo/?.lua;;/usr/local/openresty/site/lualib/resty/?.lua;;";

    lua_code_cache on;

    server {
        error_log /home/cauchy/openresty/logs/error.log;
        error_log /home/cauchy/openresty/logs/error.log info;
        access_log /home/cauchy/openresty/logs/access.log;

        listen 8002;

        location /api/register {
            content_by_lua_file /home/cauchy/openresty/modules/runnergo/register.lua;
        }

        location /api/login {
            content_by_lua_file /home/cauchy/openresty/modules/runnergo/login.lua;
        }

        location /api/protected {
            access_by_lua_file /home/cauchy/openresty/modules/runnergo/auth.lua;

        }
    }
}
  1. register.lua
ngx.header.content_type = "application/json;charset=utf-8"

local cjson = require "cjson.safe"
local mysql = require "resty.mysql"

local db, err = mysql:new()
if not db then
    ngx.say("failed to instantiate mysql: ", err)
    return
end

local mysql_conf = {
    host = "127.0.0.1",
    port = 3306,
    database = "chat",
    user = "cauchy",
    password = "root",
    charset = "utf8",
    max_packet_size = 1024 * 1024,
}

local ok, err, errno, sqlstate = db:connect(mysql_conf)
if not ok then
    ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate)
    return
end

-- post 需要先执行read_body
ngx.req.read_body() 
-- get_post_args:
    -- 表单格式(x-www-form-urlencoded): password=12345&username=cauchy 
        -- 用法:args.field
    -- json格式(raw json): 
        -- 用法:cjson.decode(ngx.var.request_body), args.field
-- local args, err = ngx.req.get_post_args()
local args = cjson.decode(ngx.var.request_body)

-- TODO: 验证用户信息,例如检查用户名和密码的有效性


local password_hash = ngx.md5(args.password) -- 使用MD5哈希密码,生产环境中应使用更安全的方法

local fmt = [===[
insert into chat.users (`username`, `password`) values ('%s', '%s')
]===]
local sql = string.format(fmt, args.username, password_hash)

local res, err, errno, sqlstate = db:query(sql)
if not res then
    ngx.say("Bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

local result = {
    code = "200", 
    data = "Success Register User",
    userinfo = {
        username = args.username,
        password = args.password,
    }
}

ngx.status = 200
ngx.say(cjson.encode(result))

---------------------------------------------------------------

-- 需要放在数据库操作之后!

local ok, err = db:set_keepalive(3 * 1000, 50)
if not ok then
    ngx.say("failed to set keepalive: ", err)
    return
end

-- return ngx.exit(ngx.HTTP_OK)
  1. login.lua
ngx.header.content_type = "application/json;charset=utf-8"

local jwt = require "resty.jwt"
local cjson = require "cjson.safe"
local mysql = require "resty.mysql"

local secret = "your-secret-key"

local db, err = mysql:new()
if not db then
    ngx.say("failed to instantiate mysql: ", err)
    return
end

local mysql_conf = {
    host = "127.0.0.1",
    port = 3306,
    database = "chat",
    user = "cauchy",
    password = "root",
    charset = "utf8",
    max_packet_size = 1024 * 1024,
}

local ok, err, errno, sqlstate = db:connect(mysql_conf)
if not ok then
    ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate)
    return
end

---------------------------------------------------------------------

ngx.req.read_body() 
local args = ngx.req.get_post_args()

local fmt = [===[
select `password` from chat.users where `username` = '%s' limit 1;
]===]
local sql = string.format(fmt, args.username)

local res, err = db:query(sql)
if not res then
    ngx.say("Bad result: ", err)
    return
end

local password_hash = ngx.md5(args.password)

if res[1] and res[1].password == password_hash then

    local table_of_jwt = {
        header = { typ = "JWT", alg = "HS512" },
        payload = {
            username = args.username,
        }
    }
    local jwt_token = "Bearer " .. jwt:sign(secret, table_of_jwt)

    ngx.status = 200
    
    local result = {
        code = ngx.HTTP_OK,
        token = jwt_token
    }
    
    ngx.say(cjson.encode(result))
else
    ngx.say("Invalid username or password")
end

---------------------------------------------------------------------


local ok, err = db:set_keepalive(10000, 50)
if not ok then
    ngx.say("failed to set keepalive: ", err)
    return
end
  1. auth.lua
local jwt = require "resty.jwt"

local secret = "your-secret-key"
local auth_header = ngx.req.get_headers()["token"]
local token = auth_header:sub(8)

local jwt_obj = jwt:verify(secret, token)

if jwt_obj.verified then
    -- 令牌验证成功,继续处理请求
    ngx.say("Token verified successfully!")
else
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.say("Invalid token")
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
end

五、RunnerGo 自动化测试接口

RunnerGo 可以方便的进行调试接口,模拟真实业务场景,实现自动化测试。这款产品的具体细节可以参考官网。

1. 创建接口

设置全局变量 url

在这里插入图片描述

url 填写服务器 IP 和 端口:

在这里插入图片描述

下面在 接口管理 界面创建三个接口:

在这里插入图片描述

  • 注册register

Body 设置如下:(post 请求携带用户名和密码,表单格式:x-www-form-urlencoded

在这里插入图片描述
关联提取,即对返回结果提取出有用的信息,用于后续登录操作

在这里插入图片描述

  • 登录login

获取上一步提取的字段值({{}}形式),通过 post 请求,发送给登录验证

在这里插入图片描述
获取 token

在这里插入图片描述

  • 授权操作auth

请求头携带上一步登录操作后返回的 token

在这里插入图片描述

2. 创建场景

在场景管理界面,创建场景 注册-登录-操作,引入之前创建的三个接口,并将每步进行连接(即上一步关联提取的值,可用于下一步获取)。

在这里插入图片描述

3. 创建自动化测试

自动化测试界面,新建计划,然后导入场景:

在这里插入图片描述

点击调试场景,每个接口都是绿色则代表接口测试成功:

在这里插入图片描述

在场景设置中,需要导入 (txt、csv)文件,用于创建用户测试集:

在这里插入图片描述
用例集中创建多个用例,自动化测试时,每个用例都会被轮询附上文件中表头字段对应的值。

在这里插入图片描述

不过要想从文件中取值,需要在传递参数时用 {{}} 引用上:

在这里插入图片描述

在这里插入图片描述

运行自动化测试:

在这里插入图片描述


参考文章:

https://blog.csdn.net/weixin_45410366/article/details/125031959

https://zhuanlan.zhihu.com/p/530076389

https://blog.csdn.net/wzj_110/article/details/120271453

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ღCauchyོꦿ࿐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值