前言

kong网关是大多数公司作为流量网关来使用,主要原因是它的性能很高,其次它的功能很完善也很容易扩展。但是因为它的熔断机制&发现机制和spring家族没有进行整合,所以,一般企业使用两层网关,一层流量网关,一层 spring gateway 业务网关。这里可能有人疑问,两层网关,导致网络请求多了一跳,会不会性能下降,这个大可不必担心,因为kong的性能极高,它的网络消耗时间能控制在10ms以内,因此,相对于他的功能,这点网络消耗还是值得的。

一 kong 初体验

我们简单了解kong是如何使用的,当然是基于ui操作的,后面再去使用官网提供的api去操作。

1.1 新建 upstream&target

首先需要新建上游,也就是我们反向代理的地址(我们的目标地址)。

  1. step1 创建上游

image.png

  1. step 创建管理target

image.png

  1. step 创建管理target

image.png

1.2 新建 server

image.png

image.png

1.3 新建 route

新增路由
image.png
image.png

1.4 请求过程原理

image.png

二 Kong 概念解析

我们上面操作很多,完成了一个kong的反向代理,下面具体针对这些内容简单阐述。我们都知道kong其实就是nginx,关于nginx 的使用,我相信作为五年以上的开发人员都是OK的。
例如最金典的反向代理:

  1. upstream passportUpstream {
  2. server localhost:8080 weight=100;
  3. }
  4. server {
  5. listen 80;
  6. location /hello {
  7. proxy_pass http://passportUpstream;
  8. }
  9. }

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的消费者及认证方式

  1. 创建消费者

image.png

  1. 创建消费者组

image.png

  1. 选择basic认证

image.png

3.1.2 为接口指定消费者group及认证

  1. step1直接为接口添加一个basic插件(什么也不需要配置)

image.png
2.setp2 添加acl 控制插件
ps: 通过使用任意ACL组名将用户列入白名单或黑名单来限制对API的访问。这个插件需要一个已经在API上启用的身份验证插件。
image.png

3.1.3 请求测试接口

用户名密码 basic1000 密码:1234
image.png
认证成功
image.png

3.2 jwt auth

JSON Web Token (JWT),它是目前最流行的跨域身份验证解决方案。有点流言协议的问题,即去中心化。

3.2.1 jwt cookie session 的关系

这块知识点其实在我的技术专栏”网络原理”深入的探讨,这里我再次申明一下(因为很多网上博客,讲的稀里糊涂,包括我之前遇到的一些面试官,理解的也不清楚)。其实这块没有多难,这其实就是大学计算机网络课程的内容,属于“旧饭新炒”。

  1. cookie

设计者为啥需要设计出cookie ?
因为http协议是无状态的,所以我们需要维护用户的状态就需要引入一个小型文件(一小段文本信息key/value)存储用户的状态,这就是cookieId。
它的使用原理如下:
cookie原理.png

  1. session

设计者为啥设计出session?
因为cookie放在客户端不安全,被劫持,被篡改,不可控,最最主要的是很多客户端不支持cookie 或者禁用cookie,那怎么办,只能服务器受点累将用户状态信息存在服务器中。
它的原理与cookie 类似,只不过浏览器支持的方式有两种

  1. 结合cookie 将jessionId 保存到cookie中,
  2. 前面说了很多客户端不支持cookie 或者禁用cookie 因此我们可以将 jessionId 追加在url后面


  1. 为啥设计jwt

设计者为啥设计出jwt
session缺陷:

  1. 因为session保存在服务端,当数据量大的时候服务器压力就越太大,
  2. 其次做服务集群的时候session共享也是个问题,所以提出了服务器去中心化思想,即客户端存用户状态,

cookie缺陷:

  1. 那我们的cookie 不也是存客户端的嘛,其实jwt 和cookie 类似,只不过是他的升级版(我自己理解的)。
  2. cookie 同源跨域问题,cookie数据结构简单,cookie安全问题,他只是个key value 不能加密,不能提供更多的信息,不能实现更多的场景交互
  3. 最最重要的是很多客户端不支持cookie机制

因此jwt应运而生,它主要就是:

  1. 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
  2. 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
  3. 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT

PS: 它的缺点也存在和cookie类似的问题,安全问题(极短有效期规避)

数据结构

  1. Header

    1. Header通常由两部分组成:令牌的类型,即JWT。和常用的散列算法,如HMAC SHA256或RSA
      {  "alg": "HS256",  "typ": "JWT"}
      
  2. Payload

    1. 这里放声明内容,可以说就是存放沟通讯息的地方
      {  "sub": "1234567890",  "name": "John Doe",  "admin": true}
      
  3. Signature

    1. 第三部分signature用来验证发送请求者身份,由前两部分加密形成
      HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret)
      

      3.2.2 新建具有jwt的消费者及认证方式

  4. setp 创建 jwt消费者

