发布于 

基于 Nginx 的异构后端统一鉴权服务🛡️

JWT authorization with NGINX Ingress Controller

背景知识

JWT 验证

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境中安全地传递信息。JWT 的设计目的是为了在各方之间安全地传输声明(claim),这些声明可以包含关于用户身份(ID)、权限或其他元数据的信息。

JWT 的结构

一个 JWT 通常由三部分组成,分别用点(.)分隔:

  • 头部(Header),包含令牌的类型(通常是 “JWT”)和所使用的签名算法(如 SHA256、RSA、ECDSA)
  • 负载(Payload),包含实际的数据(声明),这些声明可以是公开声明、私有声明或注册声明。
    • 公开声明:可以包含在 JWT 中的标准化声明,例如 iss(发行者)、exp(过期时间)、sub(主题)等。
    • 私有声明:用户自定义的数据,不是标准化的声明,需要双方协商一致。
    • 注册声明:一些预定义的声明,用于在 JWT 中传递用户相关的基本信息。
    • 负载部分的内容是 Base64Url 编码的 JSON 对象。
  • 签名(Signature),由头部和负载部分的数据以及一个密钥经过特定算法(如 HMAC SHA256、RSA 等)生成,用于验证 JWT 的完整性。

无状态特性

JWT 包含了所有必要的用户身份和权限信息(如用户 ID、权限等),并通过签名保证数据的完整性。当客户端每次发送请求时,会将 JWT 令牌包含在请求头中,服务器通过验证该令牌来进行身份验证和授权,而不需要维护和查询服务器端的会话数据。服务器只需检查 JWT 的签名和有效性,而不需要存储任何会话数据。

安全性考虑

JWT(JSON Web Token)的数据通常 不被加密,而是 签名 来保证数据的完整性和真实性,签名的目的是确保 JWT 的数据没有被篡改。接收方使用相同的签名算法和密钥来验证签名是否匹配。如果不匹配,表明数据可能被篡改。

JWT 的头部(Header)和负载(Payload)部分一般 不加密,只进行 Base64Url 编码。这种编码只是将数据转换为 URL 安全的字符串形式,并不提供任何安全性,任何人都可以解码 JWT,查看其中的内容。这意味着敏感信息不应该直接存储在 JWT 的负载中,除非数据经过加密或采取了其他安全措施。

  • 数据加密:如果需要在 JWT 中存储敏感信息,可以对数据进行加密后再放入负载中。这样,即使 JWT 被截获,未经授权的第三方也无法解密和查看数据。

  • 使用 HTTPS:为了防止 JWT 在传输过程中被截获,应该使用 HTTPS 进行加密通信。

  • 如果使用 JWE(JSON Web Encryption) 规范,可以对 JWT 进行加密,只有持有解密密钥的方能读取。

RBAC 模型

表设计

  • users
  • auth_user_roles
  • auth_role
  • auth_role_permissions
  • auth_permission

限流组件

  • 基于 Reids ZSet + Lua 实现的滑动窗口限流

排队服务也用到了这个限流组件,排队服务是特定接口请求的限流、鉴权服务是用户请求的限流

types.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package limiter

import (
"context"
"time"
)

type Limiter interface {
// 返回 true,就是触发限流

LimitDefault(ctx context.Context, key string) (bool, error)
Limit(ctx context.Context, key string, interval time.Duration, rate int64, now time.Time) (bool, error)
}

redis_slide_window.go

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
package limiter

import (
"context"
_ "embed"
"time"

"github.com/redis/go-redis/v9"
)

//go:embed slide_window.lua
var luaScript string

type RedisSlidingWindowLimiter struct {
// Redis 客户端
cmd redis.Cmdable
// 窗口大小
// interval 内允许 rate 个请求
// 1s 内允许 3000 个请求
interval time.Duration
// 阈值
rate int64
}

