0%

如何开发一个APISIX插件

Lua

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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
-- 自定义插件,实现jiankunking网关现有AuthN能力)
--[[
一、插件逻辑:
1. 获取token(header+cookie)
2. 判断token iss
3. iss 包含xxxxx,则进行xxxxx认证
3.1. 若kid为空
3.1.1. 如果加密算法是hs 256则从lua文件中读取密钥
3.1.2. 如果加密算法是rs 256则从lua文件中读取公钥
3.1.3. 抛出异常=>签名算法不支持
3.2. 若kid不为空
3.2.1. 通过API开放平台获取公钥
4. iss 不包含xxxxx,则进行jiankunking认证
.1. 如果加密算法是rs 512则从lua文件中读取公钥
4.1. 如果加密算法是hs 512则从lua文件中读取密钥

二、插件配置:
1. env: 环境(dev,test,pre,prod)
2. cookie_name: cookie名称
3. jiankunking_jwt_sign_key: jiankunking网关jwt签名密钥
4. allow_cookie: 是否允许从cookie中获取token

三、jiankunking公钥、Open平台地址没有做成配置的原因:
1. jiankunking公钥、Open平台地址变动的概率很低
2. 配置需要在每一个Api上配置,如果要更新公钥或者开发平台地址,则需要更新每一个Api
3. 配置在每一个Api上,这样暴露的信息就太多了

代码格式化地址:https://www.easydebug.net/format-lua

]]

local core = require("apisix.core")
local ngx = ngx
local jwt = require("resty.jwt")
local http = require"resty.http"
local fmt = string.format
local sub_str = string.sub
local plugin = require("apisix.plugin")

-- 获取缓存对象 使用方法时 ngx.share.XXX
local secret_cache = ngx.shared.jiankunking_jwt_secret_cache -- 不想跟xxxxx公用一个缓存,避免相互影响
local http_timeout = 6000
local plugin_name = "jiankunking-authn"

local http_unauthorized = 401
local business_unauthorized_code = 40001
local jwt_alg_hs256 = "HS256"
local jwt_alg_hs512 = "HS512"
local jwt_alg_rs256 = "RS256"
local jwt_alg_rs512 = "RS512"

-- jiankunking网关请求标识
local header_jiankunking_request = "GATEWAY"
local header_jiankunking_request_value = header_jiankunking_request

local schema = {
type = "object",
properties = {
env = {
type = "string",
enum = {
"dev",
"test",
"pre",
"prod"
},
default = "dev"
},
allow_cookie = {
type = "boolean",
default = false
}
},
required = {
"env"
}
}

local props
local metadata_schema = {
type = "object",
properties = {
prod = props,
pre = props,
test = props,
dev = props
}
}

local _M = {
version = 0.1,
priority = 10,
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema
}

local function str_is_not_empty(str)
if str and string.len(str) ~= 0 then
return true
end
return false
end

local function clear_request_header(ctx)
core.request.set_header(ctx, "X-USER", nil)
core.request.set_header(ctx, header_jiankunking_request, nil)
return
end

local function set_request_header(ctx, uid)
core.request.set_header(ctx, "X-USER", uid)
core.log.info("X-USER:", uid)
core.request.set_header(ctx, header_jiankunking_request, header_jiankunking_request_value)
return
end

local function is_token_allowed_from_cookie(conf)
if conf.allow_cookie == nil then
core.log.info("allow_cookie is nil")
return false
end
local ac = conf.allow_cookie
core.log.info("allow_cookie: ", ac)
if ac == true then
return true
end
return false
end

local function get_token_from_request(conf)
-- 从请求的 header 中获取 authorization 字段
local auth_header = ngx.req.get_headers()["authorization"]
core.log.info("auth_header: ", auth_header)
if auth_header then
-- 移除前缀 "Bearer "
local token = auth_header:match("^Bearer%s+(.*)$")
core.log.info("token: ", token)
if token then
return token
end
end
if not is_token_allowed_from_cookie(conf) then
return nil
end
core.log.info("get token from cookie")

