apisix基于username和password的JWT验证插件

插件说明

         基于jwt-auth插件,新增了password字段和相应的逻辑判断。

         jwt-diy的属性表如下:

名称

类型

必选项

默认值

有效值

描述

key

string

必须

   

不同的 consumer 对象应有不同的值,它应当是唯一的。不同 consumer 使用了相同的 key ,将会出现请求匹配异常。

password

string

必须

 

 

Key对应的口令,唯有key和password相匹配才会返回token

secret

string

可选

   

加密秘钥。如果您未指定,后台将会自动帮您生成。

public_key

string

可选

   

RSA 公钥, algorithm 属性选择 RS256 算法时必填

private_key

string

可选

   

RSA 私钥, algorithm 属性选择 RS256 算法时必填

algorithm

string

可选

"HS256"

["HS256", "HS512", "RS256"]

加密算法

exp

integer

可选

86400

[1,...]

token 的超时时间

base64_secret

boolean

可选

false

 

密钥是否为 base64 编码

安装流程

复制插件到plugins目录

 

cd /usr/local/apisix/apisix/plugins
vi ./jwt-diy.lua    #键入插件代码

修改默认配置

cd /usr/local/apisix/conf
vi ./config-default.yaml    #修改默认配置

键入配置信息

……
……
plugins:                          # plugin list (sorted by priority)
  - client-control                 # priority: 22000
  - ext-plugin-pre-req             # priority: 12000
  - zipkin                         # priority: 11011
  - request-id                     # priority: 11010
  - fault-injection                # priority: 11000
  - serverless-pre-function        # priority: 10000
  - batch-requests                 # priority: 4010
  - cors                           # priority: 4000
  - ip-restriction                 # priority: 3000
  - referer-restriction            # priority: 2990
  - uri-blocker                    # priority: 2900
  - request-validation             # priority: 2800
  - openid-connect                 # priority: 2599
  - wolf-rbac                      # priority: 2555
  - hmac-auth                      # priority: 2530
  - basic-auth                     # priority: 2520
  - jwt-auth                       # priority: 2510
  - key-auth                       # priority: 2500
  - consumer-restriction           # priority: 2400
  - authz-keycloak                 # priority: 2000
  #- error-log-logger              # priority: 1091
  - proxy-mirror                   # priority: 1010
  - proxy-cache                    # priority: 1009
  - proxy-rewrite                  # priority: 1008
  - api-breaker                    # priority: 1005
  - limit-conn                     # priority: 1003
  - limit-count                    # priority: 1002
  - limit-req                      # priority: 1001
  #- node-status                   # priority: 1000
  - server-info                    # priority: 990
  - traffic-split                  # priority: 966
  - redirect                       # priority: 900
  - response-rewrite               # priority: 899
  #- dubbo-proxy                   # priority: 507
  - grpc-transcode                 # priority: 506
  - prometheus                     # priority: 500
  - echo                           # priority: 412
  - http-logger                    # priority: 410
  - sls-logger                     # priority: 406
  - tcp-logger                     # priority: 405
  - kafka-logger                   # priority: 403
  - syslog                         # priority: 401
  - udp-logger                     # priority: 400
  #- log-rotate                    # priority: 100
  # <- recommend to use priority (0, 100) for your custom plugins
  #键入jwt-diy插件的文件名
- jwt-diy                        # priority: 1 
  - example-plugin                 # priority: 0
  #- skywalking                    # priority: -1100
  - serverless-post-function       # priority: -2000
  - ext-plugin-post-req            # priority: -3000

stream_plugins: # sorted by priority
  - mqtt-proxy                     # priority: 1000
  # <- recommend to use priority (0, 100) for your custom plugins
……
……

如何启用

创建一个 consumer 对象,指定RSA算法,并设置插件 jwt-diy的值,配置公钥和私钥。

curl http://0.0.0.0:9080/apisix/admin/consumers -H ‘X-API-KEY: edd1c9f034335f136f87ad84b625c8f1‘ -X PUT -d ‘
{
    "username": "user",
    "plugins": {
        "jwt-diy": {
            "key": user-key,
            "password": password,
            "public_key": "-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM++1NAk1SnpjHgglXDWypaFdff07ymE
mI9AKYHgRNwutnIDN8CD2Pm4uHPtGCkkV1GAJZJkmOCsRULwGh51NdECAwEAAQ==
-----END PUBLIC KEY-----",
            "private_key": "-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAz77U0CTVKemMeCCV
cNbKloV19/TvKYSYj0ApgeBE3C62cgM3wIPY+bi4c+0YKSRXUYAlkmSY4KxFQvAa
HnU10QIDAQABAkB5CG8oTS072+uQ2Tr3oMwq4dqW+caU48GWRAVqu2Si+i7k3Igi
tqaz/xAs2iXTdj/W1F8tNmw2xsKBILN6LpGhAiEA9aHWC7FkRjPYrBDv5zvvR8pc
+CspzOHKrgyiBmKWwQ0CIQDYg5y+24S5jHv48Yr2/KeKug09S9Y60RhED5IEzZ9u
1QIhAMHKOa4l+R+t3d76ydscHQ79p9WfcC4VYatpihcRhzCtAiEA142AGcs2QfwI
2Hiw3t/+dPBxidrcd0YAIJJXzwxfc9kCIHVytvdYljt1letwDHCnloa0dQmhZUvJ
+IEib4YXcSPh
-----END PRIVATE KEY-----",
            "algorithm": "RS256"
        }
    }
}

