Kubernetes (K8S)中APISIX高级使用

APISIX高级使用

Apache APISIX 是一个基于 OpenResty 和 Etcd 实现的动态、实时、高性能、可扩展的微服务 API 网关,目前已经是 Apache 顶级项目。提供了丰富的流量管理功能,如负载均衡、动态路由、动态 upstream、A/B 测试、金丝雀发布、限速、熔断、防御恶意攻击、认证、监控指标、服务可观测性、服务治理等。可以使用 APISIX 来处理传统的南北流量以及服务之间的东西向流量。

重定向-redirect

官方文档

现在当我们访问 http://whoami.boysec.cn/tls 或者 http://whoami.boysec.cn/tls/ 的时候都可以得到正常的结果,一般来说我们可能希望能够统一访问路径,比如访问 /tls 子路径的时候可以自动跳转到 /tls/ 以 Splash 结尾的路径上去。同样要实现该需求我们只需要使用一个名为 redirect 的插件即可,该插件是 URI 重定向插件,可配置的属性如下所示:

名称类型必选项默认值有效值描述
http_to_httpsbooleanfalse[true,false]当设置为 true 并且请求是 HTTP 时,它将被重定向具有相同 URI 和 301 状态码的 HTTPS,原 URI 的查询字符串也将包含在 Location 头中。
uristring要重定向到的 URI,可以包含 NGINX 变量。例如:/test/index.htm$uri/index.html${uri}/index.htmlhttps://example.com/foo/bar。如果你引入了一个不存在的变量,它不会报错,而是将其视为一个空变量。
regex_uriarray[string]将来自客户端的 URL 与正则表达式匹配并重定向。当匹配成功后使用模板替换发送重定向到客户端,如果未匹配成功会将客户端请求的 URI 转发至上游。和 regex_uri 不可以同时存在。例如:[“^/iresty/(.)/(.)/(.*)”,”/$1-$2-$3”] 第一个元素代表匹配来自客户端请求的 URI 正则表达式,第二个元素代表匹配成功后发送重定向到客户端的 URI 模板。
ret_codeinteger302[200, …]HTTP 响应码
encode_uribooleanfalse[true,false]当设置为 true 时,对返回的 Location Header 按照 RFC3986 的编码格式进行编码。
append_query_stringbooleanfalse[true,false]当设置为 true 时,将原始请求中的查询字符串添加到 Location Header。如果已配置 uriregex_uri 已经包含查询字符串,则请求中的查询字符串将附加一个&。如果你已经处理过查询字符串(例如,使用 NGINX 变量 $request_uri),请不要再使用该参数以避免重复。
  • http_to_httpsuriregex_uri 只能配置其中一个属性。
  • http_to_https、和 append_query_string 只能配置其中一个属性。
  • 当开启http_to_https时,重定向 URL 中的端口将按如下顺序选取一个值(按优先级从高到低排列)
    • 从配置文件(conf/config.yaml)中读取 plugin_attr.redirect.https_port
    • 如果 apisix.ssl 处于开启状态,读取 apisix.ssl.listen 并从中随机选一个 port
    • 使用 443 作为默认 https port

要实现我们的需求直接使用 regex_uri 这个属性即可,只需要去匹配 /tls 的请求,然后进行跳转即可,更新 ApisixRoute 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cat > ing.yml <<EOF
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: whoami-apisix
spec:
http:
- name: whoami-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/tls*'
backends:
- serviceName: whoami
servicePort: 80
plugins:
- name: redirect
enable: true
config:
regex_uri: ['^(/tls)$', '\$1/']
EOF

我们新启用了一个 redirect 插件,并配置 regex_uri: ["^(/tls)$", "$1/"],这样当访问 /tls 的时候会自动跳转到 /tls/ 路径下面去。

同样如果我们想要重定向到 https,只需要在该插件下面设置 config.http_to_https=true 即可:

1
2
3
4
5
# ... 其他部分省略
- name: redirect
enable: true
config:
http_to_https: true

证书-tls

