Apisix(六)微服务安全配置和全局异常处理

1,373 阅读10分钟

一、Https配置

  1. 通过接口上传证书到apisix
curl http://192.168.101.23:30218/apisix/admin/ssls/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
     "cert": "'"$(cat /Users/quf/sps.axt.com.crt)"'",
     "key": "'"$(cat /Users/quf/sps.axt.com.key)"'",
     "snis": ["*.sps.axt.com","sps.axt.com"]
}'
  1. apisix开启ssl配置,重启apisix让配置生效
      ssl:
        enable: true #开启https ssl
        listen:
          - port: 9443
            enable_http2: true
        ssl_protocols: "TLSv1.2 TLSv1.3"
        ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA"
  1. 配置路由
curl http://192.168.101.23:30218/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/ams/amcenter",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "192.168.101.25:31102": 1
        }
    }
}'
  1. 通过临时配置DNS本地验证
  ~ curl --resolve 'sps.axt.com:30443:192.168.101.23' https://sps.axt.com:30443/ams/amcenter -k -vv
* Added sps.axt.com:30443:192.168.101.23 to DNS cache
* Hostname sps.axt.com was found in DNS cache
*   Trying 192.168.101.23:30443...
* Connected to sps.axt.com (192.168.101.23) port 30443 (#0)
* ALPN: offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* ALPN: server accepted h2
* Server certificate:
*  subject: C=CN; ST=\U4E0A\U6D77\U5E02; O=\U4E0A\U6D77\U5B89\U7545\U7F51\U7EDC\U79D1\U6280\U80A1\U4EFD\U6709\U9650\U516C\U53F8; CN=*.sps.axt.com
*  start date: Nov 16 00:00:00 2023 GMT
*  expire date: Dec 14 23:59:59 2024 GMT
*  issuer: C=CN; O=TrustAsia Technologies, Inc.; CN=TrustAsia RSA OV TLS CA G3
*  SSL certificate verify ok.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /ams/amcenter]
* h2h3 [:scheme: https]
* h2h3 [:authority: sps.axt.com:30443]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7fb88000bc00)
> GET /ams/amcenter HTTP/2
> Host: sps.axt.com:30443
> user-agent: curl/7.88.1
> accept: */*
>
< HTTP/2 401
< content-type: application/json
< set-cookie: client-uuid=6e85a5fa-6441-49a8-9e40-9c7bb28d91ca; Max-Age=315360000; Expires=Fri, 28-Apr-2034 02:49:08 GMT; Path=/; HttpOnly
< date: Tue, 30 Apr 2024 02:49:08 GMT
< server: APISIX/3.8.0
< vary: Access-Control-Request-Headers
< x-content-type-options: nosniff
< content-security-policy: default-src https: data: unsafe-inline unsafe-eval
< x-xss-protection: 1 ; mode=block
< referrer-policy: origin
< cache-control: no-cache, no-store, max-age=0, must-revalidate
< strict-transport-security: max-age=172800; includeSubDomains
< expires: 0
< pragma: no-cache
< x-frame-options: SAMEORIGIN
<
* Connection #0 to host sps.axt.com left intact
{"timestamp":"2024-04-30T10:49:08.557+08:00","status":401,"path":"/ams/amcenter","method":"GET","ms":4,"error":"login.required","message":"用户未登录"}%
  ~

二、安全配置

Response Header安全配置

使用springsecurity的时候,springsecurity会添加很多安全相关的header到最终返回给用户的response中。在apisix中可以通过response_rewriter插件来将这些安全header写入到response中。

        "response-rewrite":{
                "headers": {
                  "set": {
                    "X-Content-Type-Options": "nosniff",
                    "X-Frame-Options": "SAMEORIGIN",
                    "X-XSS-Protection": "1 ; mode=block",
                    "Content-Security-Policy": "default-src https: data: 'unsafe-inline' 'unsafe-eval'",
                    "Referrer-Policy": "origin",
                    "Vary": "Origin",
                    "Vary": "Access-Control-Request-Method",
                    "Vary": "Access-Control-Request-Headers",
                    "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
                    "Pragma": "no-cache",
                    "Expires": 0,
                    "Strict-Transport-Security": "max-age=172800; includeSubDomains"
                  }
            }
        }             

跨域配置

这里特别说明下不要直接使用curl命令在终端窗口来验证跨域,因为跨域是一种浏览器的保护机制,通过curl命令外加简单的-H 'origin:xxx'无法触发浏览器的跨域设置。

在springsecurity中我们可以通过代码来配置跨域的参数,在apisix中通过自带的cors插件来实现对于跨域的控制。


        "cors": {
            "allow_credential": true,
            "allow_headers": "Content-Type
            "allow_methods": "GET,POST,PUT,DELETE,OPTIONS",
            "allow_origins": "https://sps.axt.com:30443,http://127.0.0.1:8000",
            "disable": false,
            "expose_headers": "",
            "max_age": 25900
        }

三、安全配置示例

添加route url,添加对应的cors,response_rewrite插件配置

curl http://192.168.101.23:30218/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/ams/amcenter",
    "name":"1",
    "plugins": {
        "ext-plugin-pre-req": {
          "conf": [
            { "name": "say", "value":"{"body":"hello"}"}
          ]
        },
        "cors": {
            "allow_credential": true,
            "allow_headers": "Content-Type
            "allow_methods": "GET,POST,PUT,DELETE,OPTIONS",
            "allow_origins": "https://sps.axt.com:30443,http://127.0.0.1:8000",
            "disable": false,
            "expose_headers": "",
            "max_age": 25900
        },
        "response-rewrite":{
                "headers": {
                  "set": {
                    "X-Content-Type-Options": "nosniff",
                    "X-Frame-Options": "SAMEORIGIN",
                    "X-XSS-Protection": "1 ; mode=block",
                    "Content-Security-Policy": "default-src https: data: 'unsafe-inline' 'unsafe-eval'",
                    "Referrer-Policy": "origin",
                    "Vary": "Origin",
                    "Vary": "Access-Control-Request-Method",
                    "Vary": "Access-Control-Request-Headers",
                    "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
                    "Pragma": "no-cache",
                    "Expires": 0,
                    "Strict-Transport-Security": "max-age=172800; includeSubDomains"
                  }
            }
        }             
    },        
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "192.168.101.25:31102": 1
        }
    }
}'

创建一个html页面来调用route

vi cors.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Fetch JSON Example</title>
</head>
<body>

<div id="output"></div>

<script>
    // 使用Fetch API获取数据
    fetch('http://192.168.101.23:30290/ams/amcenter')
        .then(response => {
            // 检查响应是否成功
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            // 解析JSON格式的响应体
            return response.json();
        })
        .then(data => {
            // 在控制台打印数据,便于调试
            console.log(data);

            // 将数据插入到HTML中的指定元素
            document.getElementById('output').innerHTML = JSON.stringify(data, null, 2); // 使用JSON.stringify美化输出
        })
        .catch(error => {
            // 如果发生错误,则在控制台打印错误信息
            console.error('There has been a problem with your fetch operation:', error);
            // 可以选择在这里向用户显示错误信息
            document.getElementById('output').innerText = 'Error fetching data.';
        });
</script>

</body>
</html>

创建http server

mkdir cors
mv cors.html cors/
cd cors

python3 -m http.server
➜  cors python3 -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

浏览器访问http server,通过httpserver访问 cors.html验证跨域和安全配置

遇到跨域请求失败 p11-5-0.png

访问成功返回结果

p11-5-1.png

p11-5-2.png

四、全局异常配置

通过response-rewrite插件来处理全局异常,通过定义全局规则作用全部请求,在插件中通过_meta中的filter对于符合条件的请求进行统一处理。

以下示例根据upstream请求返回状态码进行处理,当状态码在4xx范围内,对于response body进行统一调整,并且添加自定义header到response header中。

创建全局规则处理全局异常

curl http://192.168.101.23:30218/apisix/admin/global_rules/gr -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "plugins": {
        "response-rewrite": {
            "body": "{"message":"backend service error by global rule error"}",
            "headers": {
                "set": {
                    "X-Customer-Resp-Err": "global_rule"
                }
            },
            "_meta":{ 
              "filter": [
                ["status", ">", 399 ],
                ["status", "!", ">", 500]
              ]
            }
        }    
    }
}'

正常请求处理验证

#正常访问正常返回
  apisix-helm-chart git:(0da1bd0)  curl -v http://192.168.101.23:30290/ams/amcenter
*   Trying 192.168.101.23:30290...
* Connected to 192.168.101.23 (192.168.101.23) port 30290 (#0)
> GET /ams/amcenter HTTP/1.1
> Host: 192.168.101.23:30290
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: client-uuid=a75b3f1b-81a1-4e46-99a3-6808495a773d; Max-Age=315360000; Expires=Sat, 06-May-2034 07:33:19 GMT; Path=/; HttpOnly
< Date: Wed, 08 May 2024 07:33:20 GMT
< Server: APISIX/3.8.0
< Vary: Access-Control-Request-Headers
< Pragma: no-cache
< Referrer-Policy: origin
< Strict-Transport-Security: max-age=172800; includeSubDomains
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< X-XSS-Protection: 1 ; mode=block
< Content-Security-Policy: default-src https: data: 'unsafe-inline' 'unsafe-eval'
< Expires: 0
<
* Connection #0 to host 192.168.101.23 left intact
{"timestamp":"2024-04-12T18:11:02.71+08:00","status":200,"data":{"id":"ba7d68","name":"troy","accountId":100980,"email":"troy@qq.com","passwordReset":false,"passwordUpdateAt":"2023-09-11T09:51:14.000+00:00","description":"用户","createdAt":"2018-07-10T20:19:18.000+00:00","createdBy":"ba7ee","accountName":"xx司","companyInfo":{}},"path":"/ams/amcenter","method":"GET","ms":50,"message":"个人中心成功"}%

4xx请求处理验证

#异常情况触发全局规则
apisix-helm-chart git:(0da1bd0)  curl -v http://192.168.101.23:30290/ams/amcenter/111
*   Trying 192.168.101.23:30290...
* Connected to 192.168.101.23 (192.168.101.23) port 30290 (#0)
> GET /ams/amcenter111 HTTP/1.1
> Host: 192.168.101.23:30290
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Wed, 08 May 2024 07:33:26 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: APISIX/3.8.0
< X-Customer-Resp-Err: global_rule
<
* Connection #0 to host 192.168.101.23 left intact
{"message":"backend service error by global rule error"}%

5xx请求处理验证

人为创建一个5xx返回状态的请求

Service Temporarily Unavailableroot@dev-nginx:~# cat 5xx.conf
worker_processes auto;


# 工作模式和连接数设置
events {
    worker_connections 1024;
}

http {
    server {
        listen 28881;
        server_name localhost;

        # 其他location配置...

        # 人为设置返回503状态码的路由
        location /route-to-error {
            default_type application/json;
            return 503 "Service Temporarily Unavailable";
        }

    }
}
root@dev-nginx:~#
root@dev-nginx:~# nginx -c /root/5xx.conf
root@dev-nginx:~# curl -v http://192.168.101.4:28881/route-to-error
*   Trying 192.168.101.4...
* TCP_NODELAY set
* Connected to 192.168.101.4 (192.168.101.4) port 28881 (#0)
> GET /route-to-error HTTP/1.1
> Host: 192.168.101.4:28881
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 503 Service Temporarily Unavailable
< Server: nginx/1.14.0 (Ubuntu)
< Date: Wed, 08 May 2024 08:16:54 GMT
< Content-Type: application/json
< Content-Length: 31
< Connection: keep-alive
<
* Connection #0 to host 192.168.101.4 left intact

apisix添加5xx请求路由并且调用,发现5xx请求不在规则处理范围内。

  t curl http://192.168.101.23:30218/apisix/admin/routes/3 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/route-to-error",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "192.168.101.4:28881": 1
        }
    }
}'
{"key":"/apisix/routes/3","value":{"create_time":1715156286,"update_time":1715156289,"status":1,"uri":"/route-to-error","priority":0,"upstream":{"hash_on":"vars","pass_host":"pass","nodes":{"192.168.101.4:28881":1},"type":"roundrobin","scheme":"http"},"id":"3"}}
  t curl -v http://192.168.101.23:30290/route-to-error
*   Trying 192.168.101.23:30290...
* Connected to 192.168.101.23 (192.168.101.23) port 30290 (#0)
> GET /route-to-error HTTP/1.1
> Host: 192.168.101.23:30290
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 503 Service Temporarily Unavailable
< Content-Type: application/json
< Content-Length: 31
< Connection: keep-alive
< Date: Wed, 08 May 2024 08:18:28 GMT
< Server: APISIX/3.8.0
< X-APISIX-Upstream-Status: 503
<
* Connection #0 to host 192.168.101.23 left intact
Service Temporarily Unavailable%

在发现4xx的全局规则不能处理5xx error后,我们再创建一个专门用来处理5xx的全局规则,来对于5xx错误进行统一处理。

通过示例可以看到当添加了对于5xx error处理的规则,5xx的error也按照我们期待的结果进行返回了。

  t curl http://192.168.101.23:30218/apisix/admin/global_rules/gr2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "plugins": {
        "response-rewrite": {
            "body": "{"message":"server error api failed in server side"}",
            "headers": {
                "set": {
                    "X-Customer-Resp-Err": "5xx-global-rule"
                }
            },
            "_meta":{
              "filter": [
                ["status", ">", 500]
              ]
            }
        }
    }
}'
HTTP/1.1 201 Created
Date: Wed, 08 May 2024 08:28:35 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.8.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Access-Control-Max-Age: 3600
X-API-VERSION: v3

{"key":"/apisix/global_rules/gr2","value":{"create_time":1715156915,"id":"gr2","update_time":1715156915,"plugins":{"response-rewrite":{"body":"{"message":"server error api failed in server side"}","body_base64":false,"_meta":{"filter":[["status",">",500]]},"headers":{"set":{"X-Customer-Resp-Err":"5xx-global-rule"}}}}}}
  t curl -v http://192.168.101.23:30290/route-to-error
*   Trying 192.168.101.23:30290...
* Connected to 192.168.101.23 (192.168.101.23) port 30290 (#0)
> GET /route-to-error HTTP/1.1
> Host: 192.168.101.23:30290
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 503 Service Temporarily Unavailable
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Date: Wed, 08 May 2024 08:28:41 GMT
< Server: APISIX/3.8.0
< X-APISIX-Upstream-Status: 503
< X-Customer-Resp-Err: 5xx-global-rule
<
* Connection #0 to host 192.168.101.23 left intact
{"message":"server error api failed in server side"}%

五、总结

在之前了解了apisix的基础功能,本文中介绍了一般微服务开发和部署中,对于网关服务来讲安全以及一些异常的处理功能。结合apisix的路由能力,我们完全可以做到使用apisix来作为微服务网关。

要注意的是在实际投入prod使用之前,必须确保这里提到的内容最好能够完全掌握,并且阅读官方关于安全层面的相关内容,作为技术了解我们可以通过快速开始以及helloworld之类的功能来了解一个技术,但是在实际prod使用我们需要考虑到所有安全边界的处理。

尤其是像apisix这种网关组件作为服务的入口安全等问题是一定要重点注意和考虑的。