func NewRedisSlidingWindowLimiter(cmd redis.Cmdable, interval time.Duration, rate int64) Limiter {
return &RedisSlidingWindowLimiter{
cmd: cmd,
interval: interval,
rate: rate,
}
}

func (b *RedisSlidingWindowLimiter) LimitDefault(ctx context.Context, key string) (bool, error) {
// 键(key)、窗口大小(interval)、速率(rate)、以及当前时间的毫秒数
return b.cmd.Eval(ctx, luaScript, []string{key},
b.interval.Milliseconds(), b.rate, time.Now().UnixMilli()).Bool()
}

func (b *RedisSlidingWindowLimiter) Limit(ctx context.Context, key string, interval time.Duration, rate int64, now time.Time) (bool, error) {
// 设置默认值
if interval == 0 {
interval = b.interval
}
if rate == 0 {
rate = b.rate
}
return b.cmd.Eval(ctx, luaScript, []string{key},
interval.Milliseconds(), rate, now.UnixMilli()).Bool()
}

slide_window.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
local min = now - window

-- 删除过期数据
redis.call('ZREMRANGEBYSCORE', key, '-inf', min)

-- 获取当前窗口内的请求数
local cnt = redis.call('ZCOUNT', key, '-inf', '+inf')

-- 检查当前时间戳是否已存在于集合中
local exists = redis.call('ZSCORE', key, now)

if cnt >= threshold then
if exists then
return "false" -- 允许继续请求
else
return "true" -- 触发限流
end
else
redis.call('ZADD', key, now, now)
redis.call('PEXPIRE', key, window)
return "false" -- 允许继续请求
end

Nginx

基本概念

Nginx 是目前最流行的 Web 服务器,最初由一位俄罗斯程序员 Igor Sysoev 开发。2019 年,Nginx 被美国的 F5 公司以 6.7 亿美元收购。

  • Nginx 的开源版本主要分为两种:
    • 主线版 (mainline):最新版本,包含较多新功能和正在开发的实验性模块功能,可能存在一些新的 bug。
    • 稳定版 (stable):经过长时间测试,bug 较少,功能较为稳定。
  • 安装方式:
    • 源码编译安装:拉取源代码后自己编译为可执行文件。
    • 预编译二进制包:直接下载编译好的可执行文件。
    • Docker Compose:拉取镜像后运行在容器中。
  • 主要用途:
    • 正向/反向代理:为客户端发出请求或为服务器接收请求。
    • 负载均衡:将请求分发到多个操作单元上执行。
    • HTTP 服务器:Nginx 也可以作为静态资源服务器使用。

配置文件

Nginx 的主要配置文件通常是 nginx.conf,并且一般位于 /etc/nginx/ 目录下。使用 nginx -t 命令可以测试配置文件的有效性而不需要实际重启 Nginx 服务。

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
# Nginx 配置文件
#
# 全局块
worker_processes 1;

# Events 块
events {
# 定义事件处理模型
}

# HTTP 块
http {
# 可以在这里定义全局的 HTTP 设置,例如 MIME 类型映射、默认错误页面等。

server {
# Server 址块
listen 80; # 示例: 监听 80 端口
server_name localhost; # 示例: 指定服务器名称

# Location 块
location / {
# 这里可以指定如何处理请求到根目录 "/" 的所有请求
root html; # 示例: 指定根目录
index index.html index.htm; # 示例: 指定索引文件
}

# 更多 location 块可以根据需要添加
}
}

全局块

全局块是配置文件的第一个块,也是配置文件的主体部分。它主要用来设置一些影响 Nginx 服务器整体运行的配置指令,包括但不限于配置运行 Nginx 服务器的用户(组)、允许生成的 worker process 数量、进程 PID 存放路径、日志存放路径和类型以及配置文件的引入等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 指定运行 Nginx 服务器的用户,只能在全局块配置
# 将 user 指令注释掉,或者配置成 nobody 的话所有用户都可以运行
# user [user] [group];
# user nobody nobody;
user nginx;

# 指定生成的 worker 进程的数量,也可使用自动模式,只能在全局块配置
worker_processes 1;