-- 如果 header 中没有获取到 token,则从 cookie 中获取
-- https://nginx.org/en/docs/http/ngx_http_core_module.html#var_cookie_
-- 相当于写死了cookie_name为jkk_jwt
local jkk_jwt_cookie = ngx.var.cookie_jkk_jwt
if jkk_jwt_cookie then
return jkk_jwt_cookie
end

return nil
end

local function get_jiankunking_secret_from_local(env)
local jiankunking_jwt_key = "x_login_2019" -- 这个目前jiankunking的登录环境中已经不用这种加密方式了,保留只是为了兼容一些未知情况
if env == "dev" or env == "test" or env == "pre" then
return jiankunking_jwt_key
elseif env == "prod" then
return jiankunking_jwt_key
end
end


--[[
metadata结构如下:
{
"dev": {
"gateway_openplat_secret": "123",
"jkk_public_key": "公钥base64之后的内容",
"openplat_address": "https://test.jiankungking.com"
},
"pre": {
"gateway_openplat_secret": "1234",
"jkk_public_key": "公钥base64之后的内容",
"openplat_address": "https://test.jiankungking.com"
},
"prod": {
"gateway_openplat_secret": "12345",
"jkk_public_key": "公钥base64之后的内容",
"openplat_address": "https://jiankungking.com"
},
"test": {
"gateway_openplat_secret": "123456",
"jkk_public_key": "公钥base64之后的内容",
"openplat_address": "https://test.jiankungking.com"
}
}
]]
local function get_jiankunking_public_key_from_local(env, metadata)
if metadata and metadata.value then
core.log.info("metadata:", core.json.delay_encode(metadata.value))
for key, value in pairs(metadata.value) do
core.log.info("env:", env)
core.log.info("key:", key)
core.log.info("value:", core.json.delay_encode(value))
if key == env and str_is_not_empty(value.jiankunking_public_key) then
local decoded = ngx.decode_base64(value.jiankunking_public_key)
if not decoded then
core.log.error("Failed to decode jiankunking_public_key:" .. value.jiankunking_public_key)
else
-- TODO 这里文本打印不全 只输出了第一行
core.log.info("get jiankunking_public_key from metadata:[[", decoded, "]]")
return decoded
end
end
end
end
core.log.info("get jiankunking_public_key from code config")
if env == "dev" or env == "test" or env == "pre" then
return [[-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----]]
elseif env == "prod" then
return [[-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----]]
end
end

-- 下面这种方式已核对过 这些公钥已经废弃了
-- 这里保留的目的是为了 兼容一些未知的情况
local function get_xx_public_key_from_local_by_env(env)
-- 默认值
if env == "dev" or env == "test" or env == "pre" then
return [[-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----]]
elseif env == "prod" then
return [[-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----]]
end
end

local function get_openplat_info_by_env(env, metadata)
if metadata and metadata.value then
core.log.info("metadata:", core.json.delay_encode(metadata.value))
for key, value in pairs(metadata.value) do
core.log.info("env:", env)
core.log.info("key:", key)
core.log.info("value:", core.json.delay_encode(value))
if key == env and str_is_not_empty(value.openplat_address) and str_is_not_empty(value.gateway_openplat_secret) then
return {
openplat_address = value.openplat_address .. "/api/v2/secrets",
gateway_openplat_secret = value.gateway_openplat_secret
}
end
end
end
core.log.info("get openplat_info from code config")
if env == "dev" or env == "test" or env == "pre" then
return {
openplat_address = "https://test.jiankungking.com/api/v2/secrets",
gateway_openplat_secret = "1"
}
elseif env == "prod" then
return {
openplat_address = "https://jiankungking.com/api/v2/secrets",
gateway_openplat_secret = '2'
}
end
end

local function http_request(url, headers)
core.log.info("Sending HTTP request to: ", url)
core.log.info("Request headers: ", core.json.delay_encode(headers))
local httpc = http.new()
httpc:set_timeout(http_timeout)
local res, err = httpc:request_uri(url, {
method = 'GET',
headers = headers,
ssl_verify = false,
})
if not res then
core.log.warn("failed request to " .. url .. ": " .. err)
return false , err
end
core.log.info("HTTP response status: ", res.status)
core.log.info("HTTP response body: ", res.body)
return res , nil
end

