kong插件应用

插件概述

插件之于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
版权声明:本文为博主原创文章,转载请附上博文链接!

posted on 2019-02-15 19:27  duanxz  阅读(9789)  评论(0编辑  收藏  举报