在 Nginx 中使用 Lua 脚本调用外部 API 验证 Token 是一种灵活且强大的方式,特别适合 OpenResty 环境。下面我将详细介绍如何实现这一方案。
😃文章最后我会附上一个我实际项目中使用的完整实例,直接可以运行,有需要的小伙伴可以参考。
nginx
server {
listen 443 ssl;
server_name your.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
access_by_lua_block {
-- 引入必要的库
local http = require "resty.http"
local cjson = require "cjson"
-- 获取 Authorization 头
local auth_header = ngx.var.http_Authorization
if not auth_header then
ngx.log(ngx.ERR, "No Authorization header provided")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 提取 Bearer token
local _, _, token = string.find(auth_header, "Bearer%s+(.+)")
if not token then
ngx.log(ngx.ERR, "Invalid Authorization header format")
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
-- 创建 HTTP 客户端
local httpc = http.new()
httpc:set_timeout(2000) -- 2秒超时
-- 调用验证 API
local res, err = httpc:request_uri("https://auth-service.example.com/verify", {
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. token,
},
body = cjson.encode({
endpoint = ngx.var.request_uri,
method = ngx.var.request_method
}),
ssl_verify = true -- 生产环境应启用 SSL 验证
})
-- 处理错误情况
if not res then
ngx.log(ngx.ERR, "Failed to request auth service: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- 检查响应状态码
if res.status ~= 200 then
ngx.log(ngx.ERR, "Auth service returned status: ", res.status)
ngx.exit(res.status == 401 and ngx.HTTP_UNAUTHORIZED or ngx.HTTP_FORBIDDEN)
end
-- 解析响应体
local auth_data = cjson.decode(res.body)
if not auth_data or not auth_data.valid then
ngx.log(ngx.ERR, "Invalid token verification response")
ngx.exit(ngx.HTTP_FORBIDDEN)
end
-- 可选:将验证信息传递给后端
ngx.req.set_header("X-User-ID", auth_data.user_id or "")
ngx.req.set_header("X-User-Roles", auth_data.roles or "")
}
# 验证通过后转发请求
proxy_pass http://backend_service;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method;
}
}
关键组件解析
1. 必要的 Lua 库
resty.http
: 用于发送 HTTP 请求到验证服务cjson
: 用于处理 JSON 数据
2. Token 提取逻辑
lua
local _, _, token = string.find(auth_header, "Bearer%s+(.+)")
这段代码从 Authorization: Bearer <token>
头中提取 token 值。
3. 验证 API 调用
lua
local res, err = httpc:request_uri("https://auth-service.example.com/verify", {
method = "POST",
headers = { ... },
body = cjson.encode({ ... })
})
向验证服务发送 POST 请求,包含 token 和当前请求信息。
4. 响应处理
lua
if res.status ~= 200 then
ngx.exit(res.status == 401 and ngx.HTTP_UNAUTHORIZED or ngx.HTTP_FORBIDDEN)
end
根据验证服务的响应状态码决定是否放行请求。
高级优化建议
1. 添加缓存机制
lua
-- 在 access_by_lua_block 开头添加
local cache = ngx.shared.token_cache
local cached = cache:get(token)
if cached then
if cached == "valid" then
return -- 验证通过
else
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
end
-- 在验证成功后添加
cache:set(token, "valid", 300) -- 缓存5分钟
2. 超时和重试机制
lua
httpc:set_timeout({
connect = 500,
send = 500,
read = 1000
})
-- 添加重试逻辑
local retries = 3
while retries > 0 do
local res, err = httpc:request_uri(...)
if res then break end
retries = retries - 1
if retries == 0 then
ngx.log(ngx.ERR, "Auth service unreachable after retries")
ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
ngx.sleep(0.1) -- 短暂延迟后重试
end
3. 验证服务响应示例
验证服务应返回类似以下结构的 JSON 响应:
json
{
"valid": true,
"user_id": "12345",
"roles": ["admin", "user"],
"expires_in": 3600
}
4. 将Lua脚本提取成.lua文件
lua脚本从 nginx.config 配置文件剥离出来,更加直观也更易于维护。使用 access_by_lua_file 引用 token_filter.lua 文件:
lua
location /api/ {
default_type 'application/json';
charset utf-8;
lua_code_cache on;
access_by_lua_file /lua/token_filter.lua;
}
安全注意事项
- 始终使用 HTTPS:验证服务与 Nginx 之间的通信必须加密
- 验证 SSL 证书 :生产环境中应启用
ssl_verify
- 限制 Token 长度:防止过长的 token 导致缓冲区溢出
- 监控验证延迟:外部 API 调用会增加请求延迟
- 实施速率限制:防止暴力攻击验证端点
这种方案提供了极大的灵活性,可以根据你的具体认证需求进行调整,同时保持了良好的性能特征。
>>下面是一个完整的 Nginx Token 验证实例<<
可直接使用的配置实例,使用 Lua 脚本调用外部 API 验证 Token。我会分步骤解释每个部分的作用。
准备工作
- 确保已安装 OpenResty(带 Lua 支持的 Nginx)
- 准备一个验证 Token 的 API 接口(示例中使用
http://ip:port/gshr/gp3/commonApi/verifyToken
)
完整配置实例
打开文件 /usr/local/openresty/nginx/conf/nginx.conf
:
nginx
worker_processes 1;
events {
worker_connections 1024;
}
http {
# 共享内存区域用于缓存(可选)
lua_shared_dict token_cache 10m;
# 设置 Lua 包路径
lua_package_path '/usr/local/openresty/lualib/?.lua;;';
# 要转发到的实际后端服务
upstream gshrserver {
server 192.168.101.11:8888;
}
server {
listen 80;
server_name localhost;
# 示例受保护的应用路径
location /api/ {
# 使用 Lua 进行访问控制
access_by_lua_block {
-- 1. 获取 Authorization 头
local token = ngx.var.http_Authorization
-- 2. 检查是否有 Token
if not token then
ngx.log(ngx.WARN, "No Authorization header found")
ngx.header["WWW-Authenticate"] = "Bearer realm=\"Access to API\""
ngx.exit(401)
end
-- 3. 提取 Bearer Token (去掉 "Bearer " 前缀)
local jwt_token = string.match(token, "Bearer%s+(.+)")
if not jwt_token then
ngx.log(ngx.WARN, "Invalid Authorization format")
ngx.exit(401)
end
-- 4. 调用验证 API
local http = require "resty.http"
local httpc = http.new()
-- 设置请求参数
local res, err = httpc:request_uri("http://ip:port/gshr/gp3/commonApi/verifyToken", {
method = "GET",
headers = {
["Authorization"] = "Bearer " .. jwt_token,
["Content-Type"] = "application/json"
},
keepalive_timeout = 60,
keepalive_pool = 10
})
-- 5. 处理验证结果
if not res then
ngx.log(ngx.ERR, "Auth service request failed: ", err)
ngx.exit(500)
end
if res.status ~= 200 then
ngx.log(ngx.WARN, "Token validation failed: HTTP ", res.status)
ngx.exit(res.status) -- 返回验证服务的状态码
end
-- 6. 验证通过,可选添加用户信息到请求头
ngx.req.set_header("X-User-Validated", "true")
}
# 7. 验证通过后转发到实际后端
proxy_pass http://gshrserver/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 验证服务健康检查(可选)
location /auth-health {
proxy_pass http://ip:port/gshr/gp3/commonApi/health;
}
# 简单的公开访问路径
location / {
return 200 'Public access area. Use /api/ for protected resources.';
}
}
}
验证Token外部API
验证Token API示例 (Java)
Java文件 CommonAPIController.java
:
java
/**
* @Author: GP3
* @Description: nginx验证token
* @Date: 22:38 2025/6/16
* @Modified By:
*/
@ResponseBody
@RequestMapping(value = "${adminPath}/commonApi/verifyToken")
public void verifyToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
String Authorization = request.getHeader("Authorization");
if (StringUtils.isBlank(Authorization)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("No Authorization header found!");
return;
} else if (!Authorization.contains("Bearer ")){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid Authorization format!");
return;
} else {
boolean success = JWTUtil.verify(Authorization.split(" ")[1]);
if (!success) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("token校验失败或失效!");
return;
}
}
}
关键点解释
-
access_by_lua_block:在访问阶段执行 Lua 代码进行验证
-
resty.http:OpenResty 提供的 HTTP 客户端库,需要单独引入
-
验证流程:
- 检查请求头是否有 Authorization
- 提取 Bearer Token
- 调用验证服务检查 Token 有效性
- 根据结果允许/拒绝访问
-
proxy_pass:验证通过后转发到实际后端服务
这个完整实例可以直接使用,你只需要:
- 安装 OpenResty
- 根据示例编写 verifyToken 验证Token接口
- 修改配置中的服务地址(如果不用默认的 localhost)
OK! 打完收工😃