用 1Panel + Authelia 给多个站点加一道统一登录墙
本文记录我在 1Panel(OpenResty + 反向代理)环境下部署 Authelia,并把 OpenList、Typecho 后台接入同一套登录体系的完整过程。
文中统一以example.com作为示例域名,请全部替换成你自己的域名。
环境:Debian/Ubuntu + 1Panel v2 + OpenResty(host 网络模式)+ Authelia 4.39.28。
一、先搞清楚 Authelia 是什么
很多人第一次接触会误以为它是「网站插件」,其实不是。
Authelia 是反向代理前面的一道鉴权前置层,它不修改你的站点代码,也不接管你的业务逻辑。工作方式是:nginx 每收到一个请求,先内部问一句 Authelia「这个人登录了吗」,Authelia 说 OK 才把请求转给后端。
1 | 外网用户 |
所以部署它实际要做三件事:
- 启动一个 Authelia 容器
- 给它一个域名和证书(必须 HTTPS)
- 在站点 nginx 配置里挂一段
auth_request
二、前置条件
| 条件 | 说明 |
|---|---|
| 一个域名 | Authelia 不能用 IP 访问,登录态存在 cookie 里,cookie 绑域名 |
| 一个泛域名证书(推荐) | *.example.com,省得每个子域单独申请 |
| DNS 控制权 | 要能加 auth 的 A 记录 |
nginx 带 http_auth_request_module | 1Panel 的 OpenResty 一般都有,见下一步验证 |
国内服务器还要注意:域名必须完成备案,否则 80/443 无法正常解析访问。
三、开工前先探测环境
这一步能省掉后面大量的试错。全部在「主机终端」里执行(1Panel 左侧菜单的「终端」,提示符形如 root@iZbp...),不要在应用卡片的「容器终端」里跑——容器里没有 docker 命令,文件也是临时的。
3.1 查真实容器名
1 | docker ps --format '{{.Names}}\t{{.Ports}}' | grep -Ei 'openresty|typecho|openlist' |
注意:1Panel 界面上显示的容器名末尾常被缩略,实际名字可能是 1Panel-openresty-cGcF 而不是界面上看到的 1Panel-openresty-6d4f。以命令输出为准。
3.2 查 OpenResty 网络模式
1 | docker inspect <openresty容器名> --format '{{.HostConfig.NetworkMode}}' |
- 输出
host→ Authelia 用127.0.0.1:9091访问 - 输出某个网络名(如
1panel-network)→ Authelia 用容器名访问,需要把两者放进同一网络
本文按 host 模式 写(1Panel 默认安装的 OpenResty 就是 host)。
3.3 查 auth_request 模块
1 | docker exec <openresty容器名> openresty -V 2>&1 | tr ' ' '\n' | grep -E 'auth|lua' |
必须看到 --with-http_auth_request_module。没有这个模块,整套方案走不通(那就得改用 Lua 的 ngx.location.capture 方案,复杂不少)。
小坑:别写成openresty -V | grep auth_request。如果容器名不对,No such container的报错会被 grep 过滤掉,屏幕上一片空白,看起来像「命令成功但没输出」。
四、部署 Authelia
4.1 建目录、写主配置
1 | mkdir -p /opt/authelia/config && cd /opt/authelia |
几个关键点,都是踩过坑才明白的:
① jwt_secret 不能省。
Authelia 4.39 起这是强制项,缺了会一直重启,日志里刷:
1 | Configuration: identity_validation.reset_password: option 'jwt_secret' is required |
② cookies.domain 必须写父域(example.com,不带 www、不带子域前缀)。
写错了会出现「登录成功了,但换个站点又让你登录」的诡异现象。登录态靠这个 cookie 在各子域间共享,SSO 能不能成立全看这一行。
③ inactivity 别用默认值。
默认很短,5 分钟不操作就掉登录,开了两步验证之后每次重登都很折磨。设成 1h 和 expiration 对齐比较舒服。
④ heredoc 用的是 <<EOF(不加引号),这样 $(openssl rand -hex 32) 会被 shell 展开,生成真正的随机密钥。如果写成 <<'EOF',密钥会变成这行字符串本身,容器起不来。
4.2 生成用户
1 | cd /opt/authelia |
看到 生成的哈希: $argon2id$v=19$m=65536,t=3,p=4$... 才算成功。如果是空的,说明密码里有 $、!、\ 之类被 shell 吃掉了——换纯字母数字密码重试。这是「明明密码对却登不进去」最常见的原因。
用户名 admin: 那一行不能加引号,且缩进 2 空格,下面的字段缩进 4 空格。YAML 缩进错一个字符,Authelia 直接起不来。
4.3 启动容器
1 | cat > docker-compose.yml <<'EOF' |
端口绑在 127.0.0.1 上是故意的:公网无法直连 Authelia,只能通过 nginx 访问,少一个暴露面。
成功标志:
1 | Authelia is listening for non-TLS connections on [::]:9091 |
如果 curl 返回空,一定是配置问题,先看 docker logs authelia 的 level=error 那几行,基本都是缩进或字段名写错。
五、给 Authelia 一个域名
5.1 加 DNS
| 类型 | 主机记录 | 记录值 |
|---|---|---|
| A | auth | 你的服务器公网 IP |
1 | ping -c 2 auth.example.com |
能解析到你的 IP 就 OK。
5.2 1Panel 建反向代理站点
网站 → 创建网站 → 反向代理
| 字段 | 值 |
|---|---|
| 域名 | auth.example.com |
| 代理地址 | http://127.0.0.1:9091 |
| 证书 | 选泛域名证书,或用 DNS 验证申请 |
HTTPS 必须开。 Authelia 的 session cookie 带 Secure 标记,走 http 根本种不上,登录会一直失败。
5.3 验证
1 | curl -sk -o /dev/null -w "%{http_code}\n" https://auth.example.com |
返回 200 就通了。浏览器打开应该能看到登录页,用 admin + 密码能登进去。
这一步必须先成功再往下做,否则后面挂站点会变成「登不进去又退不出来」的死循环。
六、把站点接入鉴权(核心步骤)
6.1 先看清 1Panel 的配置结构
1 | cp /opt/1panel/www/conf.d/pan.example.com.conf /root/pan.conf.bak |
你会看到一个大致这样的结构:
1 | server { |
关键点:location / 和 proxy_pass 不在主配置里,被拆到了 sites/xxx/proxy/*.conf。
这解释了为什么「往主配置里塞 location」会失败。也决定了我们的插入策略:写在主配置的 server 级,不动 proxy/ 目录。
原因是 1Panel 在修改站点设置(换域名、改端口、开关 HTTPS)时会重新生成 proxy/*.conf,手工改动会被覆盖;而 conf.d/xxx.conf 基本不会被动。
6.2 准备配置片段
1 | cat > /tmp/ah.conf <<'EOF' |
这几行里有三个「不写就会出怪问题」的 header:
| 配置 | 作用 | 不写的后果 |
|---|---|---|
Host $http_host | 让 Authelia 知道真实域名,按域名匹配 cookie 规则 | 判定失败,永远匿名 |
Cookie $http_cookie | 显式把 cookie 传给子请求 | 主页面能过,API 全部 302 |
Authorization "" | 抹掉客户端自带的 Authorization | 日志刷 failed to parse content of Authorization header |
注意 auth_request 写在 server 级、location 外面,不能写进 location / 里面——否则 location = /authelia-verify 自己也会被要求认证,形成递归死循环。
6.3 插入配置
1 | cd /opt/1panel/www/conf.d |
期望输出:
1 | 4: server_name pan.example.com; |
auth_request 紧跟在 server_name 后面,位置就对了。
sed的r命令是「匹配行之后追加文件内容」,比手写正则插入稳得多,也不用你去找大括号在哪。
6.4 检查语法 → 重载 → 验证
1 | docker exec <openresty容器名> openresty -t |
必须看到 test is successful。不通过就别 reload,先回滚:
1 | cp pan.example.com.conf.bak pan.example.com.conf |
通过后:
1 | docker exec <openresty容器名> openresty -s reload |
期望:
1 | 302 https://auth.example.com/?rd=https://pan.example.com/ |
看到这个 302,就说明鉴权链路通了。
七、两个必踩的坑(重点看)
7.1 -t 不会让配置生效
这是最容易浪费时间的一条:
| 命令 | 作用 |
|---|---|
openresty -t | 只检查语法对不对 |
openresty -s reload | 真正加载新配置 |
只跑 -t 会让你以为改好了,实际运行的还是旧配置。我第一次就是改完 -t 一通过就去 curl,结果首页仍然 302,找了一小时才发现少了一条 reload。
7.2 reload 之后要等一下再验证
1 | docker exec <openresty容器名> openresty -s reload; sleep 2 |
reload 是平滑重载,旧 worker 会继续处理完手头的请求,新 worker 才接管。reload 完立刻 curl,很可能打到旧 worker 上,拿到的是旧结果。
同样的道理:openresty -T(打印运行中的完整配置)读的是磁盘文件,只能证明「文件写对了」,不能证明「已经生效」。
八、进阶:只保护后台,前台保持公开
网盘这类「本来就是私密」的站点适合整站保护,但博客不行——整站加锁等于访客看不到内容、搜索引擎也抓不到。
思路:auth_request 照样挂在 server 级,但在校验入口里加一个判断,只有后台路径才去问 Authelia,其他路径直接返回 204 放行。
1 | cat > /tmp/ah-admin.conf <<'EOF' |
为什么 /action/login 也要保护?因为 Typecho 的登录表单提交到这个路径。只护 /admin 而放行 /action/login,等于留了一个绕过外层鉴权直接登后台的后门。 两者一起护才严密,而评论走的 /action/comment 不在名单里,读者照常评论。
验证:
1 | curl -sk -o /dev/null -w "首页 %{http_code} -> %{redirect_url}\n" https://blog.example.com/ |
1 | 首页 200 -> |
首页那行 -> 后面是空的,才说明真的放行了。
九、进阶:开启两步验证(TOTP / WebAuthn)
9.1 开启模块
webauthn 段必须在配置最外层(顶格),加在 totp 之后:
1 | webauthn: |
1 | docker restart authelia; sleep 4; docker logs --tail 6 authelia |
9.2 注册设备
- 打开
https://auth.example.com,用密码登录 - 访问
https://auth.example.com/settings - 左侧点「安全」——注意不是「两步验证」
| 页面 | 作用 |
|---|---|
| 安全 | 注册/删除验证方式(TOTP、WebAuthn) |
| 两步验证 | 列出哪些站点要求二因素,还没改策略时这里是空的 |
「一次性密码」点添加 → 出二维码 → 手机验证器扫码 → 填 6 位数字。
「WebAuthn 凭据」点添加 → 给设备起名 → 按指纹 / Windows Hello。
建议两种都注册:TOTP 当保底(不绑域名,换域名也能用),WebAuthn 当日常(更快、抗钓鱼)。
9.3 改成强制二因素
先确认设备注册成功并能通过验证,再改策略。 顺序反了会把自己锁在门外。
1 | sed -i "s/policy: 'one_factor'/policy: 'two_factor'/" /opt/authelia/config/configuration.yml |
十、踩坑清单
| 现象 | 原因 | 处理 |
|---|---|---|
容器反复重启,日志报 jwt_secret is required | 4.39 起强制项 | 补 identity_validation.reset_password.jwt_secret |
| 生成的哈希是空 | 密码含特殊字符被 shell 吃掉 | 换纯字母数字密码 |
| 登录成功但换站又要登录 | cookies.domain 写成了子域 | 改成父域 example.com |
| 主页面能进,API 全 302 / Network Error | 子请求没带 cookie | 加 proxy_set_header Cookie $http_cookie; |
日志刷 Invalid scheme | 客户端 Authorization 被透传 | 加 proxy_set_header Authorization ""; |
| 改了配置但没效果 | 只跑了 -t 没 reload | 补 openresty -s reload |
| reload 后仍 200 | 旧 worker 还在处理请求 | 加 sleep 2 再验证 |
| 站点改设置后保护失效 | 1Panel 重写了配置 | 重新执行 6.3,并确认改的是 conf.d |
| 验证码弹窗总失败 | 提交的码长度不对 / 触发了限流 | 见下方第十一节 |
| 一直显示「获取设置失败」 | 浏览器缓存了旧页面 | Ctrl+Shift+R 强刷 |
| 登出后所有站点一起登出 | 登录态共享在父域 cookie | 设计如此,不是 bug |
十一、关于「身份验证」弹窗和限流
Authelia 在执行敏感操作(改邮箱、改密码、注册新的两步验证设备)前,会要求一次身份确认,弹窗里写着「已向您的电子邮件发送了一次性密码」。
但如果你用的是 notifier: filesystem(本文配置就是这样),它不会真的发邮件,而是写到服务器上的一个文本文件里:
1 | tail -n 60 /opt/authelia/config/notification.txt |
验证码在两行 ==== 之间,8 位、区分大小写。想只取码不看废话:
1 | tail -n 60 /opt/authelia/config/notification.txt | grep -E "^[A-Z0-9]{8}$" | tail -1 |
两个高频错误:
- 粘贴了一整段文字进去。 日志会明说
the user provided code was 65 characters in length——正确长度是 8。粘贴前瞄一眼输入框长度。 - 连续重试触发限流。 配置里
regulation的「2 分钟内失败 3 次封 5 分钟」对验证码生成本身也生效,日志会刷Rate Limit Exceeded bucket=2 delay=245s。
被限流的解法:手动重启容器(限流计数在内存里,重启即清零):
1 | docker restart authelia |
代价是所有登录会话作废,需要重新登录。
操作纪律:一次只点一次「添加」,弹窗出来就去拿码,拿到就填。 任何一步不顺,先停下看日志,不要连点。
十二、收尾工作
12.1 关掉绕过鉴权的直连端口
这条最容易被忽略,也最要紧。
假设你的站点用 IP:8089 也能打开,那这个入口完全绕过了 OpenResty 和 Authelia——知道 IP 和端口的人可以直接打开站点登录页,Authelia 的防爆破规则根本不参与。
在云厂商安全组里删掉 8080/8089 之类的入方向规则,只保留 22、80、443 和管理面板端口。
不用改容器、不用重启应用。因为安全组只管「从外网进来」的流量,反向代理走的是 127.0.0.1 本机回环,不经过安全组:
1 | 外网访客 ──► 公网网卡:8089 ──► 容器 ← 安全组拦截(要堵的就是这条) |
动手前先确认代理用的是回环地址:
1 | grep -rn "proxy_pass" /opt/1panel/www/sites/<站点>/proxy/ 2>/dev/null |
只要写的是 127.0.0.1:xxxx,删安全组规则就绝对安全。
12.2 备份
1 | tar czf /root/authelia-$(date +%F).tar.gz -C /opt authelia/config |
config/ 里存着 encryption_key 和用户哈希,丢了等于所有会话失效、所有两步验证设备要重新注册。
需要长期保存的三个文件:
| 文件 | 内容 |
|---|---|
configuration.yml | 主配置 |
users_database.yml | 用户名 + 密码哈希 |
db.sqlite3 | TOTP 密钥、WebAuthn 凭据、登录会话(一次性密码就存在这里,不是一个单独的文件) |
十三、设备全丢了的应急复位
密码和账号不受影响,只清两步验证:
1 | docker stop authelia |
Authelia 会重建一个空库。用户名密码在 users_database.yml 里,不会丢,但所有 2FA 设备要重新注册,所有会话要重新登录。
十四、已知的局限
诚实地说,这套方案有两个不足:
① 重启即掉线。 会话存在 Authelia 自己的本地存储里,docker restart 之后所有人都要重新登录。想解决就接 Redis 存 session(内存开销约 10MB),配置里加一个 session.redis 段即可,属于优化项。
② 1Panel 改站点设置会覆盖配置。 每次在 1Panel 里换域名、改端口、开关 HTTPS 之后,跑一遍确认保护还在:
1 | curl -sk -o /dev/null -w "%{http_code}\n" https://pan.example.com/ |
302 正常,200 说明被覆盖了,重新执行第六节。
十五、最终检查清单
- [ ]
https://auth.example.com能打开登录页并登录成功 - [ ] 各站点未登录时返回
302 → auth.example.com/?rd=... - [ ] 登录后自动跳回原站点,刷新不掉线
- [ ] 登录一个站点后,其他站点无需再登(SSO 生效)
- [ ]
8080/8089等直连端口从外网已无法访问 - [ ]
/admin等后台路径被拦、前台正常 - [ ] 两步验证能通过,且至少注册了 2 个设备
- [ ] 备份已生成
整件事说到底就三块:一个容器 + 一段 nginx 钩子 + 一份用户库。以后再加站点,复制