Margrop
Articles168
Tags369
Categories24
1password AC AP API AppDaemon Aqara Cookie 认证 Cron Date Diagrams.net HA HADashboard HomeAssistant IP IPv4 Java LVM‑Thin Linux MacOS MiniMax MySQL NAS Nginx OpenAI OpenClaw OpenResty PPPoE PostgreSQL ProcessOn Proxmox VE SSL Shell TTS TimeMachine UML Uptime Kuma Web Windows activate ad adb adblock agent aligenie aliyun alpine annotation aop authy autofs backup baidupan bash bitwarden boot brew browser caddy2 cdn centos cert certbot charles chat chrome classloader client clone closures cloudflare cmd command commit container crontab ctyun ddsm demo dependency deploy developer devtools dll dns docker domain download draw drawio dsm dump dylib edge exception export fail2ban feign firewall-cmd flow frp frpc frps fuckgfw function gcc gfw git github golang gperftools gridea grub gvt-g hacs havcs heap hello hexo hibernate hidpi hoisting homeassistant hosts html htmlparser https idea image img img2kvm import index install intel io ios ip iptables iptv ipv6 iso java javascript jetbrains jni jnilib jpa js json jsonb jupter jupyterlab jvm k8s kernel key kid kms kodi koolproxy koolproxyr kvm lan lastpass launchctl learning lede letsencrypt linux live low-code lvm lxc m3u8 mac macos mariadb markdown maven md5 microcode mirror modem modules monitor mount mstsc mysql n2n n5105 nas network nfs node node-red nodejs nohup notepad++ npm nssm ntp oop openfeign openssl os otp ovz packet capture pat pdf pem perf ping pip plugin png powerbutton print pro proxy pve pvekclean python qcow2 qemu qemu-guest-agent rar reboot reflog remote remote desktop renew repo resize retina root route router rule rules runtime safari sata scipy-notebook scoping scp server slmgr so socks source spk spring springboot springfox ssh ssl stash string supernode svg svn swagger sync synology systemctl tap tap-windows tapwindows telecom template terminal tls token totp tvbox txt ubuntu udisk ui undertow uninstall unlocker upgrade url v2ray vhd vim vlmcsd vm vmdk web websocket wechat windows with worker wow xiaoya xml yum zip 中国电信 云电脑 交换机 光猫 公网IP 内存 内网IP 升级 反向代理 启动 夏令时 天猫精灵 天翼云 安全 安装 容器 导入 小米 常用软件 广告屏蔽 序列号 应用市场 异常 打工 技术 抓包 描述文件 日记 时区 显卡虚拟化 智能家居 智能音箱 梯子 模块 流程 流程图 浏览器 漫游 激活 火绒 电信 画图 直播源 续期 网关 网络风暴 群晖 腾讯 自动化 虚拟机 证书 路由 路由器 软件管家 软路由 运维 运维监控 配置 钉钉 镜像 镜像源 门窗传感器 问题排查 防火墙 阿里云 阿里源 集客

Hitokoto

Archive

使用 OpenResty + 签名 Cookie 实现"登录一次,1 小时免重复认证"的反代安全网关

使用 OpenResty + 签名 Cookie 实现"登录一次,1 小时免重复认证"的反代安全网关

背景:某些 Web 面板/管理后台近期安全风险频出,公网暴露端口容易被扫描与爆破。本文记录一种轻量加固方案:不改原应用容器,仅用 OpenResty 做反向代理与认证网关,实现首次 BasicAuth、之后 1 小时免验证

1. 目标与约束

目标

  • 原应用端口不直接暴露公网
  • 增加认证拦截
  • 登录一次后,最近 1 小时同客户端无需再次认证
  • 网关可控过期时间(严格 3600 秒)
  • 支持 WebSocket
  • 容器重启不影响(无状态认证)

约束

  • 不修改原应用容器
  • 尽量不引入额外依赖(Redis/DB/认证中心)

2. 架构概览

1
2
3
4
5
6
7
8
公网: 外网端口

OpenResty(反代网关)
├── /__login:BasicAuth(只在这里弹一次)
├── Cookie 校验:签名 + 过期校验(1 小时)
└── 反代到内部应用

内部应用:仅监听 127.0.0.1:内部端口

3. 完整 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
services:
app:
image: your-app-image:tag
container_name: app
hostname: app
restart: always
environment:
APP_ENV_1: "..."
APP_ENV_2: "..."
volumes:
- /path/to/app/data:/app/data
ports:
- "127.0.0.1:内部端口:内部端口"
networks:
- ql_net

