插件说明
基于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", -- can‘t 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