-- 自定义缓存的key,保证即使公用缓存也不会冲突
local function get_cache_key_by_kid(kid)
-- Bug 修复:使用字符串连接运算符 ..
return "jiankunking_authn_" .. kid
end

local function update_xx_public_key_info_from_openplat_by_env(env, metadata)
local openplat_info = get_openplat_info_by_env(env, metadata)
local openplat_address = openplat_info.openplat_address
local gateway_openplat_secret = openplat_info.gateway_openplat_secret
local headers = {
["Authorization"] = gateway_openplat_secret
}
local res , err = http_request(openplat_address, headers)
if not res then
core.log.warn("failed request to " .. openplat_address .. ": " .. err)
return false
end
local response_body = res.body
core.log.info("data:", fmt("load xx_public_key_info from %s, response status: %d, body: %s", openplat_address, res.status, response_body))
if res.status ~= 200 then
core.log.warn(fmt("http error, status: %d", res.status))
return false
end
local response_json = core.json.decode(response_body)
if not response_json then
core.log.warn(fmt("json parse error, text: %s", response_body))
return false
end
if response_json['code'] ~= 200 then
core.log.warn("Result error, code: ", response_json['code'], ", msg: ", response_json['msg'])
return false
end
local data = response_json['data']
for _, secretx in pairs(data) do
local datax = core.json.encode({
kid = secretx.kid,
secret = secretx.publicKey,
start_time = secretx.startTime,
end_time = secretx.endTime,
})
-- 获取当前时间戳(秒)
local timestamp = os.time()
core.log.info("当前时间戳(秒): ", timestamp)
local ttl = secretx.endTime - timestamp
core.log.info("ttl: ", ttl)
-- 设置缓存
if ttl > 0 then
local cache_key = get_cache_key_by_kid(secretx.kid)
secret_cache:set(cache_key, datax, ttl)
core.log.info(fmt("set secret cache, key: %s, value: %s , ttl: %d", cache_key, datax, ttl))
else
core.log.warn(fmt("the secret has expired , key: %s, value: %s, ttl: %d", secretx.kid, datax, ttl))
end

end
return false
end

-- 下面这种方式已核对过 这些密钥已经废弃了
-- 这里保留的目的是为了 兼容一些未知的情况
local function get_xx_jwt_secret_from_local_by_env(env)
-- 默认值
if env == "dev" or env == "test" or env == "pre" then
return "22"
elseif env == "prod" then
return "33"
end
end

local function get_xx_public_key_info_from_cache_by_env(env, kid, metadata)
local cache_key = get_cache_key_by_kid(kid)
local secret = secret_cache:get(cache_key)
core.log.info("secretcache:", secret)

if secret ~= "" and secret ~= nil then
local data = core.json.decode(secret)
if data.end_time > ngx.now() then
return data
else
core.log.warn("the secret has expired, key: ", kid)
end
end

local start_time = ngx.now()
update_xx_public_key_info_from_openplat_by_env(env, metadata)
local end_time = ngx.now()
local elapsed_time = end_time - start_time
core.log.info("update_xx_public_key_info_from_openplat_by_env 方法耗时: ", elapsed_time, " 秒")

secret = secret_cache:get(cache_key)
if secret ~= "" and secret ~= nil then
return core.json.decode(secret)
end
return false
end


function _M.check_schema(conf, schema_type)
if schema_type == core.schema.TYPE_METADATA then
return core.schema.check(metadata_schema, conf)
end
return core.schema.check(schema, conf)
end

local function is_xxxxx_token(iss)
-- 判断字符串是否以 "xxxxx" 开头
core.log.info("iss: ", iss)
if string.len(iss) < 5 then
return false
end
local xxxxx = string.sub(iss, 1, 5)
if xxxxx == "xxxxx" then
return true
else
return false
end
end