# 错误日志存放路径和类型
error_log /var/log/nginx/error.log warn;

# 进程 PID 存放路径
pid /var/run/nginx.pid;

events 块

1
2
3
4
5
6
7
events {
# 指定使用哪种网络 IO 模型,只能在 events 块中进行配置
# use epoll;

# 每个 worker process 允许的最大连接数
worker_connections 1024;
}

http 块

http 块是配置文件的主要部分,包括 http 全局块和 server 块

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
http {
# 引入其他配置文件
include /etc/nginx/mime.types;

# 默认类型,如果请求的 URL 没有包含文件类型,则使用默认类型
default_type application/octet-stream;

# 开启高效文件传输模式
sendfile on;

# 连接超时时间
keepalive_timeout 65;

# Access log 日志存放路径和类型
# 格式为:access_log <path> [format [buffer = size] [gzip[= level]] [flush = time] [if = condition]];
access_log /var/log/nginx/access.log main;

# 定义日志格式
log_format main '$remote_addr - $ remote_user [$time_local] '
'"$request" $ status $body_bytes_sent "$ http_referer" '
'"$http_user_agent" "$ http_x_forwarded_for"';

# 设置 sendfile 最大传输片段大小,默认为 0,表示不限制
# sendfile_max_chunk 1m;

# 每个连接的请求次数
# keepalive_requests 100;

# 开启 gzip 压缩
# gzip on;

# 开启 gzip 压缩的最小文件大小
# gzip_min_length 1k;

# gzip 压缩级别,1-9,级别越高压缩率越高,但是消耗 CPU 资源也越多
# gzip_comp_level 2;

# gzip 压缩文件类型
# gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

# upstream 指令用于定义一组服务器,一般用来配置反向代理和负载均衡
upstream www.example.com {
# ip_hash 指令用于设置负载均衡的方式,ip_hash 表示使用客户端的 IP 进行 hash,
# 这样可以保证同一个客户端的请求每次都会分配到同一个服务器,解决了 session 共享的问题
ip_hash;

# weight 用于设置权重,权重越高被分配到的几率越大
server 192.168.50.11:80 weight = 3;
server 192.168.50.12:80;
server 192.168.50.13:80;
}

# server 块
server {
# 参考 server 块的配置
}
}

server 块

server 块是配置虚拟主机的,⼀个 http 块可以包含多个 server 块,每个 server 块就是⼀个虚拟主机

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
server {
# 监听 IP 和端口
# listen 的格式为:
# listen [ip]:port [default_server] [ssl] [http2] [spdy]
# [proxy_protocol] [setfib = number] [fastopen = number] [backlog = number];
# listen 指令非常灵活,可以指定多个 IP 和端口,也可以使用通配符
# 下面是一些实际的例子:
# listen 127.0.0.1:80; # 监听来自 127.0.0.1 的 80 端口的请求
# listen 80; # 监听来自所有 IP 的 80 端口的请求
# listen *: 80; # 监听来自所有 IP 的 80 端口的请求,同上
# listen 127.0.0.1; # 监听来自 127.0.0.1 的 80 端口,默认端口为 80
listen 80;

# server_name 用来指定虚拟主机的域名,可以使用精确匹配、通配符匹配和正则匹配等方式
# server_name example.org www.example.org; # 精确匹配
# server_name *.example.org; # 通配符匹配
# server_name ~^www\d+\.example\.net$; # 正则匹配
server_name localhost;

# location 块用来配置请求的路由,一个 server 块可以包含多个 location 块,每个
# location 块就是一个请求路由
# location 块的格式是:
# location [=|~|~*|^~] /uri/ { ... }
# = 表示精确匹配,只有完全匹配上才能生效
# ~ 表示区分大小写的正则匹配
# ~* 表示不区分大小写的正则匹配
# ^~ 表示普通字符匹配,如果匹配成功,则不再匹配其他 location
# /uri/ 表示请求的 URI,可以是字符串,也可以是正则表达式
# { ... } 表示 location 块的配置内容

location / {
# root 指令用于指定请求的根目录,可以是绝对路径,也可以是相对路径
root /usr/share/nginx/html; # 根目录
# index 指令用于指定默认文件,如果请求的是目录,则会在目录下查找默认文件
index index.html index.htm; # 默认文件
}

# 下面是一些 location 的示例:
location = / { # 精确匹配请求
root /usr/share/nginx/html;
index index.html index.htm;
}
location ^~ /images/ { # 匹配以/images/开头的请求
root /usr/share/nginx/html;
}
location ~* \.(gif|jpg|jpeg)$ { # 匹配以 gif、jpg 或者 jpeg 结尾的请求
root /usr/share/nginx/html;
}
location !~ \.(gif|jpg|jpeg)$ { # 不匹配以 gif、jpg 或者 jpeg 结尾的请求
root /usr/share/nginx/html;
}
location !~* \.(gif|jpg|jpeg)$ { # 不匹配以 gif、jpg 或者 jpeg 结尾的请求
root /usr/share/nginx/html;
}

# error_page 用于指定错误页面,可以指定多个,按照优先级从高到低依次查找
error_page 500 502 503 504 /50x.html; # 错误页面
location = /50x.html {
root /usr/share/nginx/html;
}
}

