
本文将介绍如何使用用目前最好的鉴权框架 Better Auth 接入微信扫码登录。
better auth 原生是不支持微信登陆的,不过 better auth 的扩展性非常好,只需要写一个自定义的插件处理微信登录的逻辑即可
export const auth = betterAuth({
baseURL: getBaseUrl(),
appName: 'WeChat Minimal Demo',
database: memoryAdapter({}),
// To use a real DB:
// database: drizzleAdapter(await db, { provider: 'pg' | 'mysql' | 'sqlite' }),
// and remove memoryAdapter above.
session: {
cookieCache: { enabled: true, maxAge: 60 * 60 },
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
freshAge: 0,
},
socialProviders: {},
account: {
accountLinking: { enabled: true, trustedProviders: ['wechat'] },
},
plugins: [wechatOAuth()] // 在这里注册你的自定义插件逻辑
}
- 插件就是返回一个 BetterAuthPlugin 对象,并在 betterAuth
({ plugins: [...] })
中注册。 - 主要做两件事:
- 声明要暴露的接口路由(endpoints),例如登录入口、回调地址。
- 在路由处理函数里编写你的业务逻辑(如鉴权、查库、创建会话、重定向等)。
- 每个路由用 createAuthEndpoint(path, options, handler) 定义;handler 通过 ctx 访问到 baseURL、logger、适配器、session、cookie 等能力。
- 插件可选接收 options(如第三方平台的 appId、密钥),让逻辑可配置、可复用。
最小插件示意
import type { BetterAuthPlugin } from 'better-auth';
import { createAuthEndpoint } from 'better-auth/api';
export function wechatOAuth(): BetterAuthPlugin {
return {
id: 'wechatOAuth',
endpoints: {
hello: createAuthEndpoint('/hello', { method: 'GET' }, async (ctx) => {
return ctx.json({ ok: true, userId: ctx.session?.userId ?? null });
}),
},
};
}
- 注册使用:plugins: [myPlugin()]
- 将微信登录逻辑(生成扫码链接、处理回调、创建/关联用户、设置会话与重定向)写成上述“endpoints”里的两个路由即可(一个发起登录,一个接收回调)。
接下来,为了接入微信登录,我们需要去微信的平台上申请应用权限。
有个需要注意的地方是,扫码登陆和h5拉起登陆在微信的平台是不同的两个平台,h5拉起登陆是在微信公众平台申请的,而扫码登陆是在微信开放平台申请的。并且他们是完全独立的两个平台,包括管理的key以及回调域名,返回的用户信息等都是不同的。
微信登陆的文档对此的描述也非常模糊,我第一次接入的时候,在这一块浪费了很多时间。比如我一开始以为扫码拿到的 access_token 和 h5拉起拿到的 access_token 是通用的,结果发现不是。扫码的 access_token 无法用于h5拉起的信息接口。其次,扫码的 openid 和 h5拉起的 openid 也是不同的。
本文主要讲扫码登陆的部分,关于 h5 拉起登陆,后续有机会再更新
对比项 | 扫码登陆 | H5拉起登陆 |
---|---|---|
使用场景 | 电脑端登陆 | 手机端登陆 |
申请平台 | 微信开放平台 | 微信公众平台 |
申请入口 | https://open.weixin.qq.com/ | https://mp.weixin.qq.com/ |
Access Token | 独立的,不通用 | 独立的,不通用 |
OpenID | 独立的,不通用 | 独立的,不通用 |
管理密钥 | 独立管理 | 独立管理 |
回调域名 | 独立配置 | 独立配置 |
扫码登陆的申请
1. 域名备案
首先,你的域名需要完成备案,才可以申请微信的扫码登陆
2. 申请应用
3. 配置回调域名
当你的申请通过后,你会得到一个 appid 和 appsecret,以及需要配置上你的生产域名,需要注意的是,这里填写的格式不包含协议,比如 https://www.example.com 只需要填写 www.example.com 即可。另外 www.example.com 和 example.com 是不同的域名,无法通用
配置 better Auth
参考仓库: https://github.com/zreren/better-auth-wechat-example
# 应用基础URL
NEXT_PUBLIC_BASE_URL=http://localhost:3005
# 微信开放平台(网站应用)配置
WECHAT_APP_ID="" # 微信开放平台应用ID
WECHAT_APP_SECRET="" # 微信开放平台应用密钥
WECHAT_SYNTHETIC_EMAIL_DOMAIN="wechat.local" # 合成邮箱的域名,由于 better auth 的邮箱字段为非空,我们必须使用一个特定的格式进行占位,方便我们后续区分
# Better Auth 密钥(用于签名 cookies/tokens/state)
# 使用以下命令生成: openssl rand -base64 32
BETTER_AUTH_SECRET=""
# 调试标志
WECHAT_DEBUG=false # 后端调试开关
NEXT_PUBLIC_WECHAT_DEBUG=false # 前端调试开关
配置完之后,即可使用封装好的组件
import { WeChatQrLogin } from "@/components/WeChatQrLogin";
<div style={{ marginTop: 16 }}>
在这里配置你登陆后回调的地址
<WeChatQrLogin callbackUrl="/dashboard" />
</div>;
到这里,微信扫码登陆的接入就完成了。
如果有兴趣,可以参考下面的原理解析 ⬇️
文件位置参考:
- 前端组件:src/components/WeChatQrLogin.tsx
- 服务端插件:src/lib/plugins/wechat-oauth.ts
- 公共样式(覆盖微信登录 iframe 样式):public/wechat-login.css
工作原理(端到端)
- 第一步:加载微信脚本
- 组件挂载后加载 https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js。
- 这个脚本在全局 window 上挂载一个构造函数:window.WxLogin。
- 第二步:向你自己的后端要“授权 URL” - 组件通过
authClient.$fetch('/sign-in/wechat', { method: 'POST', body: { disableRedirect: true, callbackURL }})
请求后端。- 服务端插件根据你的 WECHAT_APP_ID/SECRET、回调地址等,生成微信开放平台“二维码登录”的授权 URL(以 https://open.weixin.qq.com/connect/qrconnect 开头),并返回给前端。 - 第三步:把授权 URL“拆开”给 WxLogin - 组件解析返回的授权 URL,取出 appid、redirect_uri、scope、state 等参数。- 调用
new window.WxLogin({...})
,在组件的容器里渲染一个 iframe,微信官方会在 iframe 中生成二 维码。 - 第四步:用户扫码 + 微信回调
- 用户用手机微信扫码并确认后,微信会携带 code 回跳到你的后端 GET /oauth2/callback/wechat。
- 服务端用 code 向微信换取 access_token 和用户信息,创建/关联用户与会话,并设置 session cookie。
- 第五步:前端发现“登录成功”
- 组件每 1.5 秒调用 GET /get-session 检查是否已登录。
- 一旦拿到用户,清除轮询并跳转到 callbackUrl(例如 /dashboard)。
登录流程图
sequenceDiagram
participant 用户
participant 前端
participant 后端
participant 微信
前端->>前端: 加载微信脚本 wxLogin.js
前端->>后端: POST /sign-in/wechat<br/>请求授权URL
后端-->>前端: 返回授权URL
前端->>前端: WxLogin 渲染二维码
用户->>微信: 扫码授权
微信->>后端: GET /oauth2/callback/wechat<br/>携带code参数
后端->>微信: 用code换取access_token
微信-->>后端: 返回用户信息
后端->>后端: 创建/更新用户会话
loop 轮询检测
前端->>后端: GET /get-session
后端-->>前端: 返回会话状态
end
前端->>前端: 检测到会话<br/>跳转 callbackUrl
WxLogin 是什么,怎么来的
- WxLogin 是微信开放平台在网页扫码登录时提供的一个全局构造函数。
- 它不是你项目里的代码,而是由外部脚本 wxLogin.js 注入进来的。
- 只有在加载了脚本后(也就是组件挂载后),window.WxLogin 才存在。
- 组件写法示意:
- await loadWeChatScript() 加载脚本
new window.WxLogin({ id, appid, redirect_uri, scope, state, ... })
初始化二维码
- 你不需要自己实现二维码绘制,WxLogin 会注入一个 iframe 并在里面渲染二维码。
组件的关键点
- 参数
- callbackUrl?: string 登录成功后跳转地址,默认 /dashboard。
- 状态与提示
- 加载脚本、拉取授权 URL 时显示“Loading…”,出错时在组件内显示错误信息。
- 轮询会话
- 使用 GET /get-session 每 1.5 秒轮询一次。
- 检测到用户后清除轮询并跳转。
- 尺寸与样式 - 组件会把 iframe 固定到约 300×400 的尺寸,并通过 MutationObserver/ResizeObserver 兜底确保尺寸不 被覆盖。- 你可以通过 public/wechat-login.css 定制微信 iframe 内的二维码等样式,组件会把这个 CSS 地址作为 href 传给 WxLogin。
- 清理
- 组件卸载时会清除定时器并断开观察器,防止内存泄漏。
服务端插件做了什么(概念)
- 暴露 POST /sign-in/wechat:- 生成 state(防 CSRF),组装微信授权 URL(appid、redirect_uri、scope、state、lang),并返回给 前端。
- 处理 GET /oauth2/callback/wechat:
- 校验 code 与 state;用 code 向微信换取 access_token、openid 等。
- 再用 access_token 拉取用户信息(昵称、头像、unionid 等)。
- 将用户与账号(provider=wechat)进行创建或关联,创建会话并设置 cookie,然后 302 跳转回前端页面。
- 辅助逻辑 - 如果微信未提供邮箱,会生成一个伪邮箱:
{id}@{WECHAT_SYNTHETIC_EMAIL_DOMAIN}
,用于满足用户表的唯 一/非空要求。- 提供了服务端调试日志(WECHAT_DEBUG=true 时更详细)。
对应代码位置:
- src/lib/plugins/wechat-oauth.ts
- src/lib/auth.ts 中注册该插件
为什么要用 state,为什么要自己轮询
- state:OAuth 标准推荐参数,用于把前端“期望的回调 URL”等上下文安全地绑定到登录流程,并防止 CSRF。
- 轮询:WxLogin 在微信 iframe 内完成授权过程,前端页面本身不会自动刷新。为了“知道用户已经登录”,我们 选择“轮询会话”这种简单稳定的方式,一旦有会话就跳转。
域名与回调的坑
- 微信开放平台要求 redirect_uri 的域名必须和你在微信应用后台配置的一致。
- 本地开发时常见做法:- 使用内网穿透/反向代理生成公网域名(如 https://your-dev.example.com),并在微信后台把这个域名 配上。- 把 NEXT_PUBLIC_BASE_URL 指向这个可访问的域名端口。
- 如果域名不匹配,微信会在回调阶段直接报错。
完整示例(前端片段)
-
按钮触发式展示,避免初始加载外部脚本与轮询:
- 页面:
src/app/page.tsx
- 用法:
- 页面:
import { WeChatQrLogin } from "@/components/WeChatQrLogin";
// 在你的组件中:
{
open && <WeChatQrLogin callbackUrl="/dashboard" />;
}
常见问题排查
- 页面一直显示 Loading…
- 检查是否能访问微信脚本 wxLogin.js(可能被网络拦截)。
- 打开 NEXT_PUBLIC_WECHAT_DEBUG=true 查看控制台调试日志。
- 报错“Missing appid/redirect” - 多半是 POST /sign-in/wechat 没有返回完整的授权 URL。检查服务端环境变量 WECHAT_APP_ID/SECRET、 NEXT_PUBLIC_BASE_URL 是否正确,尤其是 redirect_uri 的域名。
- 扫码后不跳转 - 看网络面板 GET /get-session 是否返回了用户;如果没有,说明服务端回调阶段没有成功创建会话。查看 服务端日志(WECHAT_DEBUG=true)。
- 样式太挤/二维码太小
- 修改 public/wechat-login.css(例如 .impowerBox .qrcode 的宽高),组件会自动把它传给 WxLogin。
安全与生产建议
- 使用真实数据库替换内存适配器(内存只适合 Demo)。
- 及时轮换泄露过的密钥;WECHAT_APP_SECRET 不要提交到仓库。
- 回调域名只配置必要的少数几个,开启 HTTPS。
- 若需要防止同域多页面重复轮询,可用可见性 API 或全局登录管理统一协调。
欢迎关注我的 Youtube 频道
欢迎关注我的公众号,分享各种黑科技👋