通过使用上面的 redirect 插件配置 http_to_https 可以将请求重定向到 https 上去,但是我们现在并没有对我们的 ops.qikqiak.com 配置 https 证书,这里我们就需要使用 ApisixTls 对象来进行证书管理。

我们先使用 openssl 创建一个自签名的证书,当然你有正规 CA 机构购买的证书的话直接将证书下载下来使用即可:

1
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=whoami.boysec.cn/O=boysec"

然后通过 Secret 对象来引用上面创建的证书文件:

1
2
# 要注意证书文件名称必须是 tls.crt 和 tls.key
kubectl create secret tls who-tls --cert=tls.crt --key=tls.key

然后就可以创建一个 ApisixTls 资源对象,引用上面的 Secret 即可:

1
2
3
4
5
6
7
8
9
10
apiVersion: apisix.apache.org/v2
kind: ApisixTls
metadata:
name: who-tls
spec:
hosts:
- whoami.boysec.cn
secret:
name: who-tls
namespace: default

同时 APISIX TLS 还可以配置 spec.client,用于进行 mTLS 双向认证的配置。上面的资源对象创建完成后,即可访问 https 服务了(chrome 浏览器默认会限制不安全的证书,只需要在页面上输入 thisisunsafe 即可访问了):

URL跳转-proxy-rewrite

官方文档

假设现在有这样一个需求,当访问 http://whoami.boysec.cn/v1 时,流量调度至 whoami。当访问 http://whoami.boysec.cn/v2 时,流量调度至 nginx。这种需求是非常常见的。

创建俩个 ApiSixRoute规则,根据不同的访问路径代理至相对应的 service

vim appv1.yml

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: appv1
spec:
selector:
matchLabels:
app: appv1
template:
metadata:
labels:
use: test
app: appv1
spec:
containers:
- image: nginx:alpine
name: appv1
command: ["/bin/sh", "-c", "echo '你好, 这是(王先森)APP-v1服务中心'>/usr/share/nginx/html/index.html;nginx -g 'daemon off;'"]
ports:
- containerPort: 80
name: portv1
---
apiVersion: v1
kind: Service
metadata:
name: appv1
spec:
selector:
app: appv1
ports:
- name: http
port: 80
targetPort: portv1

vim who-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: whoami-apisix
spec:
http:
- name: whoami-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/v1*'
backends:
- serviceName: whoami
servicePort: 80
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/v1(/|$)(.*)', '/$2']

vim app-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: app-apisix
spec:
http:
- name: app-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/v2*'
backends:
- serviceName: appv1
servicePort: 80
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/v2(/|$)(.*)', '/$2']

这里我们启用一个 proxy-rewrite 插件,并且将所有 /v1v2 路径的请求都重写到了 / 路径下:

黑/白名单-ip-restriction

官方文档

ip-restriction 插件可以通过将 IP 地址列入白名单或黑名单来限制对服务或路由的访问。

支持对单个 IP 地址、多个 IP 地址和类似 10.1.1.0/24 的 CIDR(无类别域间路由)范围的限制。

参数名类型必选项默认值有效值描述
whitelistarray[string]加入白名单的 IP 地址或 CIDR 范围。
blacklistarray[string]加入黑名单的 IP 地址或 CIDR 范围。
messagestring“Your IP address is not allowed”[1, 1024]在未允许的 IP 访问的情况下返回的信息。

示例:

拒绝10.1.1.1这个IP访问v2版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: app-apisix
spec:
http:
- name: app-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/v2*'
backends:
- serviceName: appv1
servicePort: 80
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/v2(/|$)(.*)', '/$2']
- name: ip-restriction
enable: true
config:
blacklist: ['10.1.1.1']

在黑名单中IP访问则会出现403。

image-20231012130850212

用户认证

身份认证在日常生活当中是非常常见的一项功能,大家平时基本都会接触到。比如用支付宝消费时的人脸识别确认、公司上班下班时的指纹/面部打卡以及网站上进行账号密码登录操作等,其实都是身份认证的场景体现。

