先看这四条关键结论
这些约束决定了方案形态,建议在动工前先与公众号管理员逐条确认。
公司官网常被 PC / 外部浏览器访问,此时 wx6ff6319c3ed127f6 的网页授权无法唤起。PC 端需另在微信开放平台申请「网站应用」,用微信扫码登录(另一套 AppID)。两端通过 UnionID 统一身份 —— 前提是把该服务号绑定到同一个开放平台账号。本 Demo 对非微信浏览器做了检测与降级提示。
只有认证的企业主体服务号具备「网页授权获取用户基本信息」(snsapi_userinfo) 权限;订阅号(无论是否认证)不支持。权限不足时接口返回 48001 / 10005。请先在公众号后台「设置与开发 → 接口权限」确认该项状态。
gh_2fba6debb5b2 是服务号原始 ID,仅用于搜索/关注/后台识别,不参与 OAuth;授权链接用的是 AppID wx6ff6319c3ed127f6。此外还需 AppSecret(管理员在「基本配置」中查看/重置),本次未提供,且绝不能放进前端代码或 Git 仓库。
本次交付:完整 H5 官网(响应式、登录态、会员中心、环境检测)、可演示的模拟授权流程、以及可直接运行的 OAuth 服务端参考实现。默认 WX_MODE=demo 不调用微信接口;配置好域名、AppSecret 与网页授权域名后切到 live 即为真实登录。
技术架构
前端只负责展示与跳转,所有涉及 AppSecret 的动作全部收敛在服务端(BFF)。
PC / 外部浏览器分支:微信开放平台「网站应用」扫码登录 → 同一 UnionID → 同一会员体系(需另行申请 AppID)。
| 层 | 本次 Demo 选型 | 生产建议 | 理由 |
|---|---|---|---|
| 前端 | 原生 HTML / CSS / JS,零构建 | 保持零依赖,或换 Vue3 + Vite | H5 官网首屏优先,无构建链最省运维;业务复杂再上框架 |
| 服务端 | Node.js 18+ 内置 http,零依赖 | Express / NestJS + PM2,或云函数 / 微信云托管 | 参考实现零依赖便于阅读与部署;生产需日志、限流、结构化错误处理 |
| 会话 | 内存 Map + sid Cookie | Redis / 数据库会话表 + 签名 Cookie | 内存会话进程重启即失效,多实例无法共享 |
| 用户数据 | 内存 Map | MySQL / PostgreSQL / 云数据库,按 unionid 唯一索引 | 持久化身份、支持跨端统一与后续会员运营 |
| 接入层 | Node 直出静态 | Nginx HTTPS 反代 + CDN/COS 托管静态资源 | 静态资源走 CDN,Node 只处理 /wx /api |
项目目录与运行方式
# 项目根目录 h5_login/
├── public/ # H5 静态站点(可单独静态托管)
│ ├── index.html # 官网首页 + 登录弹窗 / 会员中心
│ ├── plan.html # 本方案页
│ ├── assets/
│ │ ├── styles.css # 官网样式(移动端优先)
│ │ ├── plan.css # 方案页样式
│ │ └── app.js # 登录态、环境检测、演示授权流程
│ └── MP_verify_XXXX.txt # 公众号域名校验文件(上线时放入)
├── server/
│ └── server.js # OAuth 回调 + 会话 + 静态服务(零依赖)
├── deploy/
│ └── nginx.conf.example # HTTPS / 反代 / 校验文件配置
├── Dockerfile # 容器化部署
├── package.json # npm start → node server/server.js
├── .env.example # 配置模板(复制为 .env)
└── .gitignore # 忽略 .env
# 本地运行
cp .env.example .env
npm start # http://localhost:3000
前端 app.js 中的 CONFIG.apiBase 默认为同源;若前后端分离部署,改成后端域名并同步处理 CORS 与 credentials: 'include'。生产环境建议由 /api/config 统一下发模式与公开配置,而不是硬编码在前端。
网页授权登录时序(OAuth 2.0)
snsapi_userinfo 会弹出授权页并返回昵称/头像;若只要 OpenID,可改用静默的 snsapi_base。
点击登录 → GET /wx/authorize?redirect=/。服务端生成随机 state(5 分钟有效、一次性),写入 wx_state Cookie,并记录登录后的回跳地址。redirect 必须校验为站内相对路径,防止开放重定向。
由服务端拼接(AppSecret 不出现在这里):
https://open.weixin.qq.com/connect/oauth2/authorize
?appid=wx6ff6319c3ed127f6
&redirect_uri=https%3A%2F%2Fwww.yourcompany.com%2Fwx%2Fcallback
&response_type=code
&scope=snsapi_userinfo
&state=STATE#wechat_redirect
redirect_uri 的域名必须与公众号后台「网页授权域名」完全一致(含 www 与否)。
用户同意后微信重定向到 /wx/callback?code=CODE&state=STATE。服务端比对 Cookie 中的 state、检查是否过期并立即删除;code 一次性、有效期 5 分钟。用户取消则走友好提示页。
GET https://api.weixin.qq.com/sns/oauth2/access_token
?appid=wx6ff6319c3ed127f6
&secret=APPSECRET // 仅服务端可见
&code=CODE
&grant_type=authorization_code
→ { access_token, openid, unionid?, scope, expires_in }GET https://api.weixin.qq.com/sns/userinfo
?access_token=...&openid=...&lang=zh_CN
→ { nickname, headimgurl, sex, city, province, country, unionid }
按 unionid(无则 openid)upsert 用户表。不要把 access_token、完整 openid 返回前端。
生成 sid,写 HttpOnly; Secure; SameSite=Lax Cookie,清掉 wx_state,302 回 /?login=success。前端调用 /api/user/me 拿脱敏资料并渲染登录态;会员中心等受保护内容由服务端会话鉴权控制。
服务端接口清单
| 路径 | 方法 | 作用 | 鉴权 / 备注 |
|---|---|---|---|
/api/config | GET | 下发公开配置(mode、AppID、scope) | 无;绝不含 AppSecret |
/wx/authorize | GET | 生成 state、写 Cookie、302 到微信 | redirect 站内白名单校验;demo 模式返回 503 |
/wx/callback | GET | 接收 code,换取身份并建立会话 | state + Cookie 双重校验、code 一次性 |
/api/user/me | GET | 返回当前登录用户(脱敏) | sid Cookie;未登录返回 loggedIn:false |
/api/user/logout | POST | 销毁会话、清 Cookie | sid Cookie |
/healthz | GET | 健康检查 / 探活 | 无;供负载均衡与监控使用 |
/* | GET | 静态站点与 MP_verify_* 校验文件 | 路径穿越防护 + MIME 白名单 |
微信公众平台后台配置清单
路径基于当前公众平台界面;不同账号类型菜单名称可能略有差异,以实际后台为准。
| # | 位置 | 要做什么 | 不做的后果 |
|---|---|---|---|
| 1 | 设置与开发 → 接口权限 | 确认「网页服务 → 网页授权获取用户基本信息」为已获得;账号为已认证服务号 | 48001 api unauthorized / 10005 无 scope 权限 |
| 2 | 设置与开发 → 基本配置 | 取得 AppSecret(管理员扫码,重置会使旧密钥失效),写入服务器 .env;把服务器出口公网 IP 加入 IP 白名单 | 调用 /cgi-bin/token 类接口报 40164 invalid ip |
| 3 | 公众号设置 → 功能设置 → 网页授权域名 | 填 www.yourcompany.com(不带协议、路径、端口;最多 20 个)。下载 MP_verify_XXXX.txt 放到 public/ 根目录,确保 https://域名/MP_verify_XXXX.txt 可访问后再提交 | 授权页提示「redirect_uri 参数错误」,微信内无法打开 |
| 4 | 公众号设置 → 功能设置 → JS 接口安全域名 | 如需微信分享 / 扫一扫等 JS-SDK 能力再配置(最多 5 个) | JS-SDK 签名校验失败 |
| 5 | 微信开放平台 open.weixin.qq.com | 绑定该服务号以获取 UnionID;PC 扫码登录另建「网站应用」并通过审核 | 官网 PC 端无法微信登录;多端身份无法统一 |
| 6 | 站点合规 | 上线前补齐《用户协议》《隐私政策》、ICP 备案与主体信息,头像/昵称按最小必要收集 | 审核与合规风险,微信内可能被拦截 |
网页授权域名只接受可公网访问的域名,且不识别端口/IP。开发阶段请用固定域名的 HTTPS 内网穿透(ngrok / cpolar / natapp)或先部署到测试服务器;也可用微信公众平台接口测试号(mp.weixin.qq.com/debug/cgi-bin/sandbox)拿独立测试 AppID/AppSecret 联调,避免改动正式号配置。测试号的网页授权域名配置以后台页面要求为准。
开发阶段与交付物
- 服务号认证状态 / 权限核验
- AppSecret 与域名归属
- 品牌名称、LOGO、真实文案
- 确定是否覆盖 PC 扫码登录
- 确定会话与用户存储方案
- 响应式 H5 官网 6 个区块
- 登录态 / 会员中心 / 退出
- 微信内外环境检测与降级
- 模拟授权四步流程
- 接入状态调试面板
- 授权入口 + state 防 CSRF
- code 换取 / 用户资料
- 会话 Cookie 与接口
- 静态托管 + 域名校验文件位
- Nginx / Dockerfile 模板
- 测试号 / 正式号授权联调
- 用户与会话落库
- 限流、日志脱敏、监控告警
- 隐私协议与备案
- PC 扫码登录(如需)
建议顺序:先把 PHASE 2/3 用测试号 + 内网穿透跑通一次真实授权,再切换到正式 AppID 与生产域名,避免反复改动正式服务号配置。
部署方案(三选一或分阶段递进)
| 方案 | 适用 | 做法 | 真实微信登录 | 成本 / 门槛 |
|---|---|---|---|---|
| A 演示预览 | 内部评审、给客户看效果 | 本地 npm start;或仅把 public/ 作为纯静态页托管(COS+CDN / Vercel / Netlify / 发布为应用)。手机预览配 HTTPS 内网穿透 | 否,走模拟流程 | 最低,无需域名与备案 |
| B 联调测试 | 开发期真实授权验证(推荐先做) | 测试号 AppID/AppSecret + 固定域名 HTTPS 隧道 + 本地/测试服务器 Node;WX_MODE=live | 是(测试号) | 低,不污染正式号配置 |
| C 正式上线 | 对外运营 | 已备案域名 + SSL + 云服务器/容器 + Nginx 反代 + 数据库;或静态 CDN + Serverless 云函数 / 微信云托管承载 OAuth 回调 | 是(正式号) | 需备案、认证服务号、证书与运维 |
方案 C · 标准云服务器上线步骤
- 域名 ICP 备案 → DNS 解析到服务器 → 申请并部署 SSL 证书(腾讯云/阿里云免费证书或 Let's Encrypt)。
- 部署代码:
git clone→cp .env.example .env填WX_APPSECRET、PUBLIC_BASE_URL=https://www.yourcompany.com、SESSION_SECRET、WX_MODE=live→npm start由 PM2 守护,或用Dockerfile构建镜像交给容器服务。 - 按
deploy/nginx.conf.example配置 HTTPS、/wx/api反代到 3000 端口、静态资源直出,并确保MP_verify_XXXX.txt可从域名根目录访问。 - 公众号后台配置「网页授权域名」与 IP 白名单(见第 06 节),保存后等待生效。
- 微信内打开
https://www.yourcompany.com实测:授权弹窗 → 同意 → 回跳 → 头像昵称 → 刷新保持登录 → 退出 → 再次登录。 - 接入日志、错误告警、备份与
/healthz探活;上线后按第 10 节清单验收。
方案 C 的两种替代运行形态
| 形态 | 优点 | 注意点 |
|---|---|---|
| Serverless(云函数 + API 网关 / 静态 CDN) | 免运维、按量计费;静态站点与 OAuth 回调解耦 | 云函数出口 IP 不固定,依赖 IP 白名单的接口需配 NAT 网关固定出口;会话必须存外部存储,不能放内存 |
| 微信云托管 / CloudBase | 与微信生态集成度高、免证书与服务器运维 | 网页授权域名仍需自有已备案域名并绑定;默认分配域名不能替代域名归属校验 |
只要没有服务端,AppSecret 就无处安放,也无法用 code 换取身份。方案 A 的静态链接只能演示交互;要真实登录,至少需要一个 HTTPS 接口承载 /wx/callback。
环境变量与安全基线
| 变量 | 示例 / 来源 | 说明 |
|---|---|---|
WX_APPID | wx6ff6319c3ed127f6 | 可公开;用于拼授权链接 |
WX_APPSECRET | 公众号后台「基本配置」 | 绝密;仅服务端环境变量 / 密钥管理,不进镜像、仓库、前端、日志 |
PUBLIC_BASE_URL | https://www.yourcompany.com | 拼接 redirect_uri;须与网页授权域名一致 |
SESSION_SECRET | 32 位以上随机串 | 生产用于会话签名 / 加密,定期轮换 |
WX_MODE | demo / live | 环境隔离,避免测试流量走正式号 |
PORT | 3000 | 平台注入,服务监听 0.0.0.0 |
- state:服务端随机生成、一次性、5 分钟过期,并与 Cookie 双重校验,防 CSRF 与重放。
- redirect:只接受站内相对路径(拒绝
//、反斜杠、换行),防开放重定向。 - Cookie:
HttpOnly+Secure(HTTPS)+SameSite=Lax;多个 Cookie 必须一次性以数组下发,避免被后一次Set-Cookie覆盖。 - 数据最小化:不向前端返回完整
openid/unionid/access_token;头像昵称按需收集并在隐私政策中说明。 - 运维:全站 HTTPS、日志脱敏、接口限流、依赖与证书定期更新、数据库备份。
上线验收清单与常见错误码
验收清单
- 服务号为已认证服务号,「网页授权获取用户基本信息」权限已生效
- 域名已备案、全站 HTTPS、证书有效,
MP_verify_XXXX.txt可从域名根目录访问 - 公众号后台网页授权域名与
redirect_uri域名完全一致(含 www) - 服务器出口公网 IP 已加入 IP 白名单;AppSecret 只存在于服务端环境变量
- 微信内:点击登录 → 授权页 → 同意 → 回跳 → 显示昵称头像 → 刷新仍为登录态 → 退出后受保护内容不可见
- 非微信浏览器:给出明确降级提示,或已接入开放平台网站应用扫码登录
state重放、code复用、取消授权、网络超时均有友好提示,无空白页- 《用户协议》《隐私政策》、ICP 备案号、真实公司信息与联系方式已替换 Demo 占位内容
- 日志与监控可见授权成功率、错误码分布;
/healthz探活正常
常见错误码速查
| 错误 | 含义 | 处理 |
|---|---|---|
48001 / 10005 | 接口未授权 / 无 scope 权限 | 确认认证服务号身份与网页授权权限;检查认证是否生效 |
10003 / redirect_uri 参数错误 | 回调域名与后台配置不一致 | 核对网页授权域名、www、协议与 URL 编码 |
40029 / 40163 | code 无效 / 已被使用 | code 一次性且 5 分钟有效,重新发起授权;检查多实例重复消费 |
40164 | 调用方 IP 不在白名单 | 把服务器出口公网 IP 加入公众号后台 IP 白名单,等待生效 |
40001 / 42001 | access_token 无效或过期 | 区分「网页授权 token」与「全局 token」;全局 token 单点缓存,避免多服务互相刷新 |
| 微信内打不开 / 被拦截 | 域名未备案、证书异常或外链规范问题 | 换已备案域名、修复 HTTPS,必要时在公众平台申诉 |
备案域名 + 证书 → 部署 Node 服务与静态站 → 放置校验文件 → 后台配网页授权域名 / IP 白名单 → 填写 AppSecret 切 WX_MODE=live → 微信内实测 → 补齐合规文案。完成这七步,本 Demo 即可变成可用的官网登录。