cn_wumo
文章4
标签4
分类4
apisix基于username和password的JWT验证插件

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目录

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

修改默认配置

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

键入配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
……
……
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的值,配置公钥和私钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
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

插件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
--
-- 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
本文作者:cn_wumo
本文链接:http://cn-wumo.top/2021/09-11-apisix%E5%9F%BA%E4%BA%8Eusername%E5%92%8Cpassword%E7%9A%84JWT%E9%AA%8C%E8%AF%81%E6%8F%92%E4%BB%B6/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可