志睿源官网|后台管理入口「免密网关」上线报告
把公众号菜单直连后台地址的裸入口,换成有身份的微信静默授权网关:管理员免密直达、非管理员看不到登录页、后台地址不外露。
- 日期:2026-09-23
- 范围:志睿源官网后台(
/zry/admin/)的入口收敛 - 状态:✅ 已上线并通过全部接口侧核验;真机端到端实测待完成
- 关联:前序评估见《公众号菜单_官网管理入口_评估与设计》
一、要解决的问题
公众号菜单里新增的「官网管理」原本直连后台地址,等于对全部关注者公布了后台入口:任何人点开都能看到登录页,后台地址也被动外泄。
M2 的目标是把这条路改成有身份的门:
| 要求 | 实现效果 |
|---|---|
| 管理员免密进入 | 点菜单 → 微信静默授权 → 直接落在后台首页,无需输账号密码 |
| 非管理员直接拒绝 | 连登录页都看不到,只回一个「未授权访问」提示页 |
| 后台地址不外露 | 后台真实路径不再出现在菜单里,菜单只带授权入口 |
| 不引入新依赖 | 复用站点既有鉴权与数据库,不新增服务、不新增端口 |
二、授权链路
用户点「管理 → 官网管理」
│
▼
授权发起端点 ── 生成一次性 state(内存表,TTL 5 分钟,用完即删)
│
▼ 302
微信静默授权(snsapi_base,用户无感知,不弹确认框)
│
▼ 携带 code + state 回调
授权回调端点
├─ state 校验失败 ──────────────► 提示「链接已失效」
├─ code 换 openid
├─ 白名单查不到 ────────────────► 「未授权访问」(显示本人授权码,便于加白)
└─ 命中白名单 且 账号启用
│
▼
签发站点 JWT(短时票据)
│
▼ 302 到后台首页,票据放在 URL fragment
后台首页脚本把票据落到本地存储,并立刻抹除地址栏上的 fragment
为什么用 fragment 携带票据:# 后面的内容不会发给服务器——不进访问日志、不进 Referer、不落任何中间件。落到本地存储后立即用 replaceState 抹掉,地址栏干净。
为什么用 snsapi_base:静默授权,用户全程无感知,不会弹出「是否允许」确认页;代价是只能拿 openid,拿不到昵称头像——而网关只需要 openid 做身份判定,刚好够用。
三、六处落地
| # | 位置 | 改动 |
|---|---|---|
| ① | 网关模块(新建) | 授权发起、回调核验、白名单判定、票据签发,一个文件自洽 |
| ② | 路由注册表 | 新增两条公开路由:授权发起 / 授权回调 |
| ③ | 模块聚合入口 | 把网关的两个函数显式具名注入导出对象 |
| ④ | 反向代理配置 | 新增两条精确匹配转发规则,早于通用静态规则生效 |
| ⑤ | 后台首页 | 增加「fragment → 本地存储 → 抹除地址」的三行注入脚本 |
| ⑥ | 菜单真源 | 管理 → 官网管理 的目标地址改指网关入口 |
要点:
- ③ 之所以「显式具名」而不是靠对象展开批量注入:聚合入口用展开语法时,新增模块容易漏挂而不报错,只表现为接口 404,排查成本高。具名写出,缺失即启动报错。
- ④ 的精确匹配是必须的,见第五节的坑 2。
- ⑤ 只加在后台首页,不影响其它页面。
四、白名单机制
新建一张绑定表,把微信身份与后台账号关联起来:
| 字段 | 含义 |
|---|---|
openid | 微信身份(主键) |
admin_id | 对应的后台账号 |
note | 备注(谁) |
created_at | 加入时间 |
last_login_at | 最近一次免密登录时间 |
放行条件=该 openid 有绑定记录 且 对应后台账号处于启用状态。
放行时的副作用(便于事后审计):
- 回写绑定的
last_login_at; - 更新账号的
last_login_at与last_login_ip; - 写一条
login类型的操作日志,与密码登录共用同一套日志表——从审计角度看,免密登录和密码登录是同一件事。
加人:往绑定表插一行 (openid, admin_id, note) 即可,无需改代码、无需重启。被拒页面会显示本人的「授权码」(就是 openid),用户截图发来即可加白——这也是为什么拒绝页要显示它。
五、三个坑(都已规避)
坑 1(最重要):防护层会把网关的合法拒绝当成攻击
站点的防护规则里,判断依据是「响应 403 / 429 即视为攻击」。这条规则成立的前提是:403 只可能来自反代的静态层拦截,真人根本碰不到。
网关打破了这个前提——「非管理员来访」是设计内的合法 403。后果很严重:
一个非管理员用户连点 5 次菜单,就会触发 IP 级封禁 24 小时,连官网首页都打不开。 管理员自己扫码失败、反复重试,同样会中招。
解法:给防护规则加精确豁免,只放过这两条网关路径,其余一切照旧拦截。并用规则回放工具验证:
3 行样本 → 1 行忽略(网关的 403,正确放过)
→ 1 行命中(探测敏感文件的 403,防护未受损)
→ 1 行未命中
关键是豁免范围必须收窄到路径级——如果图省事按状态码或按目录放宽,防护就是白做的。
坑 2:挂在静态目录路径下的动态端点会被当成静态文件
网关端点位于后台页面的子路径下,而该路径整体被一条「静态文件」规则接管。若只在明细里追加转发规则,请求会先被静态规则捕获 → 找文件 → 404。
解法:用「精确匹配」的转发规则,优先级高于前缀匹配,命中即转发,不再落到静态逻辑。
坑 3:回调路由必须走「无数据库事务」的变体
回调端点自己结束响应(302),不走框架的统一事务封装。若按普通路由注册,响应结束后连接不会归还连接池,连接数只增不减,最终全站卡死(该站点此前已因此出过一次事故)。
解法:两条新路由都注册为「无事务」变体,数据库连接在网关内部自取自还。
六、上线核验结果
| 核验项 | 结果 |
|---|---|
| 菜单线上真值回读 | ✅ 与前序方案逐字一致;菜单开关正常;无个性化菜单残留 |
| 授权发起(要求返回链接) | ✅ 200,参数完整(appid / 回调地址 / 静默作用域 / state) |
| 授权发起(要求跳转) | ✅ 302 指向微信授权页 |
| 回调(伪造 state) | ✅ 403,提示链接已失效 |
| 白名单判定(真身份) | ✅ 放行,账号启用 |
| 白名单判定(假身份) | ✅ 拒绝,无绑定记录 |
| 后台首页 / 官网首页 / 博客 / 公开接口 | ✅ 全部 200,回归无影响 |
| 反代配置语法 | ✅ 通过并平滑重载 |
| 事故影响面 | ✅ 网关落地后无人被误封 |
七、待办
- 真机端到端实测(只能由本人做):在微信里点「管理 → 官网管理」,确认免密直达后台。
接口侧无法模拟微信授权,这一步必须真机验证。
- 若被拒:把拒绝页显示的「授权码」发来,加一行白名单即可。
- 生效延迟提示:已关注的老用户,客户端菜单缓存最长 24 小时刷新;新关注立即生效。
附:可回滚点
本次共改动 6 个文件,每个都在改动前留有同目录同时间戳的备份,覆盖回去即回到改动前状态;新建的绑定表不影响任何既有逻辑,回滚时可保留(不影响旧功能)。
日志格式:<文件名>.bak-<日期>。