邮箱验证码登录家庭NAS系统:给自建NAS的对外服务项目(相册、文件管理、智能家居设备等)增加一道安全网关

 背景

家里有一台闲置的工控机,拿来跑 OpenMediaVault(OMV)搭了个 NAS,平时存文件、放相册、跑一些自用的小服务。因为家庭宽带天然具备 IPv6 地址,不需要额外做内网穿透或者买公网 IPv4,NAS 本身就能直接暴露在公网上被访问——这既是优点(省事、免费、速度不受限速影响),也是最大的风险点:IPv6 地址虽然理论上难以被批量扫描,但只要地址泄露一次,NAS 管理面板、文件服务、相册就直接暴露给了整个互联网,没有云厂商的 WAF、没有 NAT 的天然屏障。

OMV 自带的账号密码登录显然不够。为了不让 NAS 及挂在后面的其他内部系统(相册、文件管理等)直接裸奔在公网,索性利用 NAS 本身的容器功能(Docker),跑一个独立的 OpenResty 网关容器,在所有请求真正打到后端服务之前,先过一道邮箱验证码认证——相当于给整个家庭内网入口做了一层统一的 Zero Trust 收口,管理面板、业务系统本身完全不需要额外改造,认证逻辑全部收敛在网关这一层。

整个方案分三块:Nginx 配置部分、登录页渲染、验证码发送、Session 校验与续期。开发过程中踩了几个典型的坑,记录下来。

​编辑

一、Nginx 配置部分

整个网关分两类 server block:一类是认证域名本身(auth.3137666.xyz),负责渲染登录页、发送验证码、校验登录;另一类是所有需要保护的业务域名(比如 omv4.3137666.xyz),在 access_by_lua_file 阶段统一拦截、校验 Session,通过后再 proxy_pass 到后端真实服务。

1.1 http 层公共配置

第一个大坑:外网ipv6到宿主机,宿主机与容器通信,openresty-alpine容器不带ipv6,邮件对外通信的域名使用的双栈,返回的ipv6无法通信,一定一定要关闭ipv6=off

nginx

http {
    lua_package_path "/usr/local/openresty/nginx/lua/?.lua;;";

    # 会话、验证码、冷却期统一存放在这个共享内存字典
    lua_shared_dict zta_cache 10m;

    resolver 127.0.0.11 ipv6=off;  # resty.http 请求邮件 API 需要 DNS 解析,与容器的dns一致。

    # ... 其他通用配置 ...
}

1.2 统一认证域名:auth.3137666.xyz

nginx

# ==================== 1. 统一认证中心 (auth.3137666.xyz:8443) ====================
server {
    listen 8443 ssl;
    server_name auth.3137666.xyz;

    ssl_certificate /etc/nginx/ssl/3137666.xyz.fullchain.cer;
    ssl_certificate_key /etc/nginx/ssl/3137666.xyz.key;

    location = / {
        root /usr/local/openresty/nginx/html; # 导航页面,不做认证
        index index.html;
    }
	# 登录页渲染与 POST 校验
    location /login {
        content_by_lua_file /etc/nginx/lua/auth_login.lua;
    }

    # 异步发送验证码接口
    location /api/send-code {
        content_by_lua_file /etc/nginx/lua/send_otp.lua;
    }

    # 登出清空 Session
    location /logout {
        content_by_lua_file /etc/nginx/lua/auth_logout.lua;
    }
	location = /filebrowser {
        return 302 https://sample.3137666.xyz:8443;#内部系统
    }
	#其他内部系统
    }
}

1.3 受保护的业务域名(以 OMV NAS 为例)

nginx

server {
    listen 8443 ssl;
    server_name omv.3137666.xyz;
    ssl_certificate /etc/nginx/ssl/3137666.xyz.fullchain.cer;
    ssl_certificate_key /etc/nginx/ssl/3137666.xyz.key;
    access_by_lua_file /etc/nginx/lua/session_auth.lua;
    location / {
        # 填写你内网实际服务的地址与端口
        proxy_pass http://192.168.0.251:80;         
        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;
        # WebSocket 支持
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
#其他server
{
#。。。
}

相册、文件管理他内部系统,各自建一个。

二、登录流程设计

2.1 未登录跳转

访问受保护资源时,access_by_lua 阶段检查 Cookie 里的 ZTA_SESSION,没有或者失效就重定向到登录页,同时把原始访问地址带上,方便登录后跳回去:

lua​编辑

2.2 登录页 + 验证码发送

登录页是一个纯前端表单,邮箱 + 六位验证码,点击”获取验证码”调用 /api/send-code,输入验证码后 POST /login 完成校验。

​编辑

三、流量校验与Session滑动续期,Cookie同步

一周内面免校验登录,剩余一天,情况下有流量通过,自动续期一周,服务端 session时间与客户端cookie时间同步。保证状态一致。

​编辑

四、小结

问题表现本质
验证码发送时好时坏连接resned发送超时失败容器无ipv6,dns解析出的ipv6无法通信,nginx配置关闭ipv6
Session 续期失效服务端 session 有效但用户被踢下线服务端 TTL 和客户端 Cookie Max-Age 没同步刷新
验证码倒计时误触发输错邮箱也进入 60 秒冷却HTTP 状态码在前端被忽略,业务逻辑判断依赖了错误的信号

另外还有一个不经过认证的导航页面,使用相同布局,系列不同二级子域名的情况下,部分浏览器安全设计会被认作广告进行隐藏不显示。通过单一域名+路径,然后进行302跳转到对应二级子域解决。

对于自建 NAS 这类暴露在公网、又想保持轻量的场景,用 OpenResty + ngx.shared.DICT 撑起一套邮箱验证码 + Session 管理是完全够用的方案,成本低、无需额外中间件。

放两张界面截图:

​编辑

​编辑

​编辑

​编辑

​编辑 ​

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部