常用命令

1
2
3
4
5
6
7
8
nginx # 启动 Nginx
nginx -c filename # 指定配置文件
nginx -V # 查看 Nginx 的版本和编译参数等信息
nginx -t # 检查配置文件是否正确,也可用来定位配置文件的位置
nginx -s quit # 优雅停止 Nginx
nginx -s stop # 快速停止 Nginx
nginx -s reload # 热启动,重新加载配置文件
nginx -s reopen # 重新打开日志文件

常用模块

1
2
3
4
5
6
# nginx -V
nginx version: nginx/1.25.5
built by gcc 12.2.0 (Debian 12.2.0-14)
built with OpenSSL 3.0.9 30 May 2023 (running with OpenSSL 3.0.11 19 Sep 2023)
TLS SNI support enabled
configure arguments: --prefix =/etc/nginx --sbin-path =/usr/sbin/nginx --modules-path =/usr/lib/nginx/modules --conf-path =/etc/nginx/nginx.conf --error-log-path =/var/log/nginx/error.log --http-log-path =/var/log/nginx/access.log --pid-path =/var/run/nginx.pid --lock-path =/var/run/nginx.lock --http-client-body-temp-path =/var/cache/nginx/client_temp --http-proxy-temp-path =/var/cache/nginx/proxy_temp --http-fastcgi-temp-path =/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path =/var/cache/nginx/uwsgi_temp --http-scgi-temp-path =/var/cache/nginx/scgi_temp --user = nginx --group = nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_v3_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt ='-g -O2 -ffile-prefix-map =/data/builder/debuild/nginx-1.25.5/debian/debuild-base/nginx-1.25.5 =. -fstack-protector-strong -Wformat -Werror = format-security -Wp,-D_FORTIFY_SOURCE = 2 -fPIC' --with-ld-opt ='-Wl,-z, relro -Wl,-z, now -Wl,--as-needed -pie'
模块名(Module Name) 描述(Description)
http_access_module 接受或拒绝特定的客户端请求
http_auth_request_module 根据子请求的结果实现客户端授权
http_auth_basic_module 使用用户名和密码进行 HTTP 基本认证,限制对资源的访问
http_autoindex_module 自动生成目录列表
http_browser_module 从 User-Agent 请求头中识别客户端浏览器
http_charset_module 为 Content-Type 响应头添加特定字符集
http_empty_gif_module 返回一个 1 像素的透明 GIF 图片
http_fastcgi_module 提供 FastCGI 支持
http_geo_module 根据 IP 地址获取地理位置信息
http_gzip_module 支持 Gzip 压缩
http_limit_conn_module 限制并发连接数
http_limit_req_module 限制请求速率
http_map_module 基于变量映射获取值
http_memcached_module 提供 Memcached 支持
http_proxy_module 提供反向代理支持
http_referer_module 防止盗链
http_rewrite_module 支持 URL 重写
http_scgi_module 将请求转发到 SCGI 服务器
http_ssi_module 处理和支持 SSI(服务器端包含)
http_split_clients_module 根据客户端 IP 地址或其他变量将客户端分组,通常用于 A/B 测试
http_upstream_hash_module 提供一致性哈希负载均衡
http_upstream_ip_hash_module 提供 IP 哈希负载均衡
http_upstream_keepalive_module 支持长连接负载均衡
http_upstream_least_conn_module 提供最少连接负载均衡
http_upstream_zone_module 提供共享内存负载均衡
http_userid_module 为客户端设置唯一的 ID(UID、cookie)
http_uwsgi_module 将请求转发到 uWSGI 服务器,通常用于 Python 应用

