lolly/docs/lua/websocket/ws_handler.lua
xfy 6543422281 docs: 添加 Nginx 配置和 Lua 脚本示例文档
- config: 反向代理、缓存、负载均衡、安全、SSL 等配置模板
- lua: API 网关、认证、动态路由、限流、WebSocket 等脚本示例

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:59:22 +08:00

227 lines
6.5 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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,
}