插件概述
插件之于kong,就像Spring中的aop功能。
在请求到达kong之后,转发给后端应用之前,你可以应用kong自带的插件对请求进行处理,合法认证,限流控制,黑白名单校验,日志采集等等。同时,你也可以按照kong的教程文档,定制开发属于自己的插件。
kong的插件分为开源版和社区版,社区版还有更多的定制功能,但是社区版是要收费的。
目前,KONG开源版本一共开放28个插件,如下:
acl、aws-lambda、basic-auth、bot-detection、correlation-id、cors、datadog、file-log、galileo、hmac-auth、http-log、ip-restriction、jwt、key-auth、ldap-auth、loggly、oauth2、rate-limiting、request-size-limiting、request-termination、request-transformer、response-ratelimiting、response-transformer、runscope、statsd、syslog、tcp-log、udp-log
以上插件,主要分五大类,Authentication认证,Security安全,Traffic Control流量控制,Analytics & Monitoring分析&监控,Logging日志,其他还有请求报文处理类。
熔断request-termination插件
该插件用来定义指定请求或服务不进行上层服务,而直接返回指定的内容.用来为指定的请求或指定的服务进行熔断.
这样再访问指定的服务就会返回403错误,消息为So long and thanks for all the fish!
可以参考:https://docs.konghq.com/hub/kong-inc/request-termination/
限流rate-limiting插件
为"example”的APIS添加rate-limiting插件,步骤如下:
点击按钮如下图:
可以不用配置redis,不过要设置限制方法,我设置了每秒不超过1次。
没超过1次时,返回如下:
当请求超过1次,会出现
说明:
根据年、月、日、时、分、秒设置限流规则,多个限制同时生效。
比如:每天不能超过10次调用,每分不能超过3次。
当一分钟内,访问超过3次,第四次就会报错。
当一天内,访问次数超过10次,第十一次就会报错。
IP黑白名单ip-restriction限制插件
IP限制插件,是一个非常简单的插件,可以设置黑名单IP,白名单IP这个很简单。
规则:
IP黑白名单,支持单个,多个,范围分段IP(满足CIDR notation规则)。多个IP之间用逗号,分隔。
CIDR notation规范如下:
10.10.10.0/24 表示10.10.10.*的都不能访问。
关于CIDR notation的规则,不在本文讨论范围内,请自行查阅https://zh.wikipedia.org/wiki/%E6%97%A0%E7%B1%BB%E5%88%AB%E5%9F%9F%E9%97%B4%E8%B7%AF%E7%94%B1
1.设置黑名单IP
在这里,我将我自己的IP设置成黑名单.
在这里插入图片描述
似乎我安装的kong-dashboard黑白名单写反了。
基本认证Basic Authentication插件
在Consumers 页面,添加Basic Auth
在这里插入图片描述
输入用户名和密码,我这里设置为luanpeng luanpeng。计算认证头。获取luanpeng:luanpeng字符串的base64编码。
可以直接在linux下输出
$ echo "luanpeng:luanpeng"|base64
bHVhbnBlbmc6bHVhbnBlbmcK
1
2
3
在插件页面,设置Basic Auth 绑定目标service,这样请求目标service就需要在http头中添加
Authorization Basic bHVhbnBlbmc6bHVhbnBlbmcK
1
在这里插入图片描述
设置Basic Auth表单域参数介绍:
表单域名称 默认值 描述
name(必填) 无 插件名称,在这里该插件名称为:basic-auth
config.hide_credentials(选填) false boolean类型,告诉插件,是否对上游API服务隐藏认证信息。如果配置true,插件将会把认证信息清除,然后再把请求转发给上游api服务。
config.anonymous(选填) 空 String类型,用来作为匿名用户,如果认证失败。如果空,当请求失败时,返回一段4xx的错误认证信息。
key认证key-Auth插件
该插件很简单,利用提前预设好的关键字名称,如下面设置的keynote = apices,然后为consumer设置一个key-auth 密钥,假如key-auth=test@keyauth。
在请求api的时候,将apikey=test@keyauth,作为一个参数附加到请求url后,或者放置到headers中。
在插件页面添加key-auth插件
在这里插入图片描述
配置consumer key-auth
在这里插入图片描述
key-auth两种方式可通过校验
curl http://xxx.xx.xx.xx:xxx/xxx -H 'apikey: luanpeng'
http://xxx.xxx.xxx.xxx:xxx/xxx?apikey=luanpeng
1
2
如果选中key_in_body, 则必须在传递body的参数中加入{“apikey”:“xxxx”}来实现认证.
HMAC认证
先启动HMAC插件,设置绑定的service和rout,以启动hmac验证。然后在Consumers页面中Hmac credentials of Consumer设置中添加一个username和secret。
在这里插入图片描述
准备生成http的header中的签名。请求是使用该签名。这里附上python的调用包
# kong_hmac.py
import base64
import hashlib
import hmac
import re
from wsgiref.handlers import format_date_time
from datetime import datetime
from time import mktime
def create_date_header():
now = datetime.now()
stamp = mktime(now.timetuple())
return format_date_time(stamp)
def get_headers_string(signature_headers):
headers = ""
for key in signature_headers:
if headers != "":
headers += " "
headers += key
return headers
def get_signature_string(signature_headers):
sig_string = ""
for key, value in signature_headers.items():
if sig_string != "":
sig_string += "\n"
if key.lower() == "request-line":
sig_string += value
else:
sig_string += key.lower() + ": " + value
return sig_string
def md5_hash_base64(string_to_hash):
m = hashlib.md5()
m.update(string_to_hash)
return base64.b64encode(m.digest())
# sha1签名算法,字符串的签名,并进行base64编码
def sha1_hash_base64(string_to_hash, secret):
h = hmac.new(secret, (string_to_hash).encode("utf-8"), hashlib.sha1)
return base64.b64encode(h.digest())
def generate_request_headers(username, secret, url, data=None, content_type=None):
# Set the authorization header template
auth_header_template = (
'hmac username="{}",algorithm="{}",headers="{}",signature="{}"'
)
# Set the signature hash algorithm
algorithm = "hmac-sha1"
# Set the date header
date_header = create_date_header() # 产生GMT格式时间
# print('GMT时间:',date_header)
# Set headers for the signature hash
signature_headers = {"date": date_header}
# Determine request method
if data is None or content_type is None:
request_method = "GET"
else:
request_method = "POST"
# MD5 digest of the content
base64md5 = md5_hash_base64(data)
# Set the content-length header
content_length = str(len(data))
# Add headers for the signature hash
signature_headers["content-type"] = content_type
signature_headers["content-md5"] = base64md5
signature_headers["content-length"] = content_length
# Strip the hostname from the URL
target_url = re.sub(r"^https?://[^/]+/", "/", url)
# print('请求路径:',target_url)
# Build the request-line header
request_line = request_method + " " + target_url + " HTTP/1.1"
# print('request_line:',request_line)
# Add to headers for the signature hash
signature_headers["request-line"] = request_line
# Get the list of headers
headers = get_headers_string(signature_headers) # 转化为list
# print('签名的属性名称:',headers)
# Build the signature string
signature_string = get_signature_string(signature_headers) # 获取要签名的字符串
# print('要签名的字符串:',signature_string)
# Hash the signature string using the specified algorithm
signature_hash = sha1_hash_base64(signature_string, secret) # 签名
# print('签名后字符串:',signature_hash)
# Format the authorization header
auth_header = auth_header_template.format(
username, algorithm, headers, signature_hash.decode('utf-8')
)
if request_method == "GET":
request_headers = {"Authorization": auth_header, "Date": date_header}
else:
request_headers = {
"Authorization": auth_header,
"Date": date_header,
"Content-Type": content_type,
"Content-MD5": base64md5,
"Content-Length": content_length,
}
return request_headers
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
调用该包,demo如下
# get示例
username = 'vesionbook'
secret = 'vesionbook'.encode('utf-8')
url = 'http://192.168.11.127:30309/arctern'
request_headers = generate_request_headers(username, secret, url)
print('请求头:',request_headers)
r = requests.get(url, headers=request_headers)
print('Response code: %d\n' % r.status_code)
print(r.text)
1
2
3
4
5
6
7
8
9
10
11
jwt认证插件
先为Consumer消费者建立jwt凭证
在这里插入图片描述
在线JWT编码和解码https://jwt.io/
在这里插入图片描述
图中HEADER 部分声明了验证方式为 JWT,加密算法为 HS256
PAYLOAD 部分原本有 5 个参数
{
"iss": "kirito", # Consumer的jwt中设置的key
"iat": 1546853545, # 签发时间戳
"exp": 1546853585, # 过期时间戳
"nbf": 1546853585 # 生效日期
"aud": "cnkirito.moe",
"sub": "250577914@qq.com",
}
1
2
3
4
5
6
7
8
这里面的前五个字段都是由 JWT 的标准(RFC7519)所定义的。
iss: 该 JWT 的签发者,(验证的时候判断是否是签发者)
sub: 该 JWT 所面向的用户,(验证的时候判断是否是所有者)
aud: 接收该 JWT 的一方,标识令牌的目标受众。(验证的时候判断我是否是其中一员)
exp(expires): 什么时候过期,这里是一个 Unix 时间戳,精确到s, ,它必须大于jwt的签发时间
iat(issued at): 在什么时候签发的,精确到s的时间戳, claims_to_verify配置参数不允许设置iat
nbf:定义jwt的生效时间
jti:jwt唯一身份标识,主要用来作为一次性token来使用,从而回避重放攻击
iss 这一参数在 Kong 的 Jwt 插件中对应的是curl http://127.0.0.1:8001/consumers/kirito/jwt 获取的用户信息中的 key 值。
而其他值都可以选填.
在页面上VERIFY SIGNATURE中填入自己的secret, 也就是在kong的dashboard中消费者创建jwt证书时的secret.
我们使用 jwt 官网(jwt.io)提供的 Debugger 功能快速生成我们的 Jwt, 由三个圆点分隔的长串便是用户身份的标识了.
打开kong的jwt插件
在这里插入图片描述
在key_claim_name中定义存储key的字段名称. 我们是使用的iss字段.
cookie_names表示如果使用cookie传递证书, 则cookie中的名称.
claims_to_verify表示验证证书中哪些字段, 我这里验证证书的发布时间和过期时间.
然后在header中携带证书信息就可以了.
在这里插入图片描述
Jwt 也可以作为 QueryString 参数携带在 get 请求中
curl http://localhost:8000/hello/hi?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ2Y252WVNGelRJR3lNeHpLU2duTlUwdXZ4aXhkWVdCOSJ9.3iL4sXgZyvRx2XtIe2X73yplfmSSu1WPGcvyhwq7TVE
1
如果在插件配置中设置了cookie_names为luanpeng-cookie
则在发送中
--cookie luanpeng-cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJYSnFRMXpSQVhUWk52dlNHZ1Nsb1FyejczOFBqT0hFZyIsImV4cCI6MTUyNTc5MzQyNSwibmJmIjoxNTI1Nzc1NDI1LCJpYXQiOjE1MjU3NzU0MjV9.0Cv8rJkXTMNKAvPTOBV1w0UYVhRx3XRb6xJofxloRuA
1
不同配置下,可能返回证书未生效, 证书已过期, 或者返回正常结果
通常用户需要自己写一个服务去帮助 Consumer 生成自己的 Jwt,自然不能总是依赖于 Jwt 官方的 Debugger,当然也没必要重复造*(尽管这并不难),可以考虑使用开源实现,在jwt官网上Libraries for Token Signing/Verification部分 根据自己使用的语言,选择对应的包,来实现证书生成器. 最好可以直接集成到api网关中.
这里用python实现了一个简单的签名生成器
import sys
import os
dir_common = os.path.split(os.path.realpath(__file__))[0] + '/../'
sys.path.append(dir_common) # 将根目录添加到系统目录,才能正常引用common文件夹
from aiohttp import web
import asyncio
import logging
import uvloop
import time,datetime
import jwt
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
routes = web.RouteTableDef()
# 返回客户的json信息
def write_response(status,message,result):
response={
'status':status, # 状态,0为成功,1为失败
'message':message, # 错误或成功描述。字符串
'result':result # 成功的返回结果,字典格式
}
return response
@routes.get('/')
async def hello(request):
return web.Response(text="Hello, world")
# 签名
@routes.post('/sign')
async def sign(request): # 异步监听,只要一有握手就开始触发
try:
data = await request.json() # 等待post数据完成接收,只有接收完成才能进行后续操作.data['key']获取参数
except Exception as e:
logging.error("image file too large or cannot convert to json")
return web.json_response(write_response(1,"image file too large or cannot convert to json",{}))
logging.info('license sign request start, data is %s,%s' % (data, datetime.datetime.now()))
if "username" not in data or 'password' not in data:
logging.error("username or password not in data")
return web.json_response(write_response(2, "username or password not in data", {}))
payload = {
"iss": data['username'],
"iat": int(time.time()),
"exp": int(time.time()) + 60*60, # 有效期一个小时
}
encoded_jwt = jwt.encode(payload, data['password'], algorithm='HS256')
encoded_jwt = encoded_jwt.decode('utf-8')
logging.info('license sign request finish %s, %s' % (datetime.datetime.now(),encoded_jwt))
header = {"Access-Control-Allow-Origin": "*", 'Access-Control-Allow-Methods': 'GET,POST'}
result = write_response(0, "success",encoded_jwt)
# 同时放在cookie中
header['cookie']='--cookie aicloud-cookie='+encoded_jwt
return web.json_response(result,headers=header)
# 校验
@routes.post('/check')
async def check(request): # 异步监听,只要一有握手就开始触发
try:
data = await request.json() # 等待post数据完成接收,只有接收完成才能进行后续操作.data['key']获取参数
except Exception as e:
logging.error("image file too large or cannot convert to json")
return web.json_response(write_response(1,"image file too large or cannot convert to json",{}))
logging.info('license check request start, data is %s,%s' % (data,datetime.datetime.now()))
if "username" not in data or 'password' not in data or 'sign' not in data:
logging.error("username or password or sign not in data")
return web.json_response(write_response(2, "username or password or sign not in data", {}))
encoded_jwt = data['sign'].encode('utf-8')
payload = jwt.decode(encoded_jwt, data['password'], algorithms=['HS256'])
if payload['iss']!=data['username']:
logging.error("iss in sign != username")
return web.json_response(write_response(3, "username error", {}))
elif payload['iat']>time.time():
logging.error("sign not effective")
return web.json_response(write_response(4, "sign not effective", {}))
elif payload['exp']<time.time():
logging.error("sign lose effectiveness")
return web.json_response(write_response(5, "sign lose effectiveness", {}))
logging.info('license check request finish %s, %s' % (datetime.datetime.now(),encoded_jwt))
header = {"Access-Control-Allow-Origin": "*", 'Access-Control-Allow-Methods': 'GET,POST'}
result = write_response(0, "success", {})
return web.json_response(result,headers=header)
if __name__ == '__main__':
logger = logging.getLogger()
logger.setLevel(logging.INFO) # 最低输出等级
app = web.Application(client_max_size=int(1024)) # 创建app,设置最大接收图片大小为2M
app.add_routes(routes) # 添加路由映射
web.run_app(app,host='0.0.0.0',port=8080) # 启动app
logging.info('server close:%s'% datetime.datetime.now())
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
ACL授权插件
该插件相当于授权插件,授权必须建立在认证的基础上,认证和授权是相互独立的。
ACL策略插件
策略分组规则:
1).为用户分配授权策略组
2).为api添加授权策略分组插件。
3).只有拥有api授权策略分组的用户才可以调用该api。
4).授权策略分组,必须建立在认证机制上,该策略生效的前提,api至少要开启任意一个auth认证插件。
在这里插入图片描述
如果为同一service启用的授权和认证,则光认证是不行的。必须还要授权。将用户设置为授权组。
上面的设置以后,只有属于白名单组的用户才能访问该service,但是究竟哪些用户属于这些组呢,这需要去Consumers页面设置。
在这里插入图片描述
如果想限制某些用户访问某些路径,可以在路由处添加几个路由匹配,对不同的路由匹配设置授权
链路跟踪Zipkin插件
Zipkin 是一款开源的分布式实时数据追踪系统。其主要功能是聚集来自各个异构系统的实时监控数据,用来追踪微服务架构下的系统延时问题。应用系统需要向 Zipkin 报告数据。Kong的Zipkin插件作为zipkin-client就是组装好Zipkin需要的数据包,往Zipkin-server发送数据。
所以首先要部署一个zipkin服务端:参考https://blog.csdn.net/luanpeng825485697/article/details/85772954
部署结束后打开http://xx.xx.xx.xx:9411/api/v2/spans?servicename=test看是否能正常打开
启动zipkin插件:
在插件页面启动插件配置参数
config.http_endpoint :Zipkin接收数据的地址,配置http://xx.xx.xx.xx:9411/api/v2/spans
config.sample_ratio : 采样的频率。设为0,则不采样;设为1,则完整采样。默认为0.001也就是0.1%的采样率, 再调试阶段建议设置采样率为1.
zipkin插件会每次请求,打上如下标签,推送到zipkin服务端
span.kind (sent to Zipkin as “kind”)
http.method
http.status_code
http.url
peer.ipv4
peer.ipv6
peer.port
peer.hostname
peer.service
可以参考:https://github.com/Kong/kong-plugin-zipkin
启用后,此插件会以与zipkin兼容的方式跟踪请求。
代码围绕一个opentracing核心构建,使用opentracing-lua库来收集每个Kong阶段的请求的时间数据。该插件使用opentracing-lua兼容的提取器,注入器和记者来实现Zipkin的协议。
提取器和注射器
opentracing“提取器”从传入的请求中收集信息。如果传入请求中不存在跟踪ID,则基于sample_ratio配置值概率地生成一个跟踪ID 。
opentracing“injector”将跟踪信息添加到传出请求中。目前,仅对kong代理的请求调用注入器; 它不尚未用于请求到数据库,或通过其他插件(如HTTP日志插件)。
日志
目前在Kong的 free plugins中,比较常用的有这么三个:Syslog、File-Log以及Http-Log,下面对这三种插件逐一分析一下。
Syslog
顾名思义,这个插件是把Kong中记录的日志给打印到系统日志中,开启插件之后只需要指定需要使用的API,无需做多余的配置,即可在/var/log/message中发现对应的日志信息,d 但是系统日志鱼龙混杂,如果需要用到ELK等日志分析工具时,需要做一次数据清洗工作。
File-Log
与Syslog一样,File-log的配置也很方便,只需要配置日志路劲就行,开启插件之后,会在对应的对应产生一个logFile。Syslog中提到需要做一些日志清洗工作,但是换成了File-log乍一看好像解决了之前的痛点,实则不然,官方建议这个插件不适合在生产环境中使用,会带来一些性能上的开销,影响正常业务。
Http-Log
http-log是我比较推荐的,它的原理是设置一个log-server地址,然后Kong会把日志通过post请求发送到设置的log-server,然后通过log-server把日志给沉淀下来,相比之前两种插件,这一种只要启一个log-server就好了,出于性能考虑,我用Rust实现了一个log-server,有兴趣可以参考看一下。
prometheus可视化
kong自带的prometheus插件,metrics比较少, 可以网上查一下丰富版的prometheus插件.
比如:https://github.com/yciabaud/kong-plugin-prometheus
现在用这个插件替换kong自带的插件.
最方便的安装方式,一般linux机器上都会自带 luarocks(lua包管理程序),这样一来我们只要把 Plugins 所在的文件夹给移动到服务器的任意目录,然后在该目录下,执行luarocks make 这样一来插件便会自动安装到系统中,不过需要注意的是,此时插件还需要进行手动开启,首先进入/etc/kong/目录,然后cp kong.conf.default kong.conf, 这里注意一定要复制一份单独的kong.conf文件,不能直接对kong.conf.default进行修改,这样是不生效的,然后取消plugin = bundled前面的注释,在这一行后面增加你的插件名,这里注意插件名是不包含前缀 kong-plugin的,重启Kong即可在可视化界面里发现
plugins = bundled,prometheus
1
在使用新插件之前,需要更新一下数据库:
bash ./resty.sh kong/bin/kong migrations up -c kong.conf
1
爬虫控制插件bot-detection
备注:
config.whitelist :白名单,逗号分隔的正则表达式数组。正则表达式是根据 User-Agent 头部匹配的。
config.blacklist :黑名单,逗号分隔的正则表达式数组。正则表达式是根据 User-Agent 头部匹配的。
这个字段是用来匹配客户端身份的, 比如是浏览器还是模拟器, 还是python代码.
这个插件已经包含了一个基本的规则列表,这些规则将在每个请求上进行检查。你可以在GitHub上找到这个列表 https://github.com/Kong/kong/blob/master/kong/plugins/bot-detection/rules.lua.
---------------------
作者:数据架构师
来源:CSDN
原文:https://blog.csdn.net/luanpeng825485697/article/details/85326831
版权声明:本文为博主原创文章,转载请附上博文链接!