创建Route或Service对象,开启jwt-diy插件

curl http://127.0.0.1:9080/apisix/admin/routes/1 -H ‘X-API-KEY: edd1c9f034335f136f87ad84b625c8f1‘ -X PUT -d ‘
{
    "methods": ["GET"],
    "uri": "/index.html",
    "plugins": {
        "jwt-diy": {}
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpunit.sourceforge.net": 1
        },
        "scheme": "http"
    }
}

获取token

curl http://0.0.0.0:9080/apisix/plugin/jwt/sign?key=user-key\&password=password –i

HTTP/1.1 200 OK
Date: Wed, 11 Aug 2021 02:42:19 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.7

eyJhbGciOiJSUzI1NiIsIng1YyI6WyItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTSsrMU5BazFTbnBqSGdnbFhEV3lwYUZkZmYwN3ltRVxubUk5QUtZSGdSTnd1dG5JRE44Q0QyUG00dUhQdEdDa2tWMUdBSlpKa21PQ3NSVUx3R2g1MU5kRUNBd0VBQVE9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tIl0sInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYyODczNjEzOX0.JcWTONIumTfwZjfsx0kxNGIA_DpPCXmhIf9EWQxGG7y_UL6s_4eFvV-iAeqIt8yshR12DcaR6R9jMpCoCiYB3A

插件代码

--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License.  You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core     = require("apisix.core")
local jwt      = require("resty.jwt")
local ck       = require("resty.cookie")
local consumer_mod = require("apisix.consumer")
local resty_random = require("resty.random")

local ngx_encode_base64 = ngx.encode_base64
local ngx_decode_base64 = ngx.decode_base64
local ipairs   = ipairs
local ngx      = ngx
local ngx_time = ngx.time
local sub_str  = string.sub
local plugin_name = "jwt-diy"
local pcall = pcall


local lrucache = core.lrucache.new({
    type = "plugin",
})

local schema = {
    type = "object",
    additionalProperties = false,
    properties = {},
}

local consumer_schema = {
    type = "object",
    -- cant use additionalProperties with dependencies
    -- additionalProperties = false,
    properties = {
        key = {type = "string"},
        secret = {type = "string"},
        password = {type = "string"},   --changed
        algorithm = {
            type = "string",
            enum = {"HS256", "HS512", "RS256"},
            default = "HS256"
        },
        exp = {type = "integer", minimum = 1, default = 86400},
        base64_secret = {
            type = "boolean",
            default = false
        }
    },
    dependencies = {
        algorithm = {
            oneOf = {
                {
                    properties = {
                        algorithm = {
                            enum = {"HS256", "HS512"},
                            default = "HS256"
                        },
                    },
                },
                {
                    properties = {
                        public_key = {type = "string"},
                        private_key= {type = "string"},
                        algorithm = {
                            enum = {"RS256"},
                        },
                    },
                    required = {"public_key", "private_key"},
                }
            }
        }
    },
    required = {"key","password"},  --changed
}


local _M = {
    version = 0.1,
    priority = 1,
    type = auth,
    name = plugin_name,
    schema = schema,
    consumer_schema = consumer_schema
}


local create_consume_cache
do
    local consumer_names = {}

    function create_consume_cache(consumers)
        core.table.clear(consumer_names)

        for _, consumer in ipairs(consumers.nodes) do
            core.log.info("consumer node: ", core.json.delay_encode(consumer))
            consumer_names[consumer.auth_conf.key] = consumer
        end

        return consumer_names
    end

end -- do


function _M.check_schema(conf, schema_type)
    core.log.info("input conf: ", core.json.delay_encode(conf))

    local ok, err
    if schema_type == core.schema.TYPE_CONSUMER then
        ok, err = core.schema.check(consumer_schema, conf)
    else
        ok, err = core.schema.check(schema, conf)
    end

    if not ok then
        return false, err
    end

    if schema_type == core.schema.TYPE_CONSUMER then
        if conf.algorithm ~= "RS256" and not conf.secret then
            conf.secret = ngx_encode_base64(resty_random.bytes(32, true))
        elseif conf.base64_secret then
            if ngx_decode_base64(conf.secret) == nil then
                return false, "base64_secret required but the secret is not in base64 format"
            end
        end

        if conf.algorithm == "RS256" then
            if not conf.public_key then
                return false, "missing valid public key"
            end
            if not conf.private_key then
                return false, "missing valid private key"
            end
        end
    end

    return true
end