统一鉴权

异构的后端服务,使用 API 网关统一鉴权

  • 客户端请求: 客户端请求 API 网关,附带 JWT
  • API网关鉴权: API 网关验证JWT的有效性和权限(扩展做限流)
  • 转发请求: 如果 JWT 有效,API 网关将请求转发给相应的后端服务(还可以添加一些头部信息)
  • 后端服务响应: 后端服务处理请求并返回响应,API 网关将响应返回给客户端

通过将 NGINX 作为反向代理和 API 网关,可以在 NGINX 层面集中处理所有的身份验证和授权逻辑,如果验证通过,Nginx 将请求转发到相应的后端服务;如果验证失败,返回 401 未授权错误。将鉴权逻辑外包给专门的鉴权服务,方便管理和扩展。

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
server {
listen 80;
server_name localhost;

# 内部鉴权位置,不要对外暴露
location = /auth-internal {
internal;
proxy_pass http://host.docker.internal: 7777/api/auth;
proxy_intercept_errors on; # 确保拦截错误
}
# 自定义 401 错误响应
error_page 401 = @auth_error;
location @auth_error {
return 401 Unauthorize;
}

# 需要鉴权的请求
location /api/payment/ {
# 使用 http_auth_request_module 鉴权
auth_request /auth-internal;
# 如果鉴权通过,设置请求头
auth_request_set $user_role $ upstream_http_x_user_role;
auth_request_set $user_id $ upstream_http_x_user_id;
# 设置鉴权通过后的自定义头部
proxy_set_header X-User-Role $user_role;
proxy_set_header X-User-ID $user_id;
# 转发到后端服务
proxy_pass http://host.docker.internal: 8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

鉴权服务

JWT 校验

用户服务做 JWT 的签发与注销(需要依赖第三方组件做记录,如 Redis)

请求不带 JWT 或 JWT 校验失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$hcjjj: ~ ❯ curl -i http://127.0.0.1:7777/api/auth                                                       
HTTP/1.1 401 Unauthorized
Access-Control-Allow-Headers: Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range
Access-Control-Allow-Methods: GET, HEAD, POST, PATCH, PUT, DELETE
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers
Access-Control-Max-Age: 86400
Content-Type: text/plain; charset = utf-8
Traceparent: 00-229a8d047c093eb9637d020dd9471d3a-b59927828a3f7d87-00
Vary: Origin
X-Content-Type-Options: nosniff
Date: Mon, 05 Aug 2024 11:42:26 GMT
Content-Length: 13

Unauthorized

请求带正确的 JWT :

1
2
3
4
5
6
7
8
9
10
11
12
13
$hcjjj: ~ ❯ curl -H " Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1IiwidHlwZSI6IkFDQ0VTU19UT0tFTiIsInJvbGUiOiJVU0VSIiwiZXhwIjoxNzI1Njc3NjEzLCJpYXQiOjE3MjA0OTM2MTN9.Z0CXV0B-7USnB6VhHRT4RsN3lxv11R_5h7wxvGoeoLQ "  -i http://127.0.0.1:7777/api/auth
HTTP/1.1 200 OK
Access-Control-Allow-Headers: Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range
Access-Control-Allow-Methods: GET, HEAD, POST, PATCH, PUT, DELETE
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers
Access-Control-Max-Age: 86400
Traceparent: 00-02f50d29723ccdd50dfab510b294e763-17fb6226dc8b5e07-00
Vary: Origin
X-User-Id: 5
X-User-Role: USER
Date: Mon, 05 Aug 2024 11:43:47 GMT
Content-Length: 0

通过鉴权的请求会在其头部添加 user_id、user_role、user_uuid 字段,逻辑为:

鉴权服务 ➡️ Nginx

1
2
3
4
5
6
7
8
9
10
if permission {
// 鉴权通过的话,设置自定义头部,给到 Nginx,后续再转发到对应的服务
w.Header().Set("X-User-ID", userId)
w.Header().Set("X-User-Role", userInfo.Role)
w.Header().Set("X-User-UUID", userInfo.Uuid)
// 返回 200 OK
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}

Nginx ➡️ 对应服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 需要鉴权的请求
location /api/payment/ {
# 使用 http_auth_request_module 鉴权
auth_request /auth-internal;
# 从鉴权服务的响应中提取名为 X-User-Role 的头部,并将其值赋给变量 $user_role
auth_request_set $user_role $ upstream_http_x_user_role;
auth_request_set $user_id $ upstream_http_x_user_id;
auth_request_set $user_uuid $ upstream_http_x_user_uuid;
# 设置鉴权服务校验通过后的 user_role user_id 和 user_uuid 到原始请求中
proxy_set_header X-User-Role $user_role;
proxy_set_header X-User-ID $user_id;
proxy_set_header X-User-UUID $user_uuid;
# 转发到后端服务
proxy_pass http://host.docker.internal: 8888;
# proxy_pass https://prod.creaibo.com;
}

JWT退出机制:使用 Redis 记录已退出的 JWT,每次校验的时候检查一下 JWT 是否在黑名单中,过期时间与 JWT 保存一致

请求限流

[基于 IP 的限流检查] ➡️ [JWT 登录校验] ➡️ [RBAC 权限检查] ➡️ [写入相关信息到请求头并转发至对于后端服务]

1
2
3
4
5
6
7
8
9
10
// 根据 IP 做限流检查
limited, err := svc.RateLimiter.LimitDefault(context.Background(), GenLimiterKey(r))
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if limited {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

RBAC 鉴权

Nginx 配置

1
2
3
4
5
6
7
8
location = /auth-internal {
internal;
# 设置代理请求头以包含原始客户端 IP 和 URL 信息, Method 信息
proxy_set_header X-Original-IP $remote_addr;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_pass http://host.docker.internal: 7777/api/auth;
}
  1. 获取请求的鉴权信息
  2. 根据 [用户ID,原始 URL,Method] 进行接口权限的检查
  3. 白名单模式,需要对系统接口进行梳理和配置(后续可扩展为正则表达式或通配符模式)

Ingress

Ingress 是对集群中服务的外部访问进行管理的 API 对象,Ingress可以提供统一的入口控制、HTTP/HTTPS路由、SSL终止和负载均衡等功能。如通过 Ingress 资源来配置不同的转发规则,从而达到根据不同的规则设置访问集群内不同的 Service 所对应的后端 Pod

API对象(Application Programming Interface Object)通常指的是在软件系统中定义的一组接口和协议,它们允许不同的软件应用程序之间进行交互。API对象可以是数据结构、函数、类或服务,它们提供了一种标准化的方法来访问一个应用程序或服务的功能或数据。

Nginx Ingress Controller 是 Kubernetes 生态系统中广泛使用的 Ingress 控制器之一,上文统一鉴权是用 Nginx 的 auth_request 模块,部署在 K8s 的后需要使用 Nginx Ingress Controller 的 auth-url 注解External Authentication )或插入自定义 Nginx 配置Configuration snippet)来实现。

Nginx Ingress

  1. 安装 heml
  2. 安装 Kubectl
  3. kubectl 连接集群
  4. helm 安装 ingress-nginx
  5. 版本与升级 Supported Versions table
  6. 新建 Ingress

  • Ingressannotations 主要用于配置全局的行为和特性,如超时、重写规则等,但不能直接修改或插入复杂的 location 配置
  • 使用 nginx.ingress.kubernetes.io/server-snippet 可以添加全局的自定义 location 块,但不能针对单个路径
  • 使用 nginx.ingress.kubernetes.io/location-snippet 可以针对特定路径添加自定义的 location 配置

相关扩展

Kubernetes 资源对象

  • 定义: Kubernetes 资源对象是 Kubernetes 集群中用于描述和管理各种资源的 API 对象。它们定义了集群中需要运行和管理的服务、应用程序、网络、存储等。
  • 种类: 包括多种类型,如 PodServiceDeploymentIngressConfigMapSecretPersistentVolume 等。
  • 功能: 它们用于声明和管理集群中的资源状态。例如,Deployment 资源用于管理应用的副本和滚动更新,Service 资源用于定义服务的访问方式。
  • 运行方式: 资源对象本身不直接运行在 Pod 中,它们是 Kubernetes 集群的配置和管理单位。它们通过 Kubernetes 控制平面(控制器)和调度器来管理集群中的实际工作负载。

Pod

  • 定义: Pod 是 Kubernetes 中最基本的部署单元,是运行在集群节点上的一个或多个容器的集合。Pod 提供了容器运行时所需的网络和存储资源。
  • 功能: Pod 是实际运行应用程序代码的地方。每个 Pod 包含一个或多个容器,这些容器共享网络和存储。Pod 还可以包括 Init 容器、存储卷等。
  • 运行方式: Pod 是 Kubernetes 集群中的实际工作负载,它们被 Kubernetes 调度器分配到节点上,并由容器运行时(如 Docker、containerd)执行容器。Pod 通过 Deployment、DaemonSet 或 StatefulSet 等控制器进行管理和调度。

关系

  • 资源对象 vs Pod: Pod 是一种 Kubernetes 资源对象(kind: Pod),它用于定义和运行容器。其他资源对象(如 Deployment)用于管理 Pod 的生命周期。资源对象提供了集群的配置和管理功能,而 Pod 是实际运行应用程序的实体。
  • 管理: Kubernetes 控制平面和调度器使用资源对象来管理和调度 Pod。通过创建和更新资源对象,用户可以控制 Pod 的部署、扩展、更新等操作。

Ingress 资源

  • 定义: Ingress 是 Kubernetes 的一个标准 API 资源,用于管理集群外部访问服务的路由规则。

  • 功能: 它定义了如何将 HTTP 和 HTTPS 请求路由到集群内部的服务。通过 Ingress 资源,可以指定路径、主机、证书等信息。

  • 实现: Ingress 可以由不同的控制器来实现,包括 Nginx、Traefik、HAProxy、Istio 等。具体的功能和配置会依赖于你使用的 Ingress 控制器。

  • 运行: Ingress 本身不是一个运行中的组件,它只是一个 Kubernetes 资源对象。它定义了路由规则、主机、路径等,控制如何将请求转发到服务。

    部署: 你通过 kubectl apply 命令创建或更新 Ingress 资源,它会被 Kubernetes 控制平面管理,并与相应的 Ingress 控制器一起工作。

NginxIngress 控制器

  • 定义: NginxIngress 控制器(即 Ingress-Nginx 控制器)是实现 Ingress 资源的具体方案之一。
  • 功能: 它使用 Nginx 作为反向代理服务器,将外部请求根据 Ingress 规则转发到集群内部的服务。它提供了许多高级功能,如自定义 Nginx 配置、TLS 终止、路径重写等。
  • 配置: Ingress-Nginx 控制器通常通过 ConfigMapIngress 注解等方式进行配置。它还允许使用注解来修改或扩展 Nginx 的行为。
  • 运行: NginxIngress 控制器运行在 Kubernetes 集群中的 Pod 中。它通常是一个或多个 Pod 的集合,这些 Pod 运行着 Nginx 实例,负责根据 Ingress 资源的规则处理和路由流量。
  • 部署: Ingress-Nginx 控制器作为 Kubernetes 的 Deployment 或 DaemonSet 部署,通常会在 kube-system 命名空间中或其他指定的命名空间中运行。

关系

  • Ingress Controller 是一个运行在 Kubernetes 集群中的应用程序,它负责实现 Ingress 资源定义的规则。它监听 Ingress 资源的变化,并根据这些变化来配置自己的负载均衡和服务路由规则。你提供的 Service 配置是 NGINX Ingress Controller 的一部分,它作为服务运行在集群中,并被 Kubernetes API 管理。
  • Ingress 资源 是 Kubernetes 的 API 对象,它定义了如何将外部请求路由到集群内的服务。Ingress 资源包含了路由规则,例如基于域名或路径的路由。
  • 首先,你需要部署一个 Ingress Controller(比如 NGINX Ingress Controller)到你的 Kubernetes 集群中。这个 Controller 会作为一个服务运行,并且监听 API Server 以获取 Ingress 资源的变化。
  • 然后,你创建 Ingress 资源,定义了路由规则。这些规则告诉 Ingress Controller 如何将进入的请求转发到集群内的特定服务。
  • 当外部请求到达时,它们首先会被 Ingress Controller 接收,然后根据 Ingress 资源中定义的规则,将请求路由到正确的服务。
  • Ingress Controller 是实现 Ingress 规则的组件,而 Ingress 资源定义了这些规则

问题记录

如何查看 Ingress 配置是否加载到 Nginx 中

Pod 实例名称:ingress-web-ingress-nginx-controller-5cxxxxxf4b-jxxxr

1
2
3
ingress-web-ingress-nginx-controller-5cxxxxxf4b-jxxxr:/etc/nginx$ pwd
/etc/nginx
ingress-web-ingress-nginx-controller-5cxxxxxf4b-jxxxr:/etc/nginx$ cat nginx.conf

测试 ingress-nginx-controller 访问鉴权服务的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ingress-web-ingress-nginx-controller-5cxxxxxf4b-jxxxr:/etc/nginx$ curl -H "Authorization:bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1IiwidHlwZSI6IkFDQ0VTU19UT0tFTiIsInJvbGUiOiJVU0VSIiwiZXhwIjoxNzM0NTc1MDQzLCJpYXQiOjE3MjkzOTEwNDN9.4WvI5ldUidYxtYonCNZZ2rurW6U2B8A5xSpLhs-_WUU" -i http://auth-service.default:7777/api/auth
HTTP/1.1 200 OK
Access-Control-Allow-Headers: Content-Type, Origin, X-CSRF-Token, Authorization, AccessToken, Token, Range
Access-Control-Allow-Methods: GET, HEAD, POST, PATCH, PUT, DELETE
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers
Access-Control-Max-Age: 86400
Traceparent: 00-eea4932e4369c57578feb3c51a3bb081-cda499a1e131d834-00
Vary: Origin
X-User-Id: 5
X-User-Role: USER
X-User-Uuid: c2a2b349b565403fa98b72691ec40d12
Date: Tue, 03 Dec 2024 08:38:27 GMT
Content-Length: 0

ingress-web-ingress-nginx-controller-5cxxxxxf4b-jxxxr:/etc/nginx$

持续查看 Nginx 日志

1
ingress-web-ingress-nginx-controller-54xxxxxx58-bh4bl:/etc/nginx$ tail -f /var/log/nginx/nginx_access.log

在 TKE 中获取客户端真实源 IP