node服务实现微信扫码登录和注册
# 实现思路
微信扫码登录和注册,这个功能需要微信提供接口,在我调研之后,发现需要微信的企业用户,才有开通这个接口的资格。所以这里我采用了曲线的方式,易登,这个平台提供了微信扫码的服务的回调的转发,所以我才可以实现这个功能
# 2024 年 06 月更新
易登也被微信禁止这样转发了,所以这个功能现在下线了
# 准备环节
这里我准备了一张表 qr_code_table 用来记录每次二维码生成的唯一 code,和这个 code 的当前状态,根据这个 code 和状态,从而接口进行不同的操作
# 生成二维码
调用微信(易登)的生成二维码的接口,拿到二维码后,插入一条数据 { code, status: 'init' } 到表 qr_code_table
// 生成 二维码 的接口
router.post('/generateQRCode', async (req, res) => {
try {
const ydUrl = `https://yd.jylt.cc/api/face/tempUserId?secret=102e2171&width=400`;
// 生成二维码
const { success, data, msg } = await fetchRequest(ydUrl, 'get', {});
if (success) {
// 在 qr_code_table 新增一条数据
const qr_code = data.tempUserId;
const insertSql = `insert into ${DB_NAME}.qr_code_table (qr_code, status) values ("${qr_code}", "init") `;
await runSql(insertSql);
res.send({ success: true, data });
} else {
res.send({ error: msg });
}
} catch (e) {
console.error('e', e);
res.send({ error: '二维码生成失败' });
}
});
# 用户取消扫码
这个简单,记得要把表 qr_code_table 中对应的 code 的状态 status 置为 canceled
// 手动取消扫描二维码
router.post('/cancelReadQRCode', async (req, res) => {
const { qrCode } = req.body;
try {
const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='canceled' WHERE qr_code="${qrCode}"`;
await runSql(updateQrCodeSql);
res.send({ success: true });
} catch (e) {
console.error('e', e);
res.send({ error: '' });
}
});
# 持续问询 前端的登录二维码 的 状态
正常的扫码流程,这个过程必定是持续的,所以这里采用长连接的方式,这里是前端持续的查询这个接口,所以这个接口需要持续提供 60 秒的数据传输
什么时候停止呢? code 二维码的状态发生了变更,就会停止
怎么停止呢? 其他接口更改了表
qr_code_table中对应的 code 的状态 status,这个时候就会进行状态判断,从而告诉前端当前扫码登录进行的状态用户点击了确认登录:从用户表内取出当前的微信扫码登录的用户信息,生成 jwt,然后一起返回给前端,前端会拿着用户信息和 jwt 调用登录的接口,从而完成登录
超时、取消等其他状态:修改表
qr_code_table中对应的 code 的状态 status,结束长连接
// SSE 长连接接口 持续问询 前端的登录二维码 的 状态
router.get('/checkQRCodeStatus/:qrCode', async (req, res) => {
const { qrCode } = req.params;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const checkStatus = async () => {
try {
const querySql = `select status, user_id from ${DB_NAME}.qr_code_table where qr_code = "${qrCode}"`;
const result = await runSql(querySql);
const { status, user_id } = result[0];
if (result.length > 0) {
// 确认登录
if (status === 'confirmed') {
const queryUserSql = `select user_name, user_type, user_id, deadline_time, score from ${DB_NAME}.user_table where user_id="${user_id}" `;
const userInfo = await runSql(queryUserSql);
const jwt = generateJWToken(userInfo[0]);
res.write(
`data: ${JSON.stringify({
status,
token: jwt,
userInfo: userInfo[0],
})} \n\n`,
);
res.end(); // 断开连接
} else if (status === 'success') {
// 扫描成功
setTimeout(checkStatus, 1000); // 每1秒检查一次状态,不断开连接
} else if (status === 'failed') {
// 扫描失败
res.write(`data: ${JSON.stringify({ status })} \n\n`);
res.end(); // 断开连接
} else if (status === 'canceled') {
// 取消登录
res.write(`data: ${JSON.stringify({ status })} \n\n`);
res.end(); // 断开连接
} else if (status === 'init') {
// 未被扫描
setTimeout(checkStatus, 1000); // 每1秒检查一次状态
} else if (status === 'expired') {
// 超时间了,不在轮询了
res.write(`data: ${JSON.stringify({ status })} \n\n`);
res.end(); // 断开连接
} else {
// 其他情况 也返回出去,但是断开连接
res.write(`data: ${JSON.stringify({ status })} \n\n`);
res.end(); // 断开连接
}
} else {
res.end();
}
} catch (error) {
console.error(error);
res.end();
}
};
checkStatus();
// 60 秒后关闭连接
setTimeout(() => {
res.write(`data: ${JSON.stringify({ status: 'expired' })} \n\n`);
res.end();
}, 60000);
});
# 用户扫码的回调
在这个接口里,记录了用户扫码的各种回调
取消扫码,就直接更新表
qr_code_table中对应的 code 的状态 status 为 'canceled' 即可,后续的操作在持续问询的长连接接口里面会进行处理的扫码失败,就直接更新表
qr_code_table中对应的 code 的状态 status 为 'failed' 即可,后续的操作在持续问询的长连接接口里面会进行处理的扫码成功,需要根据用户是否是新注册的用户来区分,进行不同的工作
用户是已经注册用户,更新表
qr_code_table中对应的 code 的状态 status 为 'confirmed' ,后续的操作在持续问询的长连接接口里面会进行处理的用户不是已经注册用户,用当前的微信信息注册一个用户,然后再更新表
qr_code_table中对应的 code 的状态 status 为 'confirmed' ,后续的操作在持续问询的长连接接口里面会进行处理的;从而实现了扫码注册并登录的操作
// 扫码的回调
router.get('/qrCodeCallback', async (req, res) => {
const {
scanSuccess,
tempUserId: qr_code,
cancelLogin,
wxMaUserInfo: wxMaUserInfoInParam,
} = req.query;
try {
// 判断是否取消登录
if (cancelLogin) {
// 扫码失败,状态更新
const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='canceled' WHERE qr_code="${qr_code}"`;
await runSql(updateQrCodeSql);
res.send({ code: 1, msg: '取消登录' });
return;
}
if (!scanSuccess) {
// 扫码失败,状态更新
const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='failed' WHERE qr_code="${qr_code}"`;
await runSql(updateQrCodeSql);
res.send({ code: 1, msg: '扫码失败' });
return;
}
if (scanSuccess) {
// 扫码成功
const wxMaUserInfo = JSON.parse(wxMaUserInfoInParam) || {};
const { openId, nickName } = wxMaUserInfo;
// 先根据 当前微信用户信息查询用户表,有没有这个人,如果有,就登录;没有就注册并登录
const queryUserSql = `select * from ${DB_NAME}.user_table where wechat_id='${openId}' `;
const userList = await runSql(queryUserSql);
if (userList.length > 0) {
// 更新二维码状态和用户信息,说明是已有用户,直接登录即可
const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='confirmed', user_id="${userList[0].user_id}" WHERE qr_code="${qr_code}"`;
await runSql(updateQrCodeSql);
res.send({ code: 0, msg: '登录成功' });
} else {
// 用当前微信信息注册一个用户,虽然是使用者 但是积分只有0
const values = {
user_name: nickName,
user_id: openId,
password: openId,
wechat_id: openId,
user_type: 'user',
disabled: 'false',
create_time: dayjs().format('YYYYMMDD'),
deadline_time: '20990101',
score: '0',
};
const fields = Object.keys(values).join(',');
const fieldsAddValue = Object.values(values)
.map((v) => `'${v || ''}'`)
.join(',');
const addUserSql = `insert into ${DB_NAME}.user_table (${fields}) values (${fieldsAddValue}) `;
await runSql(addUserSql);
// 注册成功之后 登录
const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='confirmed', user_id="${openId}" WHERE qr_code="${qr_code}"`;
await runSql(updateQrCodeSql);
res.send({ code: 0, msg: '注册并登录成功' });
}
}
res.send({ code: 1, msg: '未知错误' });
} catch (e) {
console.error(e);
res.send({ code: 1, msg: e });
}
});
# 用户扫码绑定微信
这个接口整体类似于用户扫码登录的操作,唯一的区别在于不再与登录产生关联,直接扫码然后将微信 id 和 userid 关联在一起就可以了,但是易登被禁用了,这里就没有再继续开发了