目录
- 网关统一认证中心的背景与需求
- OpenResty 的关键技术点
- 实现步骤:构建认证中心
- 高并发场景下的共享内存优化
- 其他优化与建议
- 完整 Nginx 配置文件与 Lua 脚本
- 部署与测试
- 总结
网关统一认证中心的背景与需求
在微服务架构中,网关是统一入口,负责路由、认证、限流等功能。统一认证中心是网关的核心组件,用于验证客户端请求的合法性(通常通过 token)。本教程的目标是使用 OpenResty 实现一个高效的认证中心,满足以下需求:
- token 存储:利用 Nginx 的共享内存存储 token,确保高性能查找。
- token 推送接口:提供
/api/v1/tokens 接口,接受认证后的 token 并存入共享内存。
- token 验证:在请求处理阶段检查 header 中的 token,若有效则代理到后端,否则返回 401。
- 高并发优化:解决多个 Nginx Worker 对共享内存的竞争问题。
- 扩展性:提供优化建议,支持分布式部署和复杂场景。
OpenResty 的非阻塞架构和 Lua 的轻量高效使其非常适合实现这一功能。接下来,我们将逐一讲解实现步骤。
OpenResty 的关键技术点
在实现认证中心之前,了解以下 OpenResty 技术点将帮助你更好地理解代码:
- Nginx 共享内存 (
ngx.shared):Nginx 提供共享内存字典,允许多个 Worker 进程共享数据,适合存储 token。
- Lua 脚本阶段:
access_by_lua* 适合认证检查,content_by_lua* 适合处理 API 请求。
- Nginx 代理:通过
proxy_pass 将验证通过的请求转发到后端服务器。
- LuaJIT:OpenResty 使用 LuaJIT 编译 Lua 脚本,性能接近 C 语言。
- 非阻塞 I/O:OpenResty 的事件驱动模型确保高并发处理能力。
实现步骤:构建认证中心
步骤 1:配置 Nginx 共享内存存储 token
作用与原理
Nginx 的共享内存通过 lua_shared_dict 指令定义,允许多个 Worker 进程访问同一块内存区域。我们将使用它存储 token,格式为 key: token 值, value: 用户信息或其他元数据。
- 适用场景:存储高频访问的 token 数据,减少外部数据库查询。
- 注意事项:共享内存大小固定,需合理规划;操作非线程安全,需注意并发竞争。
配置共享内存
在 Nginx 配置文件(nginx.conf)的 http 块中添加以下指令:
1
2
3
4
|
http {
# 定义共享内存,名为 tokens,大小 10MB
lua_shared_dict tokens 10m;
}
|
说明:
tokens 是共享内存的名称,10m 表示分配 10MB 内存。
- 可根据 token 数量调整大小(每个 token 约占用几 KB)。
初始化共享内存
在 init_by_lua* 阶段初始化共享内存(可选,用于预加载数据):
1
2
3
4
5
|
init_by_lua_block {
-- 可选:预加载 token(例如从文件或数据库)
local tokens = ngx.shared.tokens
tokens:set("sample_token_123", "user_id:1001,expire:2025-12-31")
}
|
说明:
ngx.shared.tokens 是共享内存对象。
set 方法存储 key-value 对,value 可包含用户 ID、过期时间等。
步骤 2:实现 /api/v1/tokens 接口存储 token
作用与原理
/api/v1/tokens 是一个 POST 接口,接受外部系统推送的认证后 token。Lua 脚本解析请求体,将 token 存入共享内存。
- 适用场景:外部认证服务(如 OAuth 服务器)推送 token。
- 注意事项:需验证请求来源,防止未授权推送;建议设置 token 过期时间。
Nginx 配置
在 nginx.conf 的 server 块中添加 location:
1
2
3
4
5
6
7
8
|
server {
listen 80;
server_name auth.example.com;
location /api/v1/tokens {
content_by_lua_file /path/to/lua/push_token.lua;
}
}
|
Lua 脚本 (push_token.lua)
实现 token 推送逻辑:
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
|
-- /path/to/lua/push_token.lua
local cjson = require "cjson"
-- 读取 POST 请求体
ngx.req.read_body()
local body = ngx.req.get_body_data()
if not body then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Missing request body"}))
return
end
-- 解析 JSON
local data, err = cjson.decode(body)
if not data then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Invalid JSON: " .. (err or "unknown")}))
return
end
-- 验证必要字段
local token = data.token
local user_info = data.user_info
local expire = data.expire
if not token or not user_info or not expire then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Missing token, user_info, or expire"}))
return
end
-- 存储到共享内存
local tokens = ngx.shared.tokens
local success, err = tokens:set(token, cjson.encode({user_info = user_info, expire = expire}))
if not success then
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(cjson.encode({error = "Failed to store token: " .. (err or "unknown")}))
return
end
-- 返回成功响应
ngx.status = ngx.HTTP_OK
ngx.say(cjson.encode({message = "Token stored successfully"}))
|
代码说明:
- 使用
cjson 解析 POST 请求的 JSON 体,期望包含 token、user_info 和 expire 字段。
- 验证字段完整性,防止无效数据。
- 使用
ngx.shared.tokens:set 存储 token,value 为 JSON 编码的用户信息和过期时间。
- 返回 JSON 响应,指示成功或失败。
示例请求:
1
2
3
|
curl -X POST http://auth.example.com/api/v1/tokens \
-H "Content-Type: application/json" \
-d '{"token":"xyz123","user_info":"user_id:1001","expire":"2025-12-31"}'
|
预期响应:
1
|
{"message":"Token stored successfully"}
|
步骤 3:在 access_by_lua* 阶段验证 token
作用与原理
access_by_lua* 是访问控制阶段,适合检查请求的认证状态。我们将检查 header 中的 Authorization 字段(如 Bearer xyz123),验证 token 是否存在于共享内存。
- 适用场景:拦截未认证请求,确保只有合法用户访问后端。
- 注意事项:快速执行,减少延迟;支持 token 过期检查。
Nginx 配置
在 nginx.conf 的 location 块中添加 access_by_lua_file:
1
2
3
4
|
location / {
access_by_lua_file /path/to/lua/check_token.lua;
proxy_pass http://backend;
}
|
Lua 脚本 (check_token.lua)
实现 token 验证逻辑:
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
|
-- /path/to/lua/check_token.lua
local cjson = require "cjson"
-- 获取 Authorization header
local auth_header = ngx.var.http_authorization
if not auth_header then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["WWW-Authenticate"] = 'Bearer realm="auth.example.com"'
ngx.say(cjson.encode({error = "Missing Authorization header"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 提取 Bearer token
local token = auth_header:match("^Bearer%s+(.+)$")
if not token then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Invalid Authorization header"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 检查 token 是否存在
local tokens = ngx.shared.tokens
local value = tokens:get(token)
if not value then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Invalid or expired token"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 解析 token 元数据
local data, err = cjson.decode(value)
if not data then
ngx.log(ngx.ERR, "Failed to decode token data: ", err)
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(cjson.encode({error = "Internal server error"}))
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- 检查 token 是否过期
local expire = data.expire
local current_time = os.date("%Y-%m-%d")
if expire < current_time then
tokens:delete(token) -- 删除过期 token
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Token expired"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 认证通过,设置下游变量
ngx.var.user_info = data.user_info
|
代码说明:
- 检查
Authorization header,提取 Bearer token。
- 使用
tokens:get 查询共享内存,验证 token 存在性。
- 解析存储的 JSON 数据,检查
expire 字段是否过期。
- 若认证通过,设置
ngx.var.user_info 供后端使用;否则返回 401。
示例请求:
1
|
curl http://auth.example.com/ -H "Authorization: Bearer xyz123"
|
预期响应(认证失败):
1
|
{"error":"Invalid or expired token"}
|
步骤 4:代理请求到后端服务器
作用与原理
认证通过后,OpenResty 使用 proxy_pass 将请求转发到后端服务器。可以通过 Nginx 的 upstream 模块定义后端服务器集群。
- 适用场景:将合法请求路由到微服务。
- 注意事项:确保后端服务器支持用户信息的传递(如通过 header)。
Nginx 配置
定义 upstream 和 proxy_pass:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name auth.example.com;
location / {
access_by_lua_file /path/to/lua/check_token.lua;
proxy_set_header X-User-Info $user_info; # 传递用户信息
proxy_pass http://backend;
}
}
|
说明:
upstream backend 定义后端服务器集群。
proxy_set_header 将 user_info 传递给后端。
proxy_pass 转发请求到 backend。
高并发场景下的共享内存优化
问题分析
Nginx 的共享内存(ngx.shared)在多 Worker 进程下是非线程安全的,多个 Worker 可能同时读写同一 key,导致竞争问题。在高并发场景下(如每秒数万请求),可能出现以下问题:
- 锁竞争:共享内存操作涉及内部锁,高并发下锁竞争会导致性能下降。
- 内存溢出:若 token 数量过多,共享内存可能耗尽。
- 数据一致性:快速读写可能导致数据不一致(如覆盖有效 token)。
优化策略
以下是针对共享内存竞争的优化方案:
-
减少写操作:
- 批量写入:在
/api/v1/tokens 接口中,允许批量推送 token,减少单次写操作。
- 异步写入:使用
ngx.timer 将 token 写入操作放入后台任务,降低主请求路径的竞争。
- 示例(异步写入):
1
2
3
4
5
6
7
|
local function async_store_token(token, value)
local tokens = ngx.shared.tokens
tokens:set(token, value)
end
-- 在 push_token.lua 中
ngx.timer.at(0, async_store_token, token, cjson.encode({user_info = user_info, expire = expire}))
|
-
使用外部存储(如 Redis):
- 场景:当 token 数量较大或需要分布式部署时,使用 Redis 替代共享内存。
- 实现:引入
lua-resty-redis 模块,存储 token 到 Redis。
- 示例:
1
2
3
4
5
6
7
8
|
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
red:set("token:" .. token, value)
red:expire("token:" .. token, 86400) -- 过期时间 1 天
end
|
-
分区共享内存:
- 场景:将 token 分片存储到多个共享内存区域,减少单一字典的竞争。
- 实现:定义多个
lua_shared_dict(如 tokens_1、tokens_2),根据 token 的哈希值选择存储区域。
- 示例配置:
1
2
|
lua_shared_dict tokens_1 10m;
lua_shared_dict tokens_2 10m;
|
1
2
3
4
|
local function get_dict(token)
local hash = ngx.crc32_short(token)
return hash % 2 == 0 and ngx.shared.tokens_1 or ngx.shared.tokens_2
end
|
-
缓存热点 token:
- 场景:高频访问的 token 可缓存到 Worker 本地 Lua 表,减少共享内存查询。
- 实现:在
init_worker_by_lua* 初始化本地缓存,定期同步。
- 示例:
1
2
3
4
5
6
7
8
9
10
|
init_worker_by_lua_block {
_G.local_cache = {}
}
-- 在 check_token.lua 中
local token = auth_header:match("^Bearer%s+(.+)$")
local value = _G.local_cache[token] or tokens:get(token)
if value then
_G.local_cache[token] = value -- 更新本地缓存
end
|
-
限流写操作:
- 场景:限制
/api/v1/tokens 接口的请求速率,减少共享内存写压力。
- 实现:在
access_by_lua* 中使用 lua-resty-limit-req 模块。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
lua_shared_dict limit_req 10m;
location /api/v1/tokens {
access_by_lua_block {
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("limit_req", 100, 50) -- 100 req/s, burst 50
local delay, err = lim:incoming("tokens_api", true)
if not delay then
ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
ngx.say(cjson.encode({error = "Rate limit exceeded"}))
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
if delay > 0 then
ngx.sleep(delay)
end
}
content_by_lua_file /path/to/lua/push_token.lua;
}
|
其他优化与建议
-
token 过期管理:
- 定期清理过期 token,防止共享内存溢出。
- 实现:在
timer_by_lua* 中运行清理任务。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
init_by_lua_block {
local function clean_expired()
local tokens = ngx.shared.tokens
for _, token in ipairs(tokens:get_keys()) do
local value = tokens:get(token)
local data = cjson.decode(value)
if data.expire < os.date("%Y-%m-%d") then
tokens:delete(token)
end
end
ngx.timer.at(3600, clean_expired) -- 每小时运行
end
ngx.timer.at(0, clean_expired)
}
|
-
分布式部署:
- 使用 Redis 或数据库作为中央存储,支持多实例网关。
- 部署 Nginx 集群,结合 DNS 或负载均衡器分发流量。
-
安全加固:
- 对
/api/v1/tokens 接口添加 IP 白名单或 API 密钥验证。
- 使用 HTTPS 保护 token 传输。
- 示例(IP 白名单):
1
2
3
4
5
|
location /api/v1/tokens {
allow 192.168.1.0/24;
deny all;
content_by_lua_file /path/to/lua/push_token.lua;
}
|
-
日志与监控:
- 在
log_by_lua* 记录认证失败的请求,发送到外部系统(如 ELK)。
- 使用 Prometheus 和 Grafana 监控共享内存使用率和认证性能。
-
错误处理:
- 为所有 Lua 脚本添加错误捕获(
pcall),防止脚本异常导致服务中断。
- 示例:
1
2
3
4
5
6
7
8
9
|
local ok, err = pcall(function()
-- 脚本逻辑
end)
if not ok then
ngx.log(ngx.ERR, "Lua error: ", err)
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(cjson.encode({error = "Internal server error"}))
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
|
完整 Nginx 配置文件与 Lua 脚本
Nginx 配置文件 (nginx.conf)
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
|
http {
lua_shared_dict tokens 10m;
lua_shared_dict limit_req 10m;
upstream backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
server_name auth.example.com;
location /api/v1/tokens {
access_by_lua_block {
local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("limit_req", 100, 50)
local delay, err = lim:incoming("tokens_api", true)
if not delay then
ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
ngx.say(cjson.encode({error = "Rate limit exceeded"}))
return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
end
if delay > 0 then
ngx.sleep(delay)
end
}
content_by_lua_file /path/to/lua/push_token.lua;
}
location / {
access_by_lua_file /path/to/lua/check_token.lua;
proxy_set_header X-User-Info $user_info;
proxy_pass http://backend;
}
}
}
|
Lua 脚本 (push_token.lua)
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
|
local cjson = require "cjson"
ngx.req.read_body()
local body = ngx.req.get_body_data()
if not body then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Missing request body"}))
return
end
local data, err = cjson.decode(body)
if not data then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Invalid JSON: " .. (err or "unknown")}))
return
end
local token = data.token
local user_info = data.user_info
local expire = data.expire
if not token or not user_info or not expire then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say(cjson.encode({error = "Missing token, user_info, or expire"}))
return
end
local tokens = ngx.shared.tokens
local success, err = tokens:set(token, cjson.encode({user_info = user_info, expire = expire}))
if not success then
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(cjson.encode({error = "Failed to store token: " .. (err or "unknown")}))
return
end
ngx.status = ngx.HTTP_OK
ngx.say(cjson.encode({message = "Token stored successfully"}))
|
Lua 脚本 (check_token.lua)
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
|
local cjson = require "cjson"
local auth_header = ngx.var.http_authorization
if not auth_header then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["WWW-Authenticate"] = 'Bearer realm="auth.example.com"'
ngx.say(cjson.encode({error = "Missing Authorization header"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local token = auth_header:match("^Bearer%s+(.+)$")
if not token then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Invalid Authorization header"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local tokens = ngx.shared.tokens
local value = tokens:get(token)
if not value then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Invalid or expired token"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
local data, err = cjson.decode(value)
if not data then
ngx.log(ngx.ERR, "Failed to decode token data: ", err)
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(cjson.encode({error = "Internal server error"}))
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
local expire = data.expire
local current_time = os.date("%Y-%m-%d")
if expire < current_time then
tokens:delete(token)
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say(cjson.encode({error = "Token expired"}))
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
ngx.var.user_info = data.user_info
|
部署与测试
部署步骤
- 安装 OpenResty:从 openresty.org 下载并安装,支持 Lua 模块。
- 配置 Nginx:将上述
nginx.conf 保存到 OpenResty 的配置目录。
- 创建 Lua 脚本:将
push_token.lua 和 check_token.lua 保存到指定路径。
- 启动服务:运行
openresty -t 检查配置,执行 openresty 启动。
- 测试接口:
- 推送 token:
curl -X POST http://auth.example.com/api/v1/tokens -H "Content-Type: application/json" -d '{"token":"xyz123","user_info":"user_id:1001","expire":"2025-12-31"}'
- 验证请求:
curl http://auth.example.com/ -H "Authorization: Bearer xyz123"
测试预期
- 推送 token 成功,返回
{"message":"Token stored successfully"}。
- 使用有效 token 访问,请求被代理到后端。
- 使用无效 token 访问,返回 401 和
{"error":"Invalid or expired token"}。
总结
通过 OpenResty 和 Lua,我们实现了一个高效的网关统一认证中心,利用共享内存存储 token,提供了 /api/v1/tokens 接口和 token 验证功能。针对高并发场景,我们提出了异步写入、Redis 替代、分区存储等优化策略,确保系统在高负载下的稳定性。本教程的代码示例简单易懂,适合初学者快速上手,同时为进阶用户提供了丰富的优化思路。
评论 0