7.9.7 安全与限流鉴权实践

安全与限流鉴权实践#

1. 安全模型与风险点#

  • OpenResty 位于网关/反向代理层,重点防护:恶意扫描、暴力破解、爬虫滥用、注入与协议异常、盗链与接口滥用。
  • 安全原则:最小暴露、默认拒绝、分层防御、日志可追溯。

原理草图(网关安全流)

文章图片

2. 基础安全加固#

安装与启用基础模块(OpenResty)

# CentOS/RHEL 示例
yum install -y openresty openresty-resty
openresty -V | head -n 5
systemctl enable --now openresty

请求校验与目录防护示例
文件:/usr/local/openresty/nginx/conf/nginx.conf

http {
  server {
    listen 80;
    server_name example.com;

    # 只允许 GET/POST
    if ($request_method !~ ^(GET|POST)$) { return 405; }

    # Host 校验
    if ($host !~ ^example\.com$) { return 444; }

    # 禁用隐藏文件与敏感路径
    location ~* (\.git|\.svn|\.env|/etc/|/proc/|/root/) { return 403; }

    # 防盗链
    location /static/ {
      valid_referers none blocked example.com *.example.com;
      if ($invalid_referer) { return 403; }
    }

    location / {
      proxy_pass http://backend;
    }
  }
}

预期效果
- 非法方法返回 405;伪造 Host 返回 444;敏感路径返回 403;盗链返回 403。

排错要点

nginx -t
tail -f /usr/local/openresty/nginx/logs/error.log
curl -I -X PUT http://example.com/  # 期望 405

3. 访问控制与黑白名单#

共享字典动态黑名单(示例)
文件:/usr/local/openresty/nginx/conf/nginx.conf

lua_shared_dict blacklist 10m;

server {
  listen 80;

  access_by_lua_block {
    local dict = ngx.shared.blacklist
    local ip = ngx.var.remote_addr
    if dict:get(ip) then
      ngx.log(ngx.WARN, "deny ip=", ip)
      return ngx.exit(403)
    end
  }

  location /admin/ { return 403; }
  location / { proxy_pass http://backend; }
}

动态加入黑名单(命令示例)

# 临时通过 resty 命令注入
resty -e 'local d=ngx.shared.blacklist; d:set("1.2.3.4", true, 3600)'

排错

resty -e 'local d=ngx.shared.blacklist; print(d:get("1.2.3.4"))'

4. 限流与熔断策略#

limit_req 限流(按 IP)

http {
  limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;

  server {
    listen 80;
    location /api/ {
      limit_req zone=perip burst=10 nodelay;
      proxy_pass http://backend;
    }
  }
}

Lua 实现令牌桶(简化示例)

lua_shared_dict tokens 20m;

location /api/ {
  access_by_lua_block {
    local dict = ngx.shared.tokens
    local key = ngx.var.remote_addr
    local rate, burst = 5, 10
    local now = ngx.now()
    local last = dict:get(key .. ":t") or now
    local tokens = dict:get(key .. ":c") or burst

    tokens = math.min(burst, tokens + (now - last) * rate)
    if tokens < 1 then
      ngx.status = 429
      ngx.say("Too Many Requests")
      return ngx.exit(429)
    end
    tokens = tokens - 1
    dict:set(key .. ":t", now)
    dict:set(key .. ":c", tokens)
  }
  proxy_pass http://backend;
}

熔断与降级(后端异常)

location /api/ {
  proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
  proxy_read_timeout 3s;
  proxy_pass http://backend;
}

location /fallback/ {
  return 200 "service degraded";
}

排错

curl -i http://example.com/api/  # 触发 429 时应返回 Too Many Requests
grep "limiting requests" /usr/local/openresty/nginx/logs/error.log

5. 鉴权与认证#

JWT 鉴权(lua-resty-jwt)安装

opm get SkyLothar/lua-resty-jwt
ls /usr/local/openresty/site/lualib/resty/jwt.lua

JWT 校验示例

location /api/ {
  access_by_lua_block {
    local jwt = require "resty.jwt"
    local auth = ngx.var.http_authorization
    if not auth then return ngx.exit(401) end
    local token = auth:gsub("Bearer ", "")
    local jwt_obj = jwt:verify("secret_key", token)
    if not jwt_obj.verified then return ngx.exit(401) end
  }
  proxy_pass http://backend;
}

HMAC 签名校验(Header + 时间戳)

location /sign/ {
  access_by_lua_block {
    local hmac = require "resty.hmac"
    local secret = "s3cr3t"
    local ts = ngx.var.http_x_ts
    local sign = ngx.var.http_x_sign
    if not ts or not sign then return ngx.exit(401) end

    local hm = hmac:new(secret, hmac.ALGOS.SHA256)
    hm:update(ts .. ngx.var.request_uri)
    local calc = ngx.encode_base64(hm:final(), true)
    if calc ~= sign then return ngx.exit(401) end
  }
  proxy_pass http://backend;
}

排错

curl -H "Authorization: Bearer <token>" http://example.com/api/
tail -f /usr/local/openresty/nginx/logs/error.log

6. 防重放与幂等控制#

时间戳窗口 + nonce 防重放

lua_shared_dict nonce 50m;

location /pay/ {
  access_by_lua_block {
    local dict = ngx.shared.nonce
    local ts = tonumber(ngx.var.http_x_ts)
    local nonce = ngx.var.http_x_nonce
    if not ts or not nonce then return ngx.exit(401) end

    if math.abs(ngx.time() - ts) > 300 then return ngx.exit(401) end
    if dict:get(nonce) then return ngx.exit(409) end
    dict:set(nonce, true, 300)
  }
  proxy_pass http://backend;
}

幂等校验(订单号)

lua_shared_dict idem 50m;

location /order/submit {
  access_by_lua_block {
    local dict = ngx.shared.idem
    ngx.req.read_body()
    local args = ngx.req.get_post_args()
    local oid = args["order_id"]
    if not oid then return ngx.exit(400) end
    if dict:get(oid) then return ngx.exit(409) end
    dict:set(oid, true, 600)
  }
  proxy_pass http://backend;
}

7. 安全日志与审计#

自定义日志格式

log_format security '$request_id $remote_addr $http_user_agent '
                   '$request $status $http_authorization '
                   '$upstream_status $request_time';

access_log /usr/local/openresty/nginx/logs/security.log security;

日志脱敏(简单示例)

map $http_authorization $auth_masked {
  default "***";
  ""      "-";
}
log_format security2 '$request_id $remote_addr $auth_masked $status';
access_log /usr/local/openresty/nginx/logs/security2.log security2;

排错

tail -f /usr/local/openresty/nginx/logs/security.log

8. 实践建议#

  • 策略集中化:统一网关层完成鉴权/限流,降低业务复杂度。
  • 规则灰度:新规则先小流量试运行。
  • 压测验证:在高并发下验证限流与鉴权性能。
  • 监控闭环:限流命中率、鉴权失败率、异常响应率指标持续观察。

9. 练习与自测#

  1. 启用 limit_req 并设置 rate=2r/s,用 abwrk 观察 429 返回比例。
  2. 写一个 Lua 规则:拒绝 UA 包含 curl 的请求,并记录到安全日志。
  3. 实现 JWT 校验:过期后返回 401,验证日志是否记录用户标识。
  4. 配置黑名单动态更新,写脚本每 10 秒向共享字典写入一个 IP。

练习命令

# 简单压测
ab -n 100 -c 20 http://example.com/api/

# 写入黑名单
resty -e 'ngx.shared.blacklist:set("8.8.8.8", true, 60)'