openresty:
image: openresty/openresty:alpine
container_name: secure-gateway
restart: always
depends_on:
- app
ports:
- "外网端口:80"
environment:
COOKIE_SECRET: "REPLACE_WITH_A_LONG_RANDOM_SECRET"
COOKIE_NAME: "ql_auth"
COOKIE_TTL: "3600"
volumes:
- /path/to/openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
- /path/to/openresty/.htpasswd:/etc/openresty/.htpasswd:ro
networks:
- ql_net

networks:
ql_net:
driver: bridge

4. 完整 nginx.conf

特点:

  • map 放在 http {} 级别
  • 不依赖 resty.hmac,使用内置 ngx.hmac_sha1()
  • /__login 触发 BasicAuth
  • /__logout 可清 Cookie(可选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

resolver 127.0.0.11 ipv6=off valid=30s;

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 80;
server_name _;

# 1) 登录入口:只在这里做 BasicAuth
location = /__login {
auth_basic "Restricted";
auth_basic_user_file /etc/openresty/.htpasswd;

content_by_lua_block {
local secret = os.getenv("COOKIE_SECRET") or "change_me"
local cookie_name = os.getenv("COOKIE_NAME") or "ql_auth"
local ttl = tonumber(os.getenv("COOKIE_TTL") or "3600") or 3600

local function to_hex(s)
return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
end

local function sign(payload)
return to_hex(ngx.hmac_sha1(secret, payload))
end

local exp = ngx.time() + ttl
local payload = tostring(exp)
local token = payload .. "." .. sign(payload)

local secure = ""
if ngx.var.scheme == "https" then
secure = "; Secure"
end

ngx.header["Set-Cookie"] = cookie_name .. "=" .. token .. "; Path=/; Max-Age=" .. ttl .. "; HttpOnly; SameSite=Lax" .. secure

local next_url = ngx.var.arg_next
if not next_url or next_url == "" then
next_url = "/"
end
return ngx.redirect(next_url, 302)
}
}

# 2) 退出入口
location = /__logout {
content_by_lua_block {
local cookie_name = os.getenv("COOKIE_NAME") or "ql_auth"
ngx.header["Set-Cookie"] = cookie_name .. "=deleted; Path=/; Max-Age=0; HttpOnly; SameSite=Lax"
return ngx.redirect("/", 302)
}
}

# 3) 主入口:校验 Cookie
location / {
access_by_lua_block {
local secret = os.getenv("COOKIE_SECRET") or "change_me"
local cookie_name = os.getenv("COOKIE_NAME") or "ql_auth"

local function to_hex(s)
return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
end

local function sign(payload)
return to_hex(ngx.hmac_sha1(secret, payload))
end

if ngx.var.uri == "/__login" or ngx.var.uri == "/__logout" then
return
end

local token = ngx.var["cookie_" .. cookie_name]
local ok = false

if token and token ~= "" then
local dot = token:find("%.")
if dot then
local payload = token:sub(1, dot - 1)
local sig = token:sub(dot + 1)

if sig == sign(payload) then
local exp = tonumber(payload)
if exp and exp > ngx.time() then
ok = true
end
end
end
end

if not ok then
local next_url = ngx.escape_uri(ngx.var.request_uri)
return ngx.redirect("/__login?next=" .. next_url, 302)
end
}

proxy_pass http://app:内部端口;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}

5. 操作步骤

Step 1:准备目录

1
2
mkdir -p /docker/qinglong/openresty
mkdir -p /docker/qinglong/data

Step 2:生成 BasicAuth 账号密码

1
2
3
docker run --rm httpd:2.4-alpine \
htpasswd -nbB username 'strong_password' \
> /docker/qinglong/openresty/.htpasswd
1
2
3
4
python3 - <<'PY'
import os, base64
print(base64.b64encode(os.urandom(48)).decode())
PY

Step 4-6:启动服务

1
2
cd /docker/qinglong
docker compose up -d

Step 7:验证效果

  1. 首次访问自动跳转 /__login?next=...
  2. 弹 BasicAuth 输入一次
  3. 登录后跳回原页面
  4. 1 小时内无需再次验证
  5. 访问 /__logout 可立即退出

6. 常见错误

错误 1:map directive is not allowed here

  • 原因:map 写在了 server {}
  • 修复:确保 map 位于 http {} 级别

错误 2:module 'resty.hmac' not found

  • 原因:镜像没有 lua-resty-hmac
  • 修复:改用 ngx.hmac_sha1()

7. 总结

这套方案的价值在于:

  • 不改原应用容器,通过网关实现可控认证
  • BasicAuth 仅首次触发,之后靠签名 Cookie 严格 1 小时免验证
  • 无状态、不依赖外部存储,容器重启也不丢

适合个人/小团队把敏感面板从”裸奔公网”快速提升到”可控暴露”。

Author:Margrop
Link:http://blog.margrop.com/post/2026-02-28-openresty-signed-cookie-auth-gateway/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可