
Hi,我是松柏。
AI 时代,大家或多或少都有了一些做网站的能力。
不知道大家有没有尝试过给自己的网站增加一个登录功能呢?比如微信扫码登录。
我最近就在给我的网站增加微信扫码登录能力。
但经过一番调研发现,微信官方的扫码登录方案要么要营业执照,要么每年交 300 块认证费。
我这个简单的小项目犯不着搞这些复杂的东西。
最终经过一番查找,我发现用微信小程序就能完美绕过这些限制。
效果就是网站弹出一个小程序码:

手机微信扫一扫,网页端自动登录:

点赞关注,接下来我们就一起看看如何实现吧~
在开始之前,先来看下主流方案的对比表格:
方案 | 前置条件 | 费用 | 体验 |
|---|---|---|---|
微信开放平台(网站应用) | 企业营业执照 + 开发者资质认证 | 300 元(一次性) | 最标准,扫码直接授权 |
公众号扫码登录 | 认证服务号 | 300 元/年 | 扫码关注后登录 |
小程序 | 个人实名即可注册 | 0 元 | 扫码进小程序,确认后登录 |
小程序不认证的代价是不能被搜索、不能开通支付、也不能被分享。但咱们就是拿它做个登录跳板,用户只通过扫码进入,完全不需要这些功能。
整体流程大概是这样:

简单看下核心步骤:
getwxacodeunlimit 接口生成,scene 字段里塞的就是 ticket,用户扫码后小程序从 scene 里取出来wx.login() 拿到 code,连同 ticket 一起发给后端,后端用 code 调微信的 code2session 接口换到 openid,完成用户身份绑定核心就是,通过小程序从微信获取唯一标识,绑定到我们的网站里,实现一个唯一标识(微信号)对应一个用户。
有个细节得说明一下,小程序从微信获取到的唯一标识叫 openid,openid 只在一个应用里唯一。 什么意思呢? 就是说,假如一个用户在我的小程序里的 openid 是 123,那么我的小程序里不会有其他用户的 openid 是 123 了,但是不影响别人的小程序里有用户的 openid 是 123。
1)注册个人小程序
去微信公众平台注册一个小程序账号,只需要实名 + 微信扫码验证就可以了:

注册完拿到 AppID 和 AppSecret,后面要用。
这里用 Node.js 示例,其他语言同理。
生成 ticket + 小程序码:
// POST /api/auth/qr/create
async function createQrLogin() {
const ticket = crypto.randomBytes(16).toString('hex') // 32位 hex
// 存 Redis,5 分钟过期
await redis.set(`login:${ticket}`, JSON.stringify({ status: 'pending' }), 'EX', 300)
// 调微信接口生成小程序码
const accessToken = await getAccessToken(APP_ID, APP_SECRET)
const res = await fetch(
`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scene: ticket,
page: 'pages/login/login', // 小程序里的登录确认页
check_path: false,
width: 280,
}),
}
)
const imageBuffer = await res.arrayBuffer()
return { ticket, qrcode: Buffer.from(imageBuffer).toString('base64') }
}
这里有三个关键点:
scene 最长只能 32 个可见字符,所以用 16 字节 hex 刚好check_path: false 让生成码时不校验页面是否已上线,方便开发阶段调试轮询状态接口:
// GET /api/auth/qr/status?ticket=xxx
async function checkStatus(ticket: string) {
const data = await redis.get(`login:${ticket}`)
if (!data) return { status: 'expired' }
const { status, token } = JSON.parse(data)
if (status === 'confirmed') {
await redis.del(`login:${ticket}`) // 一次性,用完删掉
return { status: 'confirmed', token }
}
return { status: 'pending' }
}
确认登录接口(给小程序调的):
// POST /api/auth/mini/confirm
async function confirmLogin(ticket: string, code: string) {
// 1. 校验 ticket 存在且未过期
const data = await redis.get(`login:${ticket}`)
if (!data) throw new Error('二维码已过期')
// 2. 用 code 换 openid
const session = await code2Session(APP_ID, APP_SECRET, code)
const { openid } = session
// 3. 查找或创建用户,生成登录 token
const user = await findOrCreateUser(openid)
const token = generateToken(user)
// 4. 更新 ticket 状态
await redis.set(`login:${ticket}`, JSON.stringify({ status: 'confirmed', token }), 'EX', 60)
return { success: true }
}
code2Session 就是调微信的 https://api.weixin.qq.com/sns/jscode2session,传 appid、secret、js_code,换回 openid。
小程序只需要一个"登录确认页",用户扫码后会自动跳到这个页面。
// pages/login/login.js
Page({
onLoad(options) {
// 从小程序码的 scene 里取 ticket
const ticket = decodeURIComponent(options.scene || '')
if (!ticket) {
wx.showToast({ title: '二维码无效', icon: 'error' })
return
}
this.confirmLogin(ticket)
},
async confirmLogin(ticket) {
try {
// 1. wx.login 拿 code
const { code } = await wx.login()
// 2. 请求你的后端 confirm 接口
const res = await wx.request({
url: `https://your-domain.com/api/auth/mini/confirm`,
method: 'POST',
data: { ticket, code },
})
if (res.data.success) {
wx.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
}
} catch (err) {
wx.showToast({ title: '登录失败', icon: 'error' })
}
},
})
页面 UI 可以做得简单一点:显示个 loading 动画 + "正在确认登录..."的文案就行,用户感知上就是扫完码等一秒就好了。
// 伪代码,React 示例
function LoginModal() {
const [qrcode, setQrcode] = useState('')
const [ticket, setTicket] = useState('')
useEffect(() => {
// 请求后端生成小程序码
fetch('/api/auth/qr/create', { method: 'POST' })
.then(res => res.json())
.then(({ ticket, qrcode }) => {
setTicket(ticket)
setQrcode(qrcode)
})
}, [])
useEffect(() => {
if (!ticket) return
// 每 1.5 秒轮询
const timer = setInterval(async () => {
const res = await fetch(`/api/auth/qr/status?ticket=${ticket}`)
const data = await res.json()
if (data.status === 'confirmed') {
clearInterval(timer)
// 拿到 token,登录成功!
login(data.token)
} else if (data.status === 'expired') {
clearInterval(timer)
// 提示过期,让用户刷新
}
}, 1500)
return () => clearInterval(timer)
}, [ticket])
return (
<div>
<img src={`data:image/png;base64,${qrcode}`} />
<p>请用微信扫一扫</p>
</div>
)
}
scene 只有 32 字符。不要想着往里塞 JWT 或者 URL,只放一个 ticket ID 就行,其他信息让后端通过 ticket 去 Redis 里查。
小程序必须已发布。getwxacodeunlimit 生成的码指向的是正式版小程序。如果你的登录页还没发布上线,用户扫了会报"页面不存在"。开发阶段可以设 check_path: false 先把码生成出来,但真机测试还是得至少上个体验版。
本地调试怎么搞? 不需要真机扫码。在微信开发者工具里添加一个编译模式,启动页面填 pages/login/login,启动参数填 scene=你的ticket,就能模拟扫码进入的效果。
真机测试走局域网。手机访问不了你电脑的 localhost,小程序里的 API 地址要改成你电脑的局域网 IP(比如 192.168.1.8:3000),同时开发者工具里关掉"域名校验"。
access_token 要缓存。微信的 access_token 有效期 2 小时,别每次生成码都重新获取,用 Redis 缓存一下。
总结一下:个人开发者想给网站加微信扫码登录,不用花钱、不用营业执照,注册一个个人主体的小程序就够了。核心就是 getwxacodeunlimit + wx.login + 一个 Redis ticket,整套逻辑并不复杂。
下期再见,拜拜👋🏻