local function fetch_jwt_token(ctx)
    local token = core.request.header(ctx, "authorization")
    if token then
        local prefix = sub_str(token, 1, 7)
        if prefix == Bearer  or prefix == bearer  then
            return sub_str(token, 8)
        end

        return token
    end

    token = ctx.var.arg_jwt
    if token then
        return token
    end

    local cookie, err = ck:new()
    if not cookie then
        return nil, err
    end

    local val, err = cookie:get("jwt")
    return val, err
end


local function get_secret(conf)
    if conf.base64_secret then
        return ngx_decode_base64(conf.secret)
    end

    return conf.secret
end


local function get_real_payload(key, auth_conf, payload)
    local real_payload = {
        key = key,
        exp = ngx_time() + auth_conf.exp
    }
    if payload then
        local extra_payload = core.json.decode(payload)
        core.table.merge(real_payload, extra_payload)
    end
    return real_payload
end


local function sign_jwt_with_HS(key, auth_conf, payload)
    local auth_secret = get_secret(auth_conf)
    local ok, jwt_token = pcall(jwt.sign, _M,
        auth_secret,
        {
            header = {
                typ = "JWT",
                alg = auth_conf.algorithm
            },
            payload = get_real_payload(key, auth_conf, payload)
        }
    )
    if not ok then
        core.log.warn("failed to sign jwt, err: ", jwt_token.reason)
        core.response.exit(500, "failed to sign jwt")
    end
    return jwt_token
end


local function sign_jwt_with_RS256(key, auth_conf, payload)
    local ok, jwt_token = pcall(jwt.sign, _M,
        auth_conf.private_key,
        {
            header = {
                typ = "JWT",
                alg = auth_conf.algorithm,
                x5c = {
                    auth_conf.public_key,
                }
            },
            payload = get_real_payload(key, auth_conf, payload)
        }
    )
    if not ok then
        core.log.warn("failed to sign jwt, err: ", jwt_token.reason)
        core.response.exit(500, "failed to sign jwt")
    end
    return jwt_token
end


local function algorithm_handler(consumer)
    if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm == "HS256"
            or consumer.auth_conf.algorithm == "HS512" then
        return sign_jwt_with_HS, get_secret(consumer.auth_conf)
    elseif consumer.auth_conf.algorithm == "RS256" then
        return sign_jwt_with_RS256, consumer.auth_conf.public_key
    end
end


function _M.rewrite(conf, ctx)
    local jwt_token, err = fetch_jwt_token(ctx)
    if not jwt_token then
        if err and err:sub(1, #"no cookie") ~= "no cookie" then
            core.log.error("failed to fetch JWT token: ", err)
        end

        return 401, {message = "Missing JWT token in request"}
    end

    local jwt_obj = jwt:load_jwt(jwt_token)
    core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
    if not jwt_obj.valid then
        return 401, {message = jwt_obj.reason}
    end

    local user_key = jwt_obj.payload and jwt_obj.payload.key
    if not user_key then
        return 401, {message = "missing user key in JWT token"}
    end

    local consumer_conf = consumer_mod.plugin(plugin_name)
    if not consumer_conf then
        return 401, {message = "Missing related consumer"}
    end

    local consumers = lrucache("consumers_key", consumer_conf.conf_version,
        create_consume_cache, consumer_conf)

    local consumer = consumers[user_key]
    if not consumer then
        return 401, {message = "Invalid user key in JWT token"}
    end
    core.log.info("consumer: ", core.json.delay_encode(consumer))

    local _, auth_secret = algorithm_handler(consumer)
    jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj)
    core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))

    if not jwt_obj.verified then
        return 401, {message = jwt_obj.reason}
    end

    consumer_mod.attach_consumer(ctx, consumer, consumer_conf)
    core.log.info("hit jwt-auth rewrite")
end

local function gen_token()
    local args = ngx.req.get_uri_args()
    if not args or not args.key or not args.password then   --changed
        return core.response.exit(400)
    end

    local key = args.key
    local payload = args.payload
    local password = args.password   --changed
    if payload then
        payload = ngx.unescape_uri(payload)
    end

    local consumer_conf = consumer_mod.plugin(plugin_name)
    if not consumer_conf then
        return core.response.exit(404)
    end

    local consumers = lrucache("consumers_key", consumer_conf.conf_version,
        create_consume_cache, consumer_conf)

    core.log.info("consumers: ", core.json.delay_encode(consumers))
    local consumer = consumers[key]
    
    if ((not consumer) or (password ~= consumer.auth_conf.password)) then   --changed
        return core.response.exit(404)
    end

    core.log.info("consumer: ", core.json.delay_encode(consumer))

    local sign_handler, _ = algorithm_handler(consumer)
    local jwt_token = sign_handler(key, consumer.auth_conf, payload)
    if jwt_token then
        return core.response.exit(200, jwt_token)
    end

    return core.response.exit(404)
end

function _M.api()
    return {
        {
            methods = {"GET"},
            uri = "/apisix/plugin/jwt/sign",
            handler = gen_token,
        }
    }
end

return _M

 

apisix基于username和password的JWT验证插件

上一篇:原生JavaScript常用本地浏览器存储方法一(方法类型)


下一篇:Windows编程(网络编程)