auth

如上图,Jack 通过账号密码请求服务端应用,服务端应用中需要有一个专门用做身份认证的模块来处理这部分的逻辑。请求处理完毕子后,如果使用 JWT Token 认证方式,服务器会反馈一个 Token 去标识这个用户为 Jack。如果登录过程中账号密码输入错误,就会导致身份认证失败。

但是每个应用服务模块去开发一个单独的身份认证模块,用来支持身份认证的一套流程处理,当服务量多了之后,就会发现这些模块的开发工作量都是非常巨大且重复的。这个时候,我们可以通过把这部分的开发逻辑放置到 Apache APISIX 的网关层来实现统一,减少开发量。

apisix auth

如上图所示,用户或应用方直接去请求 Apache APISIX,然后 Apache APISIX 通过识别并认证通过后,会将鉴别的身份信息传递到上游应用服务,之后上游应用服务就可以从请求头中读到这部分信息,然后进行后续的逻辑处理。

Apache APISIX 作为一个 API 网关,目前已开启与各种插件功能的适配合作,插件库也比较丰富。目前已经可与大量身份认证相关的插件进行搭配处理,如下图所示。

API 网关认证插件

基础认证插件比如 Key-AuthBasic-Auth,他们是通过账号密码的方式进行认证。复杂一些的认证插件如 Hmac-AuthJWT-Auth,如 Hmac-Auth 通过对请求信息做一些加密,生成一个签名,当 API 调用方将这个签名携带到 Apache APISIX,Apache APISIX 会以相同的算法计算签名,只有当签名方和应用调用方认证相同时才予以通过。其他则是一些通用认证协议和联合第三方组件进行合作的认证协议,例如 OpenID-Connect 身份认证机制,以及 LDAP 认证等。

Apache APISIX 还可以针对每一个 Consumer (即调用方应用)去做不同级别的插件配置。如下图所示,我们创建了两个消费者 Consumer A、Consumer B,我们将 Consumer A 应用到应用 1,则后续应用 1 的访问将会开启 Consumer A 的这部分插件,例如 IP 黑白名单,限制并发数量等。将 Consumer B 应用到应用 2 ,由于开启了 http-log 插件,则应用 2 的访问日志将会通过 HTTP 的方式发送到日志系统进行收集。

配置灵活

总体说来 APISIX 的认证系统功能非常强大,我们非常有必要掌握。

basic-auth

官方文档

首先我们来了解下最简单的基本认证在 APISIX 中是如何使用的。basic-auth 是一个认证插件,它需要与 Consumer 一起配合才能工作。添加 Basic Auth 到一个 Service 或 Route,然后 Consumer 将其用户名和密码添加到请求头中以验证其请求。

首先我们需要在 APISIX Consumer 消费者中增加 basic auth 认证配置,为其指定用户名和密码,我们这里在 APISIX Ingress 中,可以通过 ApisixConsumer 资源对象进行配置,比如这里我们为前面的 whoami实例应用添加一个基本认证,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
cat > basic-auth.yaml <<EOF
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
name: whoamibauth
spec:
authParameter:
basicAuth:
value:
username: admin
password: admin321
EOF

ApisixConsumer 资源对象中只需要配置 authParameter 认证参数即可,目前只支持 BasicAuthKeyAuth 两种认证类型,在 basicAuth 下面可以通过 value 可直接去配置相关的 username 和 password,也可以直接使用 Secret 资源对象进行配置,比起明文配置会更安全一些。

然后在 ApisixRoute 中添加 authentication,将其开启并指定认证类型即可,就可以实现使用 Consumer 去完成相关配置认证,如下所示:

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
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: whoami-apisix
spec:
http:
- name: whoami-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/tls*'
- '/v1*'
backends:
- serviceName: whoami
servicePort: 80
plugins:
- name: redirect
enable: true
config:
regex_uri: ['^(/tls)$', '$1/']
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/v1(/|$)(.*)', '/$2']
authentication: # 开启 basic auth 认证
enable: true
type: basicAuth

