- config: 反向代理、缓存、负载均衡、安全、SSL 等配置模板 - lua: API 网关、认证、动态路由、限流、WebSocket 等脚本示例 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
6.5 KiB
Lua
227 lines
6.5 KiB
Lua
-- WebSocket 处理器
|
||
-- 用于 Nginx Lua 沙箱环境中的 WebSocket 连接管理
|
||
--
|
||
-- 功能:
|
||
-- 1. 连接验证(Token 校验)
|
||
-- 2. 消息路由与处理
|
||
-- 3. 心跳保活
|
||
-- 4. 连接状态管理
|
||
|
||
local cjson = require "cjson"
|
||
|
||
-- ==========================================
|
||
-- 配置
|
||
-- ==========================================
|
||
local config = {
|
||
-- Token 验证
|
||
token_param = "token",
|
||
token_header = "X-WS-Token",
|
||
|
||
-- 心跳设置
|
||
heartbeat_interval = 30, -- 心跳间隔(秒)
|
||
heartbeat_timeout = 90, -- 心跳超时断开(秒)
|
||
heartbeat_opcode = 9, -- WebSocket Ping 操作码
|
||
|
||
-- 消息限制
|
||
max_message_size = 65536, -- 最大消息大小 64KB
|
||
max_connections_per_ip = 50, -- 单 IP 最大连接数
|
||
|
||
-- 日志
|
||
log_level = ngx.INFO,
|
||
}
|
||
|
||
-- ==========================================
|
||
-- 连接验证
|
||
-- ==========================================
|
||
local function validate_connection()
|
||
local dict = ngx.shared.ws_tokens
|
||
|
||
-- 优先从查询参数获取 Token
|
||
local token = ngx.var.arg_token
|
||
if not token then
|
||
-- 从请求头获取
|
||
token = ngx.req.get_headers()[config.token_header]
|
||
end
|
||
|
||
if not token or token == "" then
|
||
ngx.log(ngx.WARN, "WebSocket connection rejected: missing token, client=", ngx.var.remote_addr)
|
||
ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
||
return false
|
||
end
|
||
|
||
-- 验证 Token 有效性
|
||
local ok, err = dict:get(token)
|
||
if not ok then
|
||
ngx.log(ngx.WARN, "WebSocket connection rejected: invalid token, client=", ngx.var.remote_addr)
|
||
ngx.exit(ngx.HTTP_UNAUTHORIZED)
|
||
return false
|
||
end
|
||
|
||
-- 检查单 IP 连接数限制
|
||
local conn_key = "conn:" .. ngx.var.remote_addr
|
||
local count = dict:get(conn_key) or 0
|
||
if count >= config.max_connections_per_ip then
|
||
ngx.log(ngx.WARN, "WebSocket connection rejected: max connections exceeded, client=", ngx.var.remote_addr)
|
||
ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
|
||
return false
|
||
end
|
||
|
||
-- 记录连接
|
||
dict:incr(conn_key, 1, 0, config.heartbeat_timeout)
|
||
|
||
ngx.log(config.log_level, "WebSocket connection accepted, client=", ngx.var.remote_addr, " token=", token)
|
||
return true
|
||
end
|
||
|
||
-- ==========================================
|
||
-- 消息处理框架
|
||
-- ==========================================
|
||
local handlers = {}
|
||
|
||
-- 注册消息处理器
|
||
-- @param msg_type 消息类型字符串
|
||
-- @param handler_fn 处理函数 function(msg_data, ctx)
|
||
local function register_handler(msg_type, handler_fn)
|
||
handlers[msg_type] = handler_fn
|
||
end
|
||
|
||
-- 默认处理器:未注册的消息类型
|
||
local function default_handler(msg_data, ctx)
|
||
ngx.log(ngx.WARN, "Unhandled message type: ", msg_data.type, " from ", ctx.client_ip)
|
||
return {
|
||
status = "error",
|
||
message = "Unknown message type: " .. tostring(msg_data.type),
|
||
}
|
||
end
|
||
|
||
-- 分派消息到对应处理器
|
||
-- @param raw_msg 原始消息字符串
|
||
-- @param ctx 连接上下文
|
||
local function dispatch_message(raw_msg, ctx)
|
||
-- 检查消息大小
|
||
if #raw_msg > config.max_message_size then
|
||
return {
|
||
status = "error",
|
||
message = "Message too large (max " .. config.max_message_size .. " bytes)",
|
||
}
|
||
end
|
||
|
||
-- 解析 JSON 消息
|
||
local ok, msg_data = pcall(cjson.decode, raw_msg)
|
||
if not ok then
|
||
ngx.log(ngx.ERR, "Failed to decode message: ", msg_data)
|
||
return {
|
||
status = "error",
|
||
message = "Invalid JSON message",
|
||
}
|
||
end
|
||
|
||
if not msg_data.type then
|
||
return {
|
||
status = "error",
|
||
message = "Missing message type",
|
||
}
|
||
end
|
||
|
||
-- 查找并调用处理器
|
||
local handler = handlers[msg_data.type] or default_handler
|
||
local status, result = pcall(handler, msg_data, ctx)
|
||
if not status then
|
||
ngx.log(ngx.ERR, "Handler error for type '", msg_data.type, "': ", result)
|
||
return {
|
||
status = "error",
|
||
message = "Internal handler error",
|
||
}
|
||
end
|
||
|
||
return result
|
||
end
|
||
|
||
-- ==========================================
|
||
-- 内置消息处理器
|
||
-- ==========================================
|
||
|
||
-- 心跳响应
|
||
register_handler("ping", function(msg_data, ctx)
|
||
local dict = ngx.shared.ws_connections
|
||
dict:set(ctx.connection_id, ngx.time(), config.heartbeat_timeout)
|
||
|
||
return {
|
||
type = "pong",
|
||
timestamp = ngx.time(),
|
||
}
|
||
end)
|
||
|
||
-- 认证消息(连接后二次验证)
|
||
register_handler("auth", function(msg_data, ctx)
|
||
local token = msg_data.token
|
||
if not token then
|
||
return { status = "error", message = "Missing auth token" }
|
||
end
|
||
|
||
local dict = ngx.shared.ws_tokens
|
||
local valid = dict:get(token)
|
||
if not valid then
|
||
return { status = "error", message = "Invalid auth token" }
|
||
end
|
||
|
||
ctx.authenticated = true
|
||
ngx.log(config.log_level, "Client authenticated: ", ctx.client_ip)
|
||
|
||
return {
|
||
status = "ok",
|
||
message = "Authenticated successfully",
|
||
}
|
||
end)
|
||
|
||
-- 示例:Echo 处理器
|
||
register_handler("echo", function(msg_data, ctx)
|
||
return {
|
||
type = "echo",
|
||
data = msg_data.data,
|
||
timestamp = ngx.time(),
|
||
}
|
||
end)
|
||
|
||
-- ==========================================
|
||
-- 连接上下文管理
|
||
-- ==========================================
|
||
local function build_context()
|
||
local connection_id = ngx.md5(ngx.time() .. ngx.var.remote_addr .. ngx.var.request_id)
|
||
|
||
return {
|
||
connection_id = connection_id,
|
||
client_ip = ngx.var.remote_addr,
|
||
user_agent = ngx.var.http_user_agent,
|
||
connected_at = ngx.time(),
|
||
authenticated = false,
|
||
last_activity = ngx.time(),
|
||
}
|
||
end
|
||
|
||
-- ==========================================
|
||
-- 主入口(用于 access_by_lua_file)
|
||
-- ==========================================
|
||
-- 注意:实际的 WebSocket 消息循环需要在后端服务或使用
|
||
-- lua-resty-websocket 等库在 content 阶段处理。
|
||
-- 此处理器主要用于连接验证和预处理。
|
||
|
||
local ctx = build_context()
|
||
local valid = validate_connection()
|
||
|
||
if valid then
|
||
-- 连接验证通过,允许继续代理到后端
|
||
-- 实际的消息处理在后端 WebSocket 服务中进行
|
||
ngx.log(config.log_level, "WebSocket proxy authorized for ", ctx.connection_id)
|
||
end
|
||
|
||
-- 导出模块供其他 Lua 脚本使用
|
||
return {
|
||
config = config,
|
||
validate_connection = validate_connection,
|
||
dispatch_message = dispatch_message,
|
||
register_handler = register_handler,
|
||
build_context = build_context,
|
||
handlers = handlers,
|
||
}
|