1. 安全概述
1.1 Web安全威胁
OpenResty作为Web应用网关,面临多种安全威胁: - SQL注入:恶意SQL代码注入 - XSS攻击:跨站脚本攻击 - CSRF攻击:跨站请求伪造 - DDoS攻击:分布式拒绝服务 - 暴力破解:密码暴力破解 - 数据泄露:敏感信息泄露 - 会话劫持:会话令牌被盗用
1.2 OpenResty安全优势
- 高性能防护:基于Nginx的高并发处理
- 灵活规则:Lua脚本自定义安全策略
- 实时响应:毫秒级安全决策
- 可扩展性:模块化安全组件
- 集成能力:与现有安全系统集成
1.3 安全架构设计
┌─────────────────┐
│ 客户端请求 │
└─────────┬───────┘
│
┌─────────▼───────┐
│ WAF防护层 │ ← 恶意请求过滤
├─────────────────┤
│ 认证授权层 │ ← 身份验证
├─────────────────┤
│ 限流控制层 │ ← 流量控制
├─────────────────┤
│ 业务逻辑层 │ ← 应用处理
└─────────────────┘
2. 身份认证
2.1 基础认证(Basic Auth)
-- 基础认证模块
local basic_auth = {}
local resty_md5 = require "resty.md5"
local str = require "resty.string"
-- 用户数据库(实际应用中应从数据库或Redis获取)
local users = {
["admin"] = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", -- password: hello
["user1"] = "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f" -- password: secret
}
-- SHA256哈希函数
local function sha256(str)
local resty_sha256 = require "resty.sha256"
local sha256 = resty_sha256:new()
sha256:update(str)
local digest = sha256:final()
return require("resty.string").to_hex(digest)
end
-- 解析Authorization头
local function parse_auth_header(auth_header)
if not auth_header then
return nil, nil
end
local auth_type, credentials = string.match(auth_header, "^(%S+)%s+(.+)$")
if auth_type ~= "Basic" then
return nil, nil
end
-- Base64解码
local decoded = ngx.decode_base64(credentials)
if not decoded then
return nil, nil
end
local username, password = string.match(decoded, "^([^:]+):(.*)$")
return username, password
end
-- 验证用户凭据
function basic_auth.authenticate()
local auth_header = ngx.var.http_authorization
local username, password = parse_auth_header(auth_header)
if not username or not password then
ngx.header["WWW-Authenticate"] = 'Basic realm="Protected Area"'
ngx.status = 401
ngx.say("Authentication required")
ngx.exit(401)
end
-- 验证用户名和密码
local stored_hash = users[username]
if not stored_hash then
ngx.log(ngx.WARN, "Unknown user: ", username)
ngx.header["WWW-Authenticate"] = 'Basic realm="Protected Area"'
ngx.status = 401
ngx.say("Invalid credentials")
ngx.exit(401)
end
local password_hash = sha256(password)
if password_hash ~= stored_hash then
ngx.log(ngx.WARN, "Invalid password for user: ", username)
ngx.header["WWW-Authenticate"] = 'Basic realm="Protected Area"'
ngx.status = 401
ngx.say("Invalid credentials")
ngx.exit(401)
end
-- 认证成功,设置用户信息
ngx.var.authenticated_user = username
ngx.log(ngx.INFO, "User authenticated: ", username)
end
-- 检查用户权限
function basic_auth.check_permission(required_role)
local username = ngx.var.authenticated_user
if not username then
ngx.status = 401
ngx.say("Not authenticated")
ngx.exit(401)
end
-- 简单的角色检查(实际应用中应从数据库获取)
local user_roles = {
["admin"] = {"admin", "user"},
["user1"] = {"user"}
}
local roles = user_roles[username] or {}
for _, role in ipairs(roles) do
if role == required_role then
return true
end
end
ngx.status = 403
ngx.say("Insufficient permissions")
ngx.exit(403)
end
return basic_auth
2.2 JWT认证
-- JWT认证模块
local jwt_auth = {}
local jwt = require "resty.jwt"
local cjson = require "cjson"
-- JWT配置
local jwt_config = {
secret = "your-secret-key-here", -- 应该从环境变量或配置文件读取
algorithm = "HS256",
exp = 3600, -- 1小时过期
issuer = "openresty-app"
}
-- 生成JWT令牌
function jwt_auth.generate_token(user_data)
local now = ngx.time()
local payload = {
iss = jwt_config.issuer,
sub = user_data.username,
iat = now,
exp = now + jwt_config.exp,
user_id = user_data.user_id,
username = user_data.username,
roles = user_data.roles or {}
}
local token = jwt:sign(jwt_config.secret, {
header = {
typ = "JWT",
alg = jwt_config.algorithm
},
payload = payload
})
return token
end
-- 验证JWT令牌
function jwt_auth.verify_token(token)
if not token then
return nil, "Token not provided"
end
local jwt_obj = jwt:verify(jwt_config.secret, token)
if not jwt_obj.valid then
return nil, "Invalid token: " .. (jwt_obj.reason or "unknown error")
end
local payload = jwt_obj.payload
-- 检查过期时间
if payload.exp and payload.exp < ngx.time() then
return nil, "Token expired"
end
-- 检查发行者
if payload.iss ~= jwt_config.issuer then
return nil, "Invalid issuer"
end
return payload
end
-- 从请求中提取令牌
local function extract_token()
-- 从Authorization头提取
local auth_header = ngx.var.http_authorization
if auth_header then
local token = string.match(auth_header, "^Bearer%s+(.+)$")
if token then
return token
end
end
-- 从查询参数提取
local args = ngx.req.get_uri_args()
if args.token then
return args.token
end
-- 从Cookie提取
local cookie_token = ngx.var.cookie_access_token
if cookie_token then
return cookie_token
end
return nil
end
-- JWT认证中间件
function jwt_auth.authenticate()
local token = extract_token()
if not token then
ngx.status = 401
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "unauthorized",
message = "Access token required"
}))
ngx.exit(401)
end
local payload, err = jwt_auth.verify_token(token)
if not payload then
ngx.log(ngx.WARN, "JWT verification failed: ", err)
ngx.status = 401
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "unauthorized",
message = err
}))
ngx.exit(401)
end
-- 设置用户上下文
ngx.ctx.user = {
user_id = payload.user_id,
username = payload.username,
roles = payload.roles
}
ngx.log(ngx.INFO, "JWT authentication successful for user: ", payload.username)
end
-- 检查用户角色
function jwt_auth.require_role(required_role)
local user = ngx.ctx.user
if not user then
ngx.status = 401
ngx.say("Not authenticated")
ngx.exit(401)
end
local roles = user.roles or {}
for _, role in ipairs(roles) do
if role == required_role then
return true
end
end
ngx.status = 403
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "forbidden",
message = "Insufficient permissions"
}))
ngx.exit(403)
end
-- 刷新令牌
function jwt_auth.refresh_token(old_token)
local payload, err = jwt_auth.verify_token(old_token)
if not payload then
return nil, err
end
-- 检查令牌是否即将过期(剩余时间少于30分钟)
local remaining_time = payload.exp - ngx.time()
if remaining_time > 1800 then -- 30分钟
return old_token -- 不需要刷新
end
-- 生成新令牌
local user_data = {
user_id = payload.user_id,
username = payload.username,
roles = payload.roles
}
return jwt_auth.generate_token(user_data)
end
return jwt_auth
2.3 OAuth 2.0集成
-- OAuth 2.0客户端模块
local oauth2 = {}
local http = require "resty.http"
local cjson = require "cjson"
local jwt_auth = require "jwt_auth"
-- OAuth配置
local oauth_config = {
google = {
client_id = "your-google-client-id",
client_secret = "your-google-client-secret",
redirect_uri = "https://your-domain.com/auth/google/callback",
auth_url = "https://accounts.google.com/o/oauth2/v2/auth",
token_url = "https://oauth2.googleapis.com/token",
user_info_url = "https://www.googleapis.com/oauth2/v2/userinfo",
scope = "openid email profile"
},
github = {
client_id = "your-github-client-id",
client_secret = "your-github-client-secret",
redirect_uri = "https://your-domain.com/auth/github/callback",
auth_url = "https://github.com/login/oauth/authorize",
token_url = "https://github.com/login/oauth/access_token",
user_info_url = "https://api.github.com/user",
scope = "user:email"
}
}
-- 生成授权URL
function oauth2.get_auth_url(provider, state)
local config = oauth_config[provider]
if not config then
return nil, "Unsupported provider: " .. provider
end
local params = {
client_id = config.client_id,
redirect_uri = config.redirect_uri,
scope = config.scope,
response_type = "code",
state = state or ngx.time()
}
local query_string = ngx.encode_args(params)
return config.auth_url .. "?" .. query_string
end
-- 交换授权码获取访问令牌
function oauth2.exchange_code(provider, code)
local config = oauth_config[provider]
if not config then
return nil, "Unsupported provider: " .. provider
end
local httpc = http.new()
httpc:set_timeout(10000) -- 10秒超时
local params = {
client_id = config.client_id,
client_secret = config.client_secret,
code = code,
grant_type = "authorization_code",
redirect_uri = config.redirect_uri
}
local res, err = httpc:request_uri(config.token_url, {
method = "POST",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Accept"] = "application/json"
},
body = ngx.encode_args(params)
})
if not res then
return nil, "HTTP request failed: " .. err
end
if res.status ~= 200 then
return nil, "OAuth token exchange failed: " .. res.status
end
local token_data = cjson.decode(res.body)
return token_data
end
-- 获取用户信息
function oauth2.get_user_info(provider, access_token)
local config = oauth_config[provider]
if not config then
return nil, "Unsupported provider: " .. provider
end
local httpc = http.new()
httpc:set_timeout(10000)
local res, err = httpc:request_uri(config.user_info_url, {
method = "GET",
headers = {
["Authorization"] = "Bearer " .. access_token,
["Accept"] = "application/json"
}
})
if not res then
return nil, "HTTP request failed: " .. err
end
if res.status ~= 200 then
return nil, "Failed to get user info: " .. res.status
end
local user_info = cjson.decode(res.body)
return user_info
end
-- OAuth认证流程
function oauth2.authenticate(provider)
local args = ngx.req.get_uri_args()
local code = args.code
local state = args.state
if not code then
-- 重定向到OAuth提供商
local auth_url = oauth2.get_auth_url(provider, ngx.time())
ngx.redirect(auth_url)
return
end
-- 交换授权码
local token_data, err = oauth2.exchange_code(provider, code)
if not token_data then
ngx.log(ngx.ERR, "OAuth code exchange failed: ", err)
ngx.status = 400
ngx.say("Authentication failed")
ngx.exit(400)
end
-- 获取用户信息
local user_info, err = oauth2.get_user_info(provider, token_data.access_token)
if not user_info then
ngx.log(ngx.ERR, "Failed to get user info: ", err)
ngx.status = 400
ngx.say("Authentication failed")
ngx.exit(400)
end
-- 创建本地用户会话
local user_data = {
user_id = user_info.id or user_info.login,
username = user_info.name or user_info.login,
email = user_info.email,
provider = provider,
roles = {"user"} -- 默认角色
}
-- 生成JWT令牌
local jwt_token = jwt_auth.generate_token(user_data)
-- 设置Cookie
ngx.header["Set-Cookie"] = "access_token=" .. jwt_token .. "; Path=/; HttpOnly; Secure; SameSite=Strict"
-- 重定向到应用首页
ngx.redirect("/dashboard")
end
return oauth2
3. 访问控制
3.1 基于角色的访问控制(RBAC)
-- RBAC访问控制模块
local rbac = {}
local cjson = require "cjson"
-- 权限定义
local permissions = {
["user.read"] = "读取用户信息",
["user.write"] = "修改用户信息",
["user.delete"] = "删除用户",
["admin.read"] = "读取管理信息",
["admin.write"] = "修改系统配置",
["api.read"] = "调用只读API",
["api.write"] = "调用写入API"
}
-- 角色权限映射
local role_permissions = {
["guest"] = {"api.read"},
["user"] = {"user.read", "api.read"},
["moderator"] = {"user.read", "user.write", "api.read", "api.write"},
["admin"] = {"user.read", "user.write", "user.delete", "admin.read", "admin.write", "api.read", "api.write"}
}
-- 资源权限映射
local resource_permissions = {
["/api/users"] = {
["GET"] = "user.read",
["POST"] = "user.write",
["PUT"] = "user.write",
["DELETE"] = "user.delete"
},
["/api/admin"] = {
["GET"] = "admin.read",
["POST"] = "admin.write",
["PUT"] = "admin.write",
["DELETE"] = "admin.write"
},
["/api/public"] = {
["GET"] = "api.read"
}
}
-- 获取用户权限
function rbac.get_user_permissions(user_roles)
local user_permissions = {}
for _, role in ipairs(user_roles) do
local role_perms = role_permissions[role] or {}
for _, perm in ipairs(role_perms) do
user_permissions[perm] = true
end
end
return user_permissions
end
-- 检查用户是否有特定权限
function rbac.has_permission(user_roles, required_permission)
local user_permissions = rbac.get_user_permissions(user_roles)
return user_permissions[required_permission] == true
end
-- 检查资源访问权限
function rbac.check_resource_access(user_roles, resource_path, method)
-- 查找匹配的资源模式
local required_permission = nil
for pattern, methods in pairs(resource_permissions) do
if string.match(resource_path, pattern) then
required_permission = methods[method]
break
end
end
if not required_permission then
-- 如果没有定义权限,默认拒绝访问
return false, "No permission defined for resource"
end
if rbac.has_permission(user_roles, required_permission) then
return true
else
return false, "Insufficient permissions: " .. required_permission
end
end
-- RBAC中间件
function rbac.middleware()
local user = ngx.ctx.user
if not user then
ngx.status = 401
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "unauthorized",
message = "Authentication required"
}))
ngx.exit(401)
end
local resource_path = ngx.var.uri
local method = ngx.var.request_method
local user_roles = user.roles or {"guest"}
local has_access, err = rbac.check_resource_access(user_roles, resource_path, method)
if not has_access then
ngx.log(ngx.WARN, "Access denied for user ", user.username, " to ", resource_path, ": ", err)
ngx.status = 403
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "forbidden",
message = err
}))
ngx.exit(403)
end
ngx.log(ngx.INFO, "Access granted for user ", user.username, " to ", resource_path)
end
-- 动态权限检查
function rbac.check_dynamic_permission(permission_name)
local user = ngx.ctx.user
if not user then
return false
end
local user_roles = user.roles or {"guest"}
return rbac.has_permission(user_roles, permission_name)
end
return rbac
3.2 IP白名单和黑名单
-- IP访问控制模块
local ip_control = {}
local iputils = require "resty.iputils"
-- IP白名单(CIDR格式)
local whitelist = {
"127.0.0.1/32", -- 本地回环
"10.0.0.0/8", -- 私有网络A类
"172.16.0.0/12", -- 私有网络B类
"192.168.0.0/16", -- 私有网络C类
"203.0.113.0/24" -- 示例公网段
}
-- IP黑名单
local blacklist = {
"192.0.2.0/24", -- 示例恶意IP段
"198.51.100.0/24" -- 示例恶意IP段
}
-- 解析IP列表
local function parse_ip_list(ip_list)
local parsed = {}
for _, ip_range in ipairs(ip_list) do
table.insert(parsed, iputils.parse_cidr(ip_range))
end
return parsed
end
-- 预解析IP列表
local parsed_whitelist = parse_ip_list(whitelist)
local parsed_blacklist = parse_ip_list(blacklist)
-- 检查IP是否在列表中
local function ip_in_list(ip, parsed_list)
for _, cidr in ipairs(parsed_list) do
if iputils.ip_in_cidrs(ip, {cidr}) then
return true
end
end
return false
end
-- 获取真实客户端IP
function ip_control.get_real_ip()
-- 检查X-Forwarded-For头(代理环境)
local xff = ngx.var.http_x_forwarded_for
if xff then
local first_ip = string.match(xff, "([^,]+)")
if first_ip then
return string.gsub(first_ip, "%s+", "")
end
end
-- 检查X-Real-IP头
local real_ip = ngx.var.http_x_real_ip
if real_ip then
return real_ip
end
-- 使用直接连接IP
return ngx.var.remote_addr
end
-- 白名单检查
function ip_control.check_whitelist()
local client_ip = ip_control.get_real_ip()
if ip_in_list(client_ip, parsed_whitelist) then
ngx.log(ngx.INFO, "IP ", client_ip, " is in whitelist")
return true
else
ngx.log(ngx.WARN, "IP ", client_ip, " is not in whitelist")
ngx.status = 403
ngx.say("Access denied: IP not in whitelist")
ngx.exit(403)
end
end
-- 黑名单检查
function ip_control.check_blacklist()
local client_ip = ip_control.get_real_ip()
if ip_in_list(client_ip, parsed_blacklist) then
ngx.log(ngx.WARN, "IP ", client_ip, " is in blacklist")
ngx.status = 403
ngx.say("Access denied: IP blacklisted")
ngx.exit(403)
else
ngx.log(ngx.INFO, "IP ", client_ip, " is not in blacklist")
return true
end
end
-- 地理位置检查
function ip_control.check_geo_location(allowed_countries)
local client_ip = ip_control.get_real_ip()
-- 这里需要集成GeoIP数据库,如MaxMind GeoLite2
-- 示例实现(需要安装lua-resty-maxminddb)
local maxminddb = require "resty.maxminddb"
if not maxminddb.initted() then
maxminddb.init("/path/to/GeoLite2-Country.mmdb")
end
local res, err = maxminddb.lookup(client_ip)
if not res then
ngx.log(ngx.WARN, "GeoIP lookup failed for ", client_ip, ": ", err)
return true -- 默认允许访问
end
local country_code = res.country and res.country.iso_code
if not country_code then
ngx.log(ngx.WARN, "No country code found for IP ", client_ip)
return true
end
-- 检查是否在允许的国家列表中
for _, allowed_country in ipairs(allowed_countries) do
if country_code == allowed_country then
ngx.log(ngx.INFO, "IP ", client_ip, " from allowed country: ", country_code)
return true
end
end
ngx.log(ngx.WARN, "IP ", client_ip, " from blocked country: ", country_code)
ngx.status = 403
ngx.say("Access denied: Geographic restriction")
ngx.exit(403)
end
-- 动态IP控制(基于Redis)
function ip_control.dynamic_check()
local redis = require "resty.redis"
local client_ip = ip_control.get_real_ip()
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return true -- 默认允许访问
end
-- 检查IP是否在动态黑名单中
local is_blocked = red:get("blocked_ip:" .. client_ip)
if is_blocked and is_blocked ~= ngx.null then
ngx.log(ngx.WARN, "IP ", client_ip, " is dynamically blocked")
red:set_keepalive(10000, 100)
ngx.status = 403
ngx.say("Access denied: IP temporarily blocked")
ngx.exit(403)
end
red:set_keepalive(10000, 100)
return true
end
return ip_control
4. 限流与防护
4.1 请求限流
-- 限流模块
local rate_limiter = {}
local limit_req = require "resty.limit.req"
local limit_conn = require "resty.limit.conn"
-- 限流配置
local rate_limit_config = {
-- 全局限流:每秒100个请求,突发200个
global = {
rate = 100,
burst = 200,
delay = 50
},
-- API限流:每秒50个请求
api = {
rate = 50,
burst = 100,
delay = 25
},
-- 用户限流:每个用户每秒10个请求
user = {
rate = 10,
burst = 20,
delay = 5
}
}
-- 创建限流器实例
local limiters = {}
local function get_limiter(limiter_type)
if not limiters[limiter_type] then
local config = rate_limit_config[limiter_type]
if config then
limiters[limiter_type] = limit_req.new("rate_limit_" .. limiter_type, config.rate, config.burst)
end
end
return limiters[limiter_type]
end
-- 全局限流
function rate_limiter.global_limit()
local limiter = get_limiter("global")
if not limiter then
return
end
local key = "global"
local delay, err = limiter:incoming(key, true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "Global rate limit exceeded")
ngx.status = 429
ngx.header["Retry-After"] = "1"
ngx.say("Too Many Requests")
ngx.exit(429)
else
ngx.log(ngx.ERR, "Rate limiter error: ", err)
end
return
end
if delay > 0 then
ngx.sleep(delay)
end
end
-- 基于IP的限流
function rate_limiter.ip_limit()
local limiter = get_limiter("api")
if not limiter then
return
end
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
local delay, err = limiter:incoming(client_ip, true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "IP rate limit exceeded for ", client_ip)
ngx.status = 429
ngx.header["Retry-After"] = "1"
ngx.say("Too Many Requests")
ngx.exit(429)
else
ngx.log(ngx.ERR, "Rate limiter error: ", err)
end
return
end
if delay > 0 then
ngx.sleep(delay)
end
end
-- 基于用户的限流
function rate_limiter.user_limit()
local user = ngx.ctx.user
if not user then
return -- 未认证用户不进行用户级限流
end
local limiter = get_limiter("user")
if not limiter then
return
end
local user_key = "user:" .. user.user_id
local delay, err = limiter:incoming(user_key, true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "User rate limit exceeded for ", user.username)
ngx.status = 429
ngx.header["Retry-After"] = "1"
ngx.say("Too Many Requests")
ngx.exit(429)
else
ngx.log(ngx.ERR, "Rate limiter error: ", err)
end
return
end
if delay > 0 then
ngx.sleep(delay)
end
end
-- 连接数限制
function rate_limiter.connection_limit(max_connections)
max_connections = max_connections or 100
local limiter = limit_conn.new("connection_limit", max_connections, 0, 0.5)
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
local delay, err = limiter:incoming(client_ip, true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "Connection limit exceeded for ", client_ip)
ngx.status = 503
ngx.say("Service Temporarily Unavailable")
ngx.exit(503)
else
ngx.log(ngx.ERR, "Connection limiter error: ", err)
end
return
end
if delay > 0 then
ngx.sleep(delay)
end
end
-- 自适应限流
function rate_limiter.adaptive_limit()
local stats_cache = ngx.shared.stats
if not stats_cache then
return
end
-- 获取系统负载指标
local cpu_usage = stats_cache:get("cpu_usage") or 0
local memory_usage = stats_cache:get("memory_usage") or 0
local response_time = stats_cache:get("avg_response_time") or 0
-- 根据系统负载调整限流阈值
local base_rate = 100
local rate_factor = 1.0
if cpu_usage > 80 or memory_usage > 80 then
rate_factor = 0.5 -- 高负载时减少50%
elseif response_time > 1000 then -- 响应时间超过1秒
rate_factor = 0.7 -- 减少30%
elseif cpu_usage < 30 and memory_usage < 30 then
rate_factor = 1.5 -- 低负载时增加50%
end
local adjusted_rate = math.floor(base_rate * rate_factor)
-- 使用调整后的限流率
local limiter = limit_req.new("adaptive_limit", adjusted_rate, adjusted_rate * 2)
local delay, err = limiter:incoming("adaptive", true)
if not delay then
if err == "rejected" then
ngx.log(ngx.WARN, "Adaptive rate limit exceeded (rate: ", adjusted_rate, ")")
ngx.status = 429
ngx.say("Too Many Requests")
ngx.exit(429)
end
end
if delay > 0 then
ngx.sleep(delay)
end
end
return rate_limiter
4.2 DDoS防护
-- DDoS防护模块
local ddos_protection = {}
local redis = require "resty.redis"
local cjson = require "cjson"
-- DDoS检测配置
local ddos_config = {
-- 检测窗口(秒)
window_size = 60,
-- 阈值配置
thresholds = {
requests_per_ip = 1000, -- 单IP每分钟请求数
total_requests = 10000, -- 总请求数每分钟
error_rate = 0.5, -- 错误率阈值
new_ips_rate = 100 -- 新IP数量每分钟
},
-- 封禁时间(秒)
ban_duration = 300,
-- 挑战模式持续时间
challenge_duration = 60
}
-- 获取Redis连接
local function get_redis()
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return nil
end
return red
end
-- 记录请求统计
function ddos_protection.record_request()
local red = get_redis()
if not red then
return
end
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
local current_time = ngx.time()
local window_key = math.floor(current_time / ddos_config.window_size)
-- 记录IP请求数
local ip_key = "ddos:ip:" .. window_key .. ":" .. client_ip
red:incr(ip_key)
red:expire(ip_key, ddos_config.window_size * 2)
-- 记录总请求数
local total_key = "ddos:total:" .. window_key
red:incr(total_key)
red:expire(total_key, ddos_config.window_size * 2)
-- 记录新IP
local new_ip_key = "ddos:new_ip:" .. window_key
local ip_seen_key = "ddos:ip_seen:" .. client_ip
local is_new = red:get(ip_seen_key)
if not is_new or is_new == ngx.null then
red:incr(new_ip_key)
red:expire(new_ip_key, ddos_config.window_size * 2)
red:setex(ip_seen_key, 3600, "1") -- 标记IP已见过,1小时过期
end
red:set_keepalive(10000, 100)
end
-- 记录错误响应
function ddos_protection.record_error()
local red = get_redis()
if not red then
return
end
local current_time = ngx.time()
local window_key = math.floor(current_time / ddos_config.window_size)
local error_key = "ddos:errors:" .. window_key
red:incr(error_key)
red:expire(error_key, ddos_config.window_size * 2)
red:set_keepalive(10000, 100)
end
-- 检测DDoS攻击
function ddos_protection.detect_attack()
local red = get_redis()
if not red then
return false
end
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
local current_time = ngx.time()
local window_key = math.floor(current_time / ddos_config.window_size)
-- 检查IP请求频率
local ip_key = "ddos:ip:" .. window_key .. ":" .. client_ip
local ip_requests = red:get(ip_key) or 0
if tonumber(ip_requests) > ddos_config.thresholds.requests_per_ip then
ngx.log(ngx.WARN, "DDoS detected: High request rate from IP ", client_ip, " (", ip_requests, " requests)")
ddos_protection.ban_ip(client_ip, "high_request_rate")
red:set_keepalive(10000, 100)
return true
end
-- 检查总请求数
local total_key = "ddos:total:" .. window_key
local total_requests = red:get(total_key) or 0
if tonumber(total_requests) > ddos_config.thresholds.total_requests then
ngx.log(ngx.WARN, "DDoS detected: High total request rate (", total_requests, " requests)")
ddos_protection.enable_challenge_mode()
red:set_keepalive(10000, 100)
return true
end
-- 检查错误率
local error_key = "ddos:errors:" .. window_key
local error_count = red:get(error_key) or 0
local error_rate = tonumber(total_requests) > 0 and tonumber(error_count) / tonumber(total_requests) or 0
if error_rate > ddos_config.thresholds.error_rate then
ngx.log(ngx.WARN, "DDoS detected: High error rate (", error_rate * 100, "%)")
ddos_protection.enable_challenge_mode()
red:set_keepalive(10000, 100)
return true
end
red:set_keepalive(10000, 100)
return false
end
-- 封禁IP
function ddos_protection.ban_ip(ip, reason)
local red = get_redis()
if not red then
return
end
local ban_key = "ddos:banned:" .. ip
local ban_info = {
reason = reason,
banned_at = ngx.time(),
expires_at = ngx.time() + ddos_config.ban_duration
}
red:setex(ban_key, ddos_config.ban_duration, cjson.encode(ban_info))
red:set_keepalive(10000, 100)
ngx.log(ngx.WARN, "IP ", ip, " banned for ", ddos_config.ban_duration, " seconds. Reason: ", reason)
end
-- 检查IP是否被封禁
function ddos_protection.is_ip_banned(ip)
local red = get_redis()
if not red then
return false
end
local ban_key = "ddos:banned:" .. ip
local ban_info = red:get(ban_key)
red:set_keepalive(10000, 100)
if ban_info and ban_info ~= ngx.null then
local ban_data = cjson.decode(ban_info)
if ban_data.expires_at > ngx.time() then
return true, ban_data
end
end
return false
end
-- 启用挑战模式
function ddos_protection.enable_challenge_mode()
local red = get_redis()
if not red then
return
end
local challenge_key = "ddos:challenge_mode"
red:setex(challenge_key, ddos_config.challenge_duration, "1")
red:set_keepalive(10000, 100)
ngx.log(ngx.WARN, "Challenge mode enabled for ", ddos_config.challenge_duration, " seconds")
end
-- 检查是否处于挑战模式
function ddos_protection.is_challenge_mode()
local red = get_redis()
if not red then
return false
end
local challenge_key = "ddos:challenge_mode"
local is_challenge = red:get(challenge_key)
red:set_keepalive(10000, 100)
return is_challenge and is_challenge ~= ngx.null
end
-- JavaScript挑战
function ddos_protection.javascript_challenge()
local challenge_html = [[
<!DOCTYPE html>
<html>
<head>
<title>Security Check</title>
<meta charset="utf-8">
</head>
<body>
<h1>Security Check</h1>
<p>Please wait while we verify your request...</p>
<script>
// 简单的JavaScript挑战
var challenge = Math.floor(Math.random() * 1000000);
var answer = challenge * 2 + 1;
setTimeout(function() {
var form = document.createElement('form');
form.method = 'POST';
form.action = window.location.href;
var challengeInput = document.createElement('input');
challengeInput.type = 'hidden';
challengeInput.name = 'challenge';
challengeInput.value = challenge;
var answerInput = document.createElement('input');
answerInput.type = 'hidden';
answerInput.name = 'answer';
answerInput.value = answer;
form.appendChild(challengeInput);
form.appendChild(answerInput);
document.body.appendChild(form);
form.submit();
}, 2000);
</script>
</body>
</html>
]]
ngx.header.content_type = "text/html"
ngx.say(challenge_html)
ngx.exit(200)
end
-- 验证JavaScript挑战
function ddos_protection.verify_challenge()
ngx.req.read_body()
local args = ngx.req.get_post_args()
local challenge = tonumber(args.challenge)
local answer = tonumber(args.answer)
if not challenge or not answer then
return false
end
local expected_answer = challenge * 2 + 1
if answer == expected_answer then
-- 挑战成功,设置通过标记
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
local red = get_redis()
if red then
local pass_key = "ddos:challenge_passed:" .. client_ip
red:setex(pass_key, 3600, "1") -- 1小时内免挑战
red:set_keepalive(10000, 100)
end
return true
end
return false
end
-- DDoS防护中间件
function ddos_protection.middleware()
local ip_control = require "ip_control"
local client_ip = ip_control.get_real_ip()
-- 检查IP是否被封禁
local is_banned, ban_info = ddos_protection.is_ip_banned(client_ip)
if is_banned then
ngx.log(ngx.WARN, "Blocked request from banned IP: ", client_ip)
ngx.status = 403
ngx.say("Access denied: IP temporarily blocked")
ngx.exit(403)
end
-- 记录请求
ddos_protection.record_request()
-- 检测攻击
if ddos_protection.detect_attack() then
-- 攻击已被处理(IP封禁或启用挑战模式)
return
end
-- 检查是否处于挑战模式
if ddos_protection.is_challenge_mode() then
-- 检查是否已通过挑战
local red = get_redis()
if red then
local pass_key = "ddos:challenge_passed:" .. client_ip
local has_passed = red:get(pass_key)
red:set_keepalive(10000, 100)
if not has_passed or has_passed == ngx.null then
-- 需要进行挑战验证
if ngx.var.request_method == "POST" then
if ddos_protection.verify_challenge() then
-- 挑战成功,继续处理请求
return
else
-- 挑战失败
ddos_protection.javascript_challenge()
end
else
-- 显示挑战页面
ddos_protection.javascript_challenge()
end
end
end
end
end
return ddos_protection
5. 安全配置示例
5.1 Nginx配置
# nginx.conf安全配置
http {
# 隐藏Nginx版本信息
server_tokens off;
# 安全头设置
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'";
# 限制请求大小
client_max_body_size 10m;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
# 超时设置
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
# 共享内存配置
lua_shared_dict rate_limit_global 10m;
lua_shared_dict rate_limit_api 10m;
lua_shared_dict rate_limit_user 10m;
lua_shared_dict connection_limit 10m;
lua_shared_dict stats 20m;
lua_shared_dict locks 10m;
# 初始化脚本
init_by_lua_block {
-- 加载安全模块
require "resty.core"
-- 初始化安全配置
local security_config = {
enable_ddos_protection = true,
enable_rate_limiting = true,
enable_ip_filtering = true,
enable_geo_blocking = false
}
ngx.shared.stats:set("security_config", require("cjson").encode(security_config))
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL配置
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全检查
access_by_lua_block {
-- DDoS防护
local ddos_protection = require "ddos_protection"
ddos_protection.middleware()
-- IP访问控制
local ip_control = require "ip_control"
ip_control.check_blacklist()
-- 限流控制
local rate_limiter = require "rate_limiter"
rate_limiter.global_limit()
rate_limiter.ip_limit()
}
# API路由
location /api/ {
access_by_lua_block {
-- JWT认证
local jwt_auth = require "jwt_auth"
jwt_auth.authenticate()
-- RBAC权限检查
local rbac = require "rbac"
rbac.middleware()
-- 用户级限流
local rate_limiter = require "rate_limiter"
rate_limiter.user_limit()
}
# 代理到后端
proxy_pass http://backend;
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;
}
# 管理员路由
location /admin/ {
access_by_lua_block {
-- IP白名单检查
local ip_control = require "ip_control"
ip_control.check_whitelist()
-- JWT认证
local jwt_auth = require "jwt_auth"
jwt_auth.authenticate()
-- 管理员权限检查
jwt_auth.require_role("admin")
}
proxy_pass http://admin_backend;
}
# 认证路由
location /auth/ {
content_by_lua_block {
local uri = ngx.var.uri
if uri == "/auth/login" then
-- 处理登录
local login_handler = require "login_handler"
login_handler.handle_login()
elseif string.match(uri, "/auth/oauth/(.+)") then
-- OAuth认证
local provider = string.match(uri, "/auth/oauth/(.+)")
local oauth2 = require "oauth2"
oauth2.authenticate(provider)
else
ngx.status = 404
ngx.say("Not found")
end
}
}
# 错误处理
log_by_lua_block {
-- 记录错误响应
if ngx.status >= 400 then
local ddos_protection = require "ddos_protection"
ddos_protection.record_error()
end
}
}
# HTTP重定向到HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
}
5.2 登录处理器
-- 登录处理模块
local login_handler = {}
local cjson = require "cjson"
local jwt_auth = require "jwt_auth"
local mysql = require "resty.mysql"
-- 用户验证
local function verify_user(username, password)
-- 连接数据库
local db, err = mysql:new()
if not db then
return nil, "Database connection failed: " .. err
end
db:set_timeout(1000)
local ok, err = db:connect({
host = "127.0.0.1",
port = 3306,
database = "auth_db",
user = "auth_user",
password = "auth_password",
charset = "utf8",
max_packet_size = 1024 * 1024
})
if not ok then
return nil, "Database connection failed: " .. err
end
-- 查询用户
local sql = "SELECT id, username, password_hash, salt, roles, status FROM users WHERE username = ? AND status = 'active'"
local res, err = db:query(sql, username)
if not res then
db:close()
return nil, "Database query failed: " .. err
end
if #res == 0 then
db:close()
return nil, "User not found or inactive"
end
local user = res[1]
-- 验证密码
local resty_sha256 = require "resty.sha256"
local sha256 = resty_sha256:new()
sha256:update(password .. user.salt)
local password_hash = require("resty.string").to_hex(sha256:final())
if password_hash ~= user.password_hash then
db:close()
return nil, "Invalid password"
end
db:close()
return {
user_id = user.id,
username = user.username,
roles = cjson.decode(user.roles or '[]')
}
end
-- 处理登录请求
function login_handler.handle_login()
if ngx.var.request_method ~= "POST" then
ngx.status = 405
ngx.say("Method not allowed")
ngx.exit(405)
end
ngx.req.read_body()
local body = ngx.req.get_body_data()
if not body then
ngx.status = 400
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "bad_request",
message = "Request body required"
}))
ngx.exit(400)
end
local login_data = cjson.decode(body)
local username = login_data.username
local password = login_data.password
if not username or not password then
ngx.status = 400
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "bad_request",
message = "Username and password required"
}))
ngx.exit(400)
end
-- 验证用户
local user, err = verify_user(username, password)
if not user then
ngx.log(ngx.WARN, "Login failed for user ", username, ": ", err)
ngx.status = 401
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "unauthorized",
message = "Invalid credentials"
}))
ngx.exit(401)
end
-- 生成JWT令牌
local token = jwt_auth.generate_token(user)
ngx.log(ngx.INFO, "User ", username, " logged in successfully")
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
success = true,
token = token,
user = {
user_id = user.user_id,
username = user.username,
roles = user.roles
}
}))
end
return login_handler
6. 监控与日志
6.1 安全事件监控
-- 安全监控模块
local security_monitor = {}
local cjson = require "cjson"
local redis = require "resty.redis"
-- 安全事件类型
local event_types = {
LOGIN_SUCCESS = "login_success",
LOGIN_FAILURE = "login_failure",
ACCESS_DENIED = "access_denied",
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded",
IP_BANNED = "ip_banned",
DDOS_DETECTED = "ddos_detected",
SUSPICIOUS_ACTIVITY = "suspicious_activity"
}
-- 记录安全事件
function security_monitor.log_event(event_type, details)
local event = {
timestamp = ngx.time(),
event_type = event_type,
ip = require("ip_control").get_real_ip(),
user_agent = ngx.var.http_user_agent,
uri = ngx.var.uri,
method = ngx.var.request_method,
details = details or {}
}
-- 记录到Nginx日志
ngx.log(ngx.WARN, "SECURITY_EVENT: ", cjson.encode(event))
-- 发送到Redis队列用于实时处理
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if ok then
red:lpush("security_events", cjson.encode(event))
red:set_keepalive(10000, 100)
end
end
-- 检测异常行为
function security_monitor.detect_anomaly()
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
return
end
local client_ip = require("ip_control").get_real_ip()
local current_time = ngx.time()
local window = 300 -- 5分钟窗口
-- 检查短时间内的失败登录次数
local login_failures = red:get("login_failures:" .. client_ip) or 0
if tonumber(login_failures) > 5 then
security_monitor.log_event(event_types.SUSPICIOUS_ACTIVITY, {
reason = "multiple_login_failures",
count = login_failures
})
end
red:set_keepalive(10000, 100)
end
return security_monitor
6.2 安全配置最佳实践
定期更新
- 及时更新OpenResty和依赖库
- 定期审查安全配置
- 监控安全漏洞公告
密钥管理
- 使用强随机密钥
- 定期轮换密钥
- 安全存储敏感信息
监控告警
- 实时监控安全事件
- 设置异常告警
- 建立应急响应机制
访问控制
- 最小权限原则
- 定期审查权限
- 多因素认证
总结
OpenResty提供了强大的安全功能,通过合理配置和使用各种安全模块,可以构建一个安全可靠的Web应用防护体系。关键要点包括:
- 多层防护:结合多种安全机制
- 实时监控:及时发现和响应安全威胁
- 灵活配置:根据业务需求调整安全策略
- 持续改进:定期评估和优化安全配置