如何实现微信关注公众号自动登录功能 ?
背景:最近和我的小伙伴们基于 ChatGPT 开发了一个脑图生成工具。而登陆一直用的短信验证,比较消耗资金,于是就准备接入微信登陆。

1. 前置条件
通过审核的微信公众号
有公网IP的web服务器
核心概念:当用户扫描二维码时,微信服务会向我们配置url发起post请求。这个请求携带着用户的一些操作数据。我们可以在生成的微信公众号二维码中附带一些数据,这些数据也会一起被发送。
如果上面条件满足则可以进入下一步。
2. 微信公众号开发平台配置
2.1 配置IP白名单
配置完IP白名单才能够去获取access_token 用于访问微信服务。这个白名单应该是你公网IP地址。

2.2 配置微信推送消息的接口

点击修改配置
- 服务器地址URL:当微信接受到用户向公众号发送的消息时,会通过post请求推送这个消息。值得注意的是,当首次配置时,回通过get请求去验证接口。
- 令牌:随便填写
- 消息加密解密密钥:随机生成
- 消息加密方式:我这里选择明文,就用不到上 令牌和密钥了。
这里实现一下处理微信第一配置url的请求的验证逻辑,我们只要返回 echostr就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 用来接收微信的推送消息
* tousername: 开发者微信号,即公众号的原始ID
* fromusername: 发送方帐号,即用户的OpenID
* createtime: 消息创建时间,整型,表示自1970年以来的秒数
* msgtype: 消息类型,即事件类型,这里为event
* event: 事件类型,这里为SCAN,表示用户已经扫描了带场景值的二维码
* eventkey: 事件KEY值,即场景值,这里为123123test
* ticket: 二维码的ticket,可用于换取二维码图片
*/
export async function main(ctx: FunctionContext) {
const { echostr } = ctx.query; // 从请求url中获取这个参数
const { event, fromusername, ticket, eventkey} = ctx.body.xml; // 这个数据是微信推送的用户操作数据
// --- 这里我们可以处理对应的逻辑,这里逻辑暂时不写。
// 返回 echostr 就代表验证通过,微信需要返回这个数据
return echostr;
}
3. 生成带参数的微信二维码
3.1 获取访问微信服务的 access_token
我们需要知道 appId 和 key,

接下来接是发起请求得到 access_token.
1
2
const res = await fetch(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${key}`);
const data = await res.json(); //这样就能够获取到,access_token
3.2 获取生成二维码的ticket
上面一步,我们已经获得了 access_token, 当然这个 token 请求次数有限,能缓存使用尽量缓存,我这里就不缓存了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取生成二维码的ticket和设置生成二维码携带的参数
async getTicket(uid: string) {
const access_token = await this.getAccessToken();
const TICKET_URL = `https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${access_token}`;
const body = {
expire_seconds: 120, // 过期时间
action_name: "QR_STR_SCENE", // 名称
action_info: {
scene: {
scene_str: uid, // 这里就是携带的参数,通过这个参数我们可以唯一标识一个人
}
}
};
const res = await fetch(TICKET_URL, {
method: "POST",
body: JSON.stringify(body)
});
return await res.json(); // 这里我们就可以得到 ticket 和 二维码过期时间
}
3.3 通过ticket 获取二维码
1
<img style="width: 200px; height: 200px" src="https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}">
这里我们就能够获取到二维码。
4. 处理微信推送的消息
当用户扫描时微信会推送携带用户的openID 和我们为二维码设置的参数到我们配置的url。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import cloud from '@lafjs/cloud'
const db = cloud.database();
/**
* 用来接收微信的推送消息
* tousername: 开发者微信号,即公众号的原始ID
* fromusername: 发送方帐号,即用户的OpenID
* createtime: 消息创建时间,整型,表示自1970年以来的秒数
* msgtype: 消息类型,即事件类型,这里为event
* event: 事件类型,这里为SCAN,表示用户已经扫描了带场景值的二维码
* eventkey: 事件KEY值,即场景值,这里为123123test
* ticket: 二维码的ticket,可用于换取二维码图片
*/
/**
* {
xml: {
tousername: [ 'gh_462fd94462f8' ],
fromusername: [ 'oX2TM53cBZk229DDBJ5bR9xc8YVaw' ],
createtime: [ '16799082520' ],
msgtype: [ 'event' ],
event: [ 'SCAN' ],
eventkey: [ '123123test' ],
ticket: [
'gQGk7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyRXpGMmtKdkFmY0QxNDh2eHhBY0MAAgSQXiFkAwR4AAAA'
]
}
}
*/
export async function main(ctx: FunctionContext) {
const {echostr} = ctx.query;
const { event, fromusername, ticket, eventkey} = ctx.body.xml;
// 如果是扫码登陆的消息
if (event[0] === "SCAN") {
// 先通过wx_id判断是用户之前是否已经登陆过
let { data: user } = await db.collection("user")
.where({wx_id: fromusername[0]}).getOne();
if (!user) {
// 将微信数据保存到用户列表中
const { id } = await db.collection("user").add({
wx_id: fromusername[0],
nickname: fromusername[0],
phone: "",
created_at: new Date(),
created: Date.now(),
});
const r = await db.collection('user').where({ _id: id }).getOne()
user = r.data
}
// 这里可以使用缓存在做,这里的目的为前端轮训提供查询条件。因为在生成二维码会给用户返回uid 作为后续查询登录状态的参数
await db.collection("wx_ticket_cache")
.where({
uid: eventkey[0]
}).update({
use: true,
wx_id: fromusername[0]
});
}
return echostr;
}
5. 处理前端轮训
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import cloud from '@lafjs/cloud'
const db = cloud.database()
export async function main(ctx: FunctionContext) {
const { uid } = ctx.request.body;
// 1.uid -> wx_id 这里可以是从缓存中获取
let { data: ticketCache } = await db.collection("wx_ticket_cache").where({
uid
}).getOne();
// 如果还没有扫码,use是用来判断扫码登录是否成功
if (!ticketCache.use) {
return {
code: 1,
data: {
user: null
}
};
}
// 如果扫码后,通过wx_id获取用户信息
const { data: user } = await db.collection("user").where({
wx_id: ticketCache.wx_id
}).getOne();
// 生成token
delete user['password'];
// 默认 token 有效期为 365 天
const expire = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365
const access_token = cloud.getToken({
uid: user._id,
exp: expire,
});
return {
code: 1,
data: {
access_token,
expire,
user
}
};
}