News

Better Auth 如何接入微信扫码登录

Better Auth 微信登陆接入最佳实践

Better Auth 如何接入微信扫码登录

本文将介绍如何使用用目前最好的鉴权框架 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. 申请应用

image-20250927115303026

Group 1 (4)

image-20250927115216067

3. 配置回调域名

当你的申请通过后,你会得到一个 appid 和 appsecret,以及需要配置上你的生产域名,需要注意的是,这里填写的格式不包含协议,比如 https://www.example.com 只需要填写 www.example.com 即可。另外 www.example.com 和 example.com 是不同的域名,无法通用

image-20250927115542494

配置 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>;

到这里,微信扫码登陆的接入就完成了。

image-20250927132353361

image-20250927132511487

如果有兴趣,可以参考下面的原理解析 ⬇️

文件位置参考:

  • 前端组件:src/components/WeChatQrLogin.tsx
  • 服务端插件:src/lib/plugins/wechat-oauth.ts
  • 公共样式(覆盖微信登录 iframe 样式):public/wechat-login.css

工作原理(端到端)

  • 第一步:加载微信脚本
  • 第二步:向你自己的后端要“授权 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)。

登录流程图

image-20250927132109912

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 频道

欢迎关注我的公众号,分享各种黑科技👋