直接更新上面的资源即可开启 basic auth 认证了,在 Dashboard 上也可以看到创建了一个 Consumer:

然后我们可以进行如下的测试来进行验证:

jwt-auth

官方文档

在平时的应用中可能使用 jwt 认证的场景是最多的,同样在 APISIX 中也有提供 jwt-auth 的插件,它同样需要与 Consumer 一起配合才能工作,我们只需要添加 JWT Auth 到一个 Service 或 Route,然后 Consumer 将其密钥添加到查询字符串参数、请求头或 cookie 中以验证其请求即可。

通过 ApisixConsumer 资源对象进行配置

1
2
3
4
5
6
7
8
9
10
11
12
cat > jwt-auth.yaml <<EOF
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
name: jwtbauth
spec:
authParameter:
jwtAuth:
value:
key: "boysec"
secret: "my-secret-key"
EOF

直接应用上面的资源即可开启 jwt auth 认证了,在 Dashboard 上也可以看到创建了一个 Consumer:

在nginx开启jwt认证:

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
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: app-apisix
spec:
http:
- name: app-root
match:
hosts:
- whoami.boysec.cn
paths:
- '/v2*'
backends:
- serviceName: appv1
servicePort: 80
plugins:
- name: jwt-auth
enable: true
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/v2(/|$)(.*)', '/$2']
- name: ip-restriction
enable: true
config:
blacklist: ['10.1.1.1']

需要注意的是 authentication 属性也不支持 jwt-auth,所以这里我们通过 plugins 进行启用,重新更新上面的对象后我们同样来测试验证下:

1
2
3
4
5
6
7
8
9
curl -i http://whoami.boysec.cn/v2/
HTTP/1.1 401 Unauthorized
Server: nginx/1.18.0
Date: Sun, 27 Aug 2023 19:06:24 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"message":"Missing JWT token in request"}

要正常访问我们的服务就需要先进行登录获取 jwt-auth 的 token,通过 APISIX 的 apisix/plugin/jwt/sign 可以获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 你需要为签发 token 的 API 配置一个 Route,该路由将使用 public-api 插件。

curl http://127.0.0.1:9180/apisix/admin/routes/default_whoamibauth \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/apisix/plugin/jwt/sign",
"plugins": {
"public-api": {}
}
}'

curl -i http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=boysec
HTTP/1.1 200 OK
Date: Fri, 13 Oct 2023 06:33:41 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.5.0

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJib3lzZWMiLCJleHAiOjE2OTcyNjUyMjF9.ts4JsOWqHJxQkDR9FO_rviLssKfP8_ZbAKrq0OyiGe8

如果觉得进入容器执行这些麻烦,也可以通过资源配置清单来管理,这样就可以实现在页面中获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat > public-api.yml <<EOF
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: public-api
namespace: apisix
spec:
http:
- name: public-api
match:
hosts:
- api.boysec.cn
paths:
- /apisix/plugin/jwt/sign
backends:
- serviceName: apisix-admin
servicePort: 9180
plugins:
- name: public-api
enable: true
EOF

通过浏览器输入:http://api.boysec.cn/apisix/plugin/jwt/sign?key=boysec 获取jwt的Token。

要注意上面我们在获取 token 的时候需要传递创建消费者的标识 key,因为可能有多个不同的 Consumer 消费者,然后我们将上面获得的 token 放入到 Header 头中进行访问:

可以看到可以正常访问。同样也可以放到请求参数或者Cookie中验证:

1
2
3
4
# GET
curl -i http://whoami.boysec.cn/v2/?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJib3lzZWMiLCJleHAiOjE2OTcyNjUyMjF9.ts4JsOWqHJxQkDR9FO_rviLssKfP8_ZbAKrq0OyiGe8
# Cookie
curl -i http://whoami.boysec.cn/v2 --cookie jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJib3lzZWMiLCJleHAiOjE2OTcyNjUyMjF9.ts4JsOWqHJxQkDR9FO_rviLssKfP8_ZbAKrq0OyiGe8