function _M.access(conf, ctx)
local token = get_token_from_request(conf)
if not token then
return http_unauthorized, {
code = business_unauthorized_code,
msg = "未获取到token"
}
end
local jwt_obj = jwt:load_jwt(token)
core.log.info("jwt object: ", core.json.delay_encode(jwt_obj))
if not jwt_obj.valid then
return http_unauthorized, {
code = business_unauthorized_code,
msg = jwt_obj.reason
}
end
-- 加载metadata数据
local metadata = plugin.plugin_metadata(plugin_name)
core.log.info("metadata:", core.json.delay_encode(metadata))

local iss = jwt_obj.payload.iss
local env = conf.env
local is_xx_token = is_xxxxx_token(iss)
local alg = jwt_obj.header.alg
if is_xx_token then
local kid = jwt_obj.header.kid
if not jwt_obj.header.kid then
core.log.info("match alg: ", alg)
if alg == jwt_alg_hs256 then
local jwt_secret = get_xx_jwt_secret_from_local_by_env(env)
jwt_obj = jwt:verify_jwt_obj(jwt_secret, jwt_obj)
elseif jwt_obj.header.alg == jwt_alg_rs256 then
local jwt_secret = get_xx_public_key_from_local_by_env(env)
jwt_obj = jwt:verify_jwt_obj(jwt_secret, jwt_obj)
else
return http_unauthorized, {
code = business_unauthorized_code,
msg = "签名算法不支持"
}
end
else
core.log.info("match kid : ", kid)
local public_key_info = get_xx_public_key_info_from_cache_by_env(env, kid, metadata)
local iat = jwt_obj.payload['iat']
if not public_key_info then
return http_unauthorized, {
code = business_unauthorized_code,
msg = "公钥不存在"
}
end
if public_key_info.start_time > iat then
core.log.warn("kid:", kid, ",start_time:", public_key_info.start_time, ",current time:", ngx.now())
return http_unauthorized, {
code = business_unauthorized_code,
msg = "公钥未生效"
}
end
if public_key_info.end_time < iat then
core.log.warn("kid:", kid, ",end_time:", public_key_info.end_time, ",current time:", ngx.now())
return http_unauthorized, {
code = business_unauthorized_code,
msg = "公钥已过期"
}
end
jwt_obj = jwt:verify_jwt_obj(public_key_info.secret, jwt_obj)
end
else
core.log.info("match alg: ", alg)
-- iss 不包含xxxxx,则进行jiankunking认证
if alg == jwt_alg_rs512 then
local jiankunking_public_key = get_jiankunking_public_key_from_local(env, metadata)
jwt_obj = jwt:verify_jwt_obj(jiankunking_public_key, jwt_obj)
elseif alg == jwt_alg_hs512 then
local jiankunking_jwt_secret = get_jiankunking_secret_from_local(env)
jwt_obj = jwt:verify_jwt_obj(jiankunking_jwt_secret, jwt_obj)
else
return http_unauthorized, {
code = business_unauthorized_code,
msg = "签名算法不支持"
}
end
end
core.log.info("jwt_obj object: ", core.json.delay_encode(jwt_obj))
if not jwt_obj.verified then
core.log.info("jwt_obj.reason: ", jwt_obj.reason)
if sub_str(jwt_obj.reason, 1, 19) == "'exp' claim expired" then
return http_unauthorized, {
code = business_unauthorized_code,
msg = "access_token已过期"
}
elseif sub_str(jwt_obj.reason, 1, 18) == "signature mismatch" then
return http_unauthorized, {
code = business_unauthorized_code,
msg = "access_token非法"
}
else
return http_unauthorized, {
code = business_unauthorized_code,
msg = jwt_obj.reason
}
end
end
clear_request_header(ctx)
local uid
if is_xx_token then
core.log.info("jwt_obj.payload.sub: ", jwt_obj.payload.sub)
uid = jwt_obj.payload.sub
else
uid = jwt_obj.payload.sub
core.log.info("jwt_obj.payload.sub: ", jwt_obj.payload.sub)
end
core.log.info("uid: ", uid)
set_request_header(ctx, uid)
end