image.png

  1. step 创建消费者组

image.png
3.step 创建jwt认证

image.png

2.2.3 为服务指定jwt认证

image.png

3.2.4请求测试接口

我这里是使用的第三方工具生成jwt,作为简单的测试,正式环境需要基础服务提供接口。
image.png
获取了jwt之后请求带参数即可
演示不带jwt,鉴权失败
image.png
演示带jwt,鉴权成功
image.png

3.3 Oauth2.0 auth

OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。……资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。-OAuth 的核心就是向第三方应用颁发令牌

3.3.1 授权码-authorization-code

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。这种方式是最常用的流程,安全性也最高,它适用于那些有后端的Web应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
step1 :A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
PS:注意这个是基于浏览器的安全认证,postman 报错404
它主要依据三类参数

  1. 是哪个商户应用请求的 client_id
  2. 该商户应用下哪个用户请求的 scope
  3. 需要什么权限 basic auth 用户名/密码
    http://localhost:8085/oauth/authorize?client_id=hzm&response_type=code&redirect_uri=http://www.baidu.com&scope=all
    
    image.png

    image.png

    上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)
    image.png
    setp2: 用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
    image.png
    点击确认跳转到百度
    image.png
    上面 URL 中,code参数就是授权码。
    image.png
    step3 : A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
    http://localhost:8085/oauth/token?grant_type=authorization_code&code=s7N5Pn&redirect_uri=http://www.baidu.com&client_id=hzm&client_secret=hzm123
    
    上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。
    image.png
    step4 : B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
    image.png
    上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了

    3.3.2 隐藏式-implicit

    有些 Web 应用是纯前端应用,没有后端。不适用上述方式,必须将令牌储存在前端。允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)
    step1:A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
    http://localhost:8085/oauth/authorize?client_id=hzm&redirect_uri=http://www.baidu.com&response_type=token&scope=read
    
    上面 URL 中,response_type参数为token,表示要求直接返回令牌。
    step2: 用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
    http://www.baidu.com#token=ACCESS_TOKEN
    
    上面 URL 中,token参数就是令牌,A 网站因此直接在前端拿到令牌。
    注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在”中间人攻击”的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
    image.png
    这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
    PS:唯独这个case我没有响应成功。

    3.3.3 密码式-password

    如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。
    step1 : A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
    http://localhost:8085/oauth/token?grant_type=password&username=admin&password=123456&client_id=hzm
    
    上面 URL 中,grant_type参数是授权方式,这里的password表示”密码式”,username和password是 B 的用户名和密码。
    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'
    
    step2 :B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
    这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

image.png

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 网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
image.png

3.4 Hmac Auth

接口参数签名,其目的就是保证请求参数的完整性,即不能接口参数被串改。

接口安全三要素:合法身份、参数防篡改、防重放攻击(请求唯一)。展开来讲:

  1. 合法身份:
    1. 我们为开发者分配 AccessKey & Secretkey,即给第三方app调用方,分配一个合法的身份。
  2. 参数防篡改:
    1. 我们拼接字符串:str=MD5取摘要(stringA(key1=value1&key2=value2…)+Secretkey+timestap)
    2. sha(str,secret)加密
  3. 防重放攻击:
    1. timestamp+nonce:我们为没一个请求生成唯一key,即防止重复的请求,此外,根据时间戳,定期清除无用的nonce(我们可以利用redis的过期策略实现)。

上面说的签名验证方式,市面上有很多实现,也可以自己手撸一个也无妨(我之前公司都是手撸的一套java+react),这里主要讨论的是kong的HAMC签名认证组件。

3.4.1 分配APP合法身份

首先为调用者APP分配 accessKey ,Secretkey。
setp1. 新建api消费者

  1. UI方式 创建:

image.png

  1. API方式 创建:

    curl -i -X POST http://localhost:8001/consumers/ \
    -d "username=alice"
    

    setp2. 为第三方应用app分配 key、secre

  2. UI方式 创建

image.png

  1. API方式 创建:
    curl -i -X POST http://localhost:8001/consumers/alice/hmac-auth \
    -d "username=alice123" \
    -d "secret=secret"
    

    3.4.2 新建HMAC(签名)认证插件

    为我的应用服务上签名认证插件,一般情况下一个服务的接口都需要签名认证,除非是一些特殊的接口,比如鉴权接口

PS:新增的签名认证填写任何参数

  1. UI方式 创建:

image.png

  1. 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. 不是明文显示保证安全性。

  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);
     }
    
    } } ```
  1. 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后面的,这里是放在头部里,其实都一样。插件这么定义的,我们就这么实现即可。

  1. 请求参数说明:

    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="
    
  2. postman方式 示意图:

image.png

  1. 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 熔断

    六 协议转换

七 链路监控

普罗米修斯
链路监控

八 日志监控

九 灰度发布