前言
kong网关是大多数公司作为流量网关来使用,主要原因是它的性能很高,其次它的功能很完善也很容易扩展。但是因为它的熔断机制&发现机制和spring家族没有进行整合,所以,一般企业使用两层网关,一层流量网关,一层 spring gateway 业务网关。这里可能有人疑问,两层网关,导致网络请求多了一跳,会不会性能下降,这个大可不必担心,因为kong的性能极高,它的网络消耗时间能控制在10ms以内,因此,相对于他的功能,这点网络消耗还是值得的。
一 kong 初体验
我们简单了解kong是如何使用的,当然是基于ui操作的,后面再去使用官网提供的api去操作。
1.1 新建 upstream&target
首先需要新建上游,也就是我们反向代理的地址(我们的目标地址)。
- step1 创建上游

- step 创建管理target

- step 创建管理target
1.2 新建 server
1.3 新建 route
1.4 请求过程原理
二 Kong 概念解析
我们上面操作很多,完成了一个kong的反向代理,下面具体针对这些内容简单阐述。我们都知道kong其实就是nginx,关于nginx 的使用,我相信作为五年以上的开发人员都是OK的。
例如最金典的反向代理:
upstream passportUpstream {server localhost:8080 weight=100;}server {listen 80;location /hello {proxy_pass http://passportUpstream;}}
2.1 Upstream
Upstream 对象表示虚拟主机名,可用于通过多个服务(目标)对传入请求进行负载均衡。例如:service.v1.xyz 为 Service 对象命名的上游 Host 是 service.v1.xyz 对此服务的请求将代理到上游定义的目标。
2.2 Target
目标 IP地址/主机名,其端口表示后端服务的实例。每个上游都可以有多个 Target,并且可以动态添加 Target。由于上游维护Target 的更改历史记录,因此无法删除或者修改Target。要禁用目标,请发布一个新的 Targer weight=0,或者使用 DELETE 来完成相同的操作。
2.3 Service
服务实体是每个上游服务的抽象。服务的示例是数据转换微服务,计费API等。服务的主要属性是它的URL(其中,Kong 应该代理流量),其可以被设置为单个串或通过指定其 protocol, host,port 和path。服务与路由相关联(服务可以有许多与之关联的路由)。路由 Kong的入口点,并定义匹配客户端请求的规则。一旦匹配路由,Kong 就会将请求代理到其关联的服务。
2.4 Route
路由实体定义规则以匹配客户端的请求。每个 Route与一个 Service相关联,一个服务可能有多个与之关联的路由。与给定路由匹配的每个请求都将代理到其关联的 Service 上。可以配置的字段有:
- hosts
- paths
- methods
Service 和 Route 的组合(以及它们之间的关注点分离)提供了一种强大的路由机制,通过它可以在 Kong 中定义细粒度的入口点,从而使基础架构路由到不同上游服务。
2.5 Consumer
Consumer 对象表示服务的使用者或者用户。你可以依靠Kong作为主数据库存储,也可以将使用者列表与数据库映射,以保持Kong与现有的主数据存储之间的一致性。
2.6 Plugin
插件实体表示将在HTTP请求/响应生命周期 期间执行的插件配置。它是为在 Kong 后面运行的服务添加功能的,例如身份验证或速率限制。将插件配置添加到服务时,客户端向该服务发出的每个请求都将运行所述插件。如果某个特定消费者需要将插件调整为不同的值,你可以通过创建一个单独的插件实例,通过 service 和 consumer 字段指定服务和消费者。
三 鉴权认证
关于鉴权认证这块的插件,它的应用场景是针对一批接口服务,因此它不能针对接口直接使用该插件。它需要接口消费者结合使用。
3.1 basic auth
他是基于浏览器的安全认证,它主要为调用者用户分配用户名密码。
3.1.1 新建具有basic的消费者及认证方式
- 创建消费者

- 创建消费者组

- 选择basic认证
3.1.2 为接口指定消费者group及认证
- step1直接为接口添加一个basic插件(什么也不需要配置)

2.setp2 添加acl 控制插件
ps: 通过使用任意ACL组名将用户列入白名单或黑名单来限制对API的访问。这个插件需要一个已经在API上启用的身份验证插件。
3.1.3 请求测试接口
3.2 jwt auth
JSON Web Token (JWT),它是目前最流行的跨域身份验证解决方案。有点流言协议的问题,即去中心化。
3.2.1 jwt cookie session 的关系
这块知识点其实在我的技术专栏”网络原理”深入的探讨,这里我再次申明一下(因为很多网上博客,讲的稀里糊涂,包括我之前遇到的一些面试官,理解的也不清楚)。其实这块没有多难,这其实就是大学计算机网络课程的内容,属于“旧饭新炒”。
- cookie
设计者为啥需要设计出cookie ?
因为http协议是无状态的,所以我们需要维护用户的状态就需要引入一个小型文件(一小段文本信息key/value)存储用户的状态,这就是cookieId。
它的使用原理如下:
- session
设计者为啥设计出session?
因为cookie放在客户端不安全,被劫持,被篡改,不可控,最最主要的是很多客户端不支持cookie 或者禁用cookie,那怎么办,只能服务器受点累将用户状态信息存在服务器中。
它的原理与cookie 类似,只不过浏览器支持的方式有两种
- 结合cookie 将jessionId 保存到cookie中,
- 前面说了很多客户端不支持cookie 或者禁用cookie 因此我们可以将 jessionId 追加在url后面
- 为啥设计jwt
设计者为啥设计出jwt?
session缺陷:
- 因为session保存在服务端,当数据量大的时候服务器压力就越太大,
- 其次做服务集群的时候session共享也是个问题,所以提出了服务器去中心化思想,即客户端存用户状态,
cookie缺陷:
- 那我们的cookie 不也是存客户端的嘛,其实jwt 和cookie 类似,只不过是他的升级版(我自己理解的)。
- cookie 同源跨域问题,cookie数据结构简单,cookie安全问题,他只是个key value 不能加密,不能提供更多的信息,不能实现更多的场景交互
- 最最重要的是很多客户端不支持cookie机制
因此jwt应运而生,它主要就是:
- 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
- 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
- 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT
PS: 它的缺点也存在和cookie类似的问题,安全问题(极短有效期规避)
数据结构
Header
- Header通常由两部分组成:令牌的类型,即JWT。和常用的散列算法,如HMAC SHA256或RSA
{ "alg": "HS256", "typ": "JWT"}
- Header通常由两部分组成:令牌的类型,即JWT。和常用的散列算法,如HMAC SHA256或RSA
Payload
- 这里放声明内容,可以说就是存放沟通讯息的地方
{ "sub": "1234567890", "name": "John Doe", "admin": true}
- 这里放声明内容,可以说就是存放沟通讯息的地方
Signature
setp 创建 jwt消费者

- step 创建消费者组

3.step 创建jwt认证
2.2.3 为服务指定jwt认证
3.2.4请求测试接口
我这里是使用的第三方工具生成jwt,作为简单的测试,正式环境需要基础服务提供接口。
获取了jwt之后请求带参数即可
演示不带jwt,鉴权失败
演示带jwt,鉴权成功
3.3 Oauth2.0 auth
OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。……资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。-OAuth 的核心就是向第三方应用颁发令牌
3.3.1 授权码-authorization-code
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。这种方式是最常用的流程,安全性也最高,它适用于那些有后端的Web应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
step1 :A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
PS:注意这个是基于浏览器的安全认证,postman 报错404
它主要依据三类参数
- 是哪个商户应用请求的 client_id
- 该商户应用下哪个用户请求的 scope
- 需要什么权限 basic auth 用户名/密码
http://localhost:8085/oauth/authorize?client_id=hzm&response_type=code&redirect_uri=http://www.baidu.com&scope=all
上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)

setp2: 用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
点击确认跳转到百度
上面 URL 中,code参数就是授权码。
step3 : A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。http://localhost:8085/oauth/token?grant_type=authorization_code&code=s7N5Pn&redirect_uri=http://www.baidu.com&client_id=hzm&client_secret=hzm123
step4 : B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了3.3.2 隐藏式-implicit
有些 Web 应用是纯前端应用,没有后端。不适用上述方式,必须将令牌储存在前端。允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)
step1:A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
上面 URL 中,response_type参数为token,表示要求直接返回令牌。http://localhost:8085/oauth/authorize?client_id=hzm&redirect_uri=http://www.baidu.com&response_type=token&scope=read
step2: 用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
上面 URL 中,token参数就是令牌,A 网站因此直接在前端拿到令牌。http://www.baidu.com#token=ACCESS_TOKEN
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在”中间人攻击”的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
PS:唯独这个case我没有响应成功。3.3.3 密码式-password
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。
step1 : A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
上面 URL 中,grant_type参数是授权方式,这里的password表示”密码式”,username和password是 B 的用户名和密码。http://localhost:8085/oauth/token?grant_type=password&username=admin&password=123456&client_id=hzm
step2 :B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。curl --location --request POST 'http://localhost:8085/oauth/token?grant_type=password&username=admin&password=123456&client_id=hzm&client_secret=hzm123' \ --header 'Authorization: Basic aHptOmh6bTEyMzQ1Ng==' \ --header 'Cookie: JSESSIONID=DE50D796513E03CD2D16AB68015142DA'
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
3.3.4 凭证式-client credentials
最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
step1: A 应用在命令行向 B 发出请求。
http://localhost:8085/oauth/token?grant_type=client_credentials&client_id=hzm&client_secret=hzm123
上面 URL 中,grant_type参数等于client_credentials表示采用凭证式,client_id和client_secret用来让 B 确认 A 的身份。
step2: B 网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
3.4 Hmac Auth
接口参数签名,其目的就是保证请求参数的完整性,即不能接口参数被串改。
接口安全三要素:合法身份、参数防篡改、防重放攻击(请求唯一)。展开来讲:
- 合法身份:
- 我们为开发者分配 AccessKey & Secretkey,即给第三方app调用方,分配一个合法的身份。
- 参数防篡改:
- 我们拼接字符串:str=MD5取摘要(stringA(key1=value1&key2=value2…)+Secretkey+timestap)
- sha(str,secret)加密
- 防重放攻击:
- timestamp+nonce:我们为没一个请求生成唯一key,即防止重复的请求,此外,根据时间戳,定期清除无用的nonce(我们可以利用redis的过期策略实现)。
上面说的签名验证方式,市面上有很多实现,也可以自己手撸一个也无妨(我之前公司都是手撸的一套java+react),这里主要讨论的是kong的HAMC签名认证组件。
3.4.1 分配APP合法身份
首先为调用者APP分配 accessKey ,Secretkey。
setp1. 新建api消费者
- UI方式 创建:

API方式 创建:
curl -i -X POST http://localhost:8001/consumers/ \ -d "username=alice"setp2. 为第三方应用app分配 key、secre
UI方式 创建:

- API方式 创建:
curl -i -X POST http://localhost:8001/consumers/alice/hmac-auth \ -d "username=alice123" \ -d "secret=secret"3.4.2 新建HMAC(签名)认证插件
为我的应用服务上签名认证插件,一般情况下一个服务的接口都需要签名认证,除非是一些特殊的接口,比如鉴权接口
PS:新增的签名认证填写任何参数
- UI方式 创建:

API方式 创建:
curl -i -X POST http://localhost:8001/services/jk-member-svc2/plugins \ -d "name=hmac-auth" \3.4.3 实现签名算法
这里涉及到两个算法,md5获取摘要,然后sha1加密,这里有个面试题:为啥不用原生的字符串加密,两个原因1. MD5摘要算法,长度固定,2. 不是明文显示保证安全性。
MD5算法 ```java package cn.sephora;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils;
import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException;
/**
- @author hezhaoming
- @date 2020/11/23
*/
public class Md5Util {
public static String md5(String param) {
} } ```if (StringUtils.isBlank(param)) { throw new IllegalArgumentException("param can not be null"); } try { byte[] bytes = param.getBytes("utf-8"); final MessageDigest md = MessageDigest.getInstance("MD5"); md.reset(); md.update(bytes); final Base64 base64 = new Base64(); final byte[] enbytes = base64.encode(md.digest()); return new String(enbytes); } catch (final NoSuchAlgorithmException e) { throw new IllegalArgumentException("unknown algorithm MD5"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); }
- HmacSha1Util算法 ```java package cn.sephora;
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Formatter;
/**
- @author hezhaoming
@date 2020/11/23 */ public class HmacSha1Util { private static final String HMAC_SHA1_ALGORITHM = “HmacSHA1”;
private static String toHexString(byte[] bytes) {
Formatter formatter = new Formatter(); for (byte b : bytes) { formatter.format("%02x", b); } return formatter.toString();}
public static String signature(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM); Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); mac.init(signingKey); return toHexString(mac.doFinal(data.getBytes()));}
public static byte[] signatureReturnBytes(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM); Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); mac.init(signingKey); return mac.doFinal(data.getBytes());}
}
3. **HmacTest**
```java
package cn.sephora;
import lombok.extern.slf4j.Slf4j;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* @author hezhaoming
* @date 2020/11/23
*/
@Slf4j
public class HmacTest {
public static void main(String[] args) {
test1();
}
static void test1() {
String queryParam = "name=122&password=123";
String contentMD5 = Md5Util.md5(queryParam);
log.info("content-md5: {}", contentMD5);
Date d = new Date();
DateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String headerDate = format.format(d);
log.info("x-date: {}", headerDate);
StringBuilder stb = new StringBuilder();
String content = stb.append("x-date: ").append(headerDate).append("\n").append("content-md5: ").append(contentMD5).toString();
log.info("签名前内容: " + content);
String secret = "secret"; //用户yanyuylou的密钥
try {
String signature2 = new String(Base64.getEncoder().encode(HmacSha1Util.signatureReturnBytes(content, secret)), "US-ASCII");
log.info("显示指定编码[推荐]: {}", signature2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.4.4 测试DEMO
我们之前的应用是把签名参数放在url后面的,这里是放在头部里,其实都一样。插件这么定义的,我们就这么实现即可。
请求参数说明:
header 三个值 Content-md5 tkygbxlPERf+/tZghlMlXw== X-Date Mon, 23 Nov 2020 06:44:19 GMT Authorization Authorization: hmac username="alice123", algorithm="hmac-sha1", headers="x-date content-md5", signature="dojzuTSR3W/nhNJ+A9SY9+pJKTo="postman方式 示意图:

- API方式 测试demo:
curl -i -X GET http://localhost/api2/user/getById/1 \ -H "Content-md5: Qnwej0d5uZ9zgi34yYcoaw==" \ -H "x-date: Mon, 23 Nov 2020 03:45:05 GMT" \ -H 'Authorization: hmac username="alice123", algorithm="hmac-sha1", headers="x-date content-md5", signature="+Ro8wRWnVYdfvkZ8l4FFFPauBwE="'四 授权认证
4.1 acl
4.2 ip
五 熔断限流
5.1 速率限流
5.2 大小限流
5.3 熔断
六 协议转换