return _M

Golang

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
package plugins

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"

pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
"github.com/apache/apisix-go-plugin-runner/pkg/log"
"github.com/apache/apisix-go-plugin-runner/pkg/plugin"
)

func init() {
err := plugin.RegisterPlugin(&Am{})
if err != nil {
log.Fatalf("failed to register plugin am: %s", err)
}
if len(amUrl) == 0 {
log.Fatalf("AM address is not configured")
}
}

var (
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, e error) {
return (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 90 * time.Second,
}).DialContext(ctx, network, addr)
},
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: false,
},
}
// 每个插件配置都配置不如在这里写死
// dev am.test.jiankungking.com
// prod am.jiankungking.com
amUrl = ""
)

type Am struct {
}

type AmConf struct {
Tenant string `json:"tenant"`
Resources []string `json:"resources"` // 资源name列表
Action string `json:"action"` // action操作名称
UserId string `json:"userId"` // 用户id
//Url string `json:"url"` // 权限中心地址
}

func (p *Am) Name() string {
return "am"
}

func (p *Am) ParseConf(in []byte) (interface{}, error) {
conf := AmConf{}
err := json.Unmarshal(in, &conf)
if err != nil {
fmt.Sprintf("Unmarshal AmConf error:%s", err.Error())
return conf, err
}
//debugLog(conf)
if len(conf.Resources) == 0 {
msg := "am plugin not find resources in conf"
fmt.Println(msg)
return conf, errors.New(msg)
}
//if len(conf.Url) == 0 {
// msg := "am plugin not find url in conf"
// fmt.Println(msg)
// return conf, errors.New(msg)
//}
if len(conf.Tenant) == 0 {
msg := "am plugin not find tenant in conf"
fmt.Println(msg)
return conf, errors.New(msg)
}
return conf, nil
}

func (p *Am) Filter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) {
ac := conf.(AmConf)
if ac.Action == "" {
ac.Action = "*"
}
//debugLog(conf)
ac.UserId = r.Header().Get("X-USER")
if len(ac.UserId) == 0 {
msg := "am plugin not find userId in conf"
fmt.Println(msg)
w.WriteHeader(http.StatusUnauthorized)
return
}
fmt.Println("begin check am perm")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
hasPerm, err := checkPerm(ctx, ac)
if err != nil {
fmt.Println(err.Error())
w.WriteHeader(http.StatusForbidden)
fmt.Println("finish check am perm,result:false")
return
}

if !hasPerm {
w.WriteHeader(http.StatusForbidden)
fmt.Println("finish check am perm,result:false")
return
}
fmt.Println("finish check am perm,result:true")
return
}

func checkPerm(ctx context.Context, ac AmConf) (bool, error) {
defer timeCost("AM CheckPerm")()

// 校验权限:通过则返回true,不通过返回false

return true, nil
}

type SingleResourceResp struct {
Result bool `json:"result"` // ture(允许) or false(拒绝)
Action []string `json:"actions,omitempty"` // 合并后的action列表
}

type ResourceResp map[string]*SingleResourceResp // 每个资源的返回结果,资源名->返回结果

type AuthzResponse struct {
*BaseResponse
Data ResourceResp `json:"data"` // 每个资源对饮的返回结果
}
type BaseResponse struct {
Code int `json:"code" example:"10000"` // 10000:成功 <br> 10004: 未发现 <br> 15000: 内部错误 <br>
Message string `json:"msg" example:"success"`
Detail string `json:"detail,omitempty" example:""`
}

// timeCost 耗时统计函数
func timeCost(msg string) func() {
start := time.Now()
return func() {
tc := time.Since(start)
fmt.Printf(msg+" time cost = %d ms\n", tc.Milliseconds())
}
}

func debugLog(obj interface{}) {
if obj == nil {
return
}
jsons, _ := json.Marshal(obj)
fmt.Println(string(jsons))
}