前言
这肯定不是最优解,本人也技术有限,但是能很简单地理解并完整实现扫码登录功能。
主要学的是前端,后端属于是能用就行,没有深入。
注册并配置测试号
这里采用测试号进行。
如果是正常商用需要服务号加微信认证(每年300好像),也可以先用测试号调试再将对应的appID和appsecret更换。
拿到对应的appID和appsecret之后可以可以先拿token(后面和公众号相关的基本都是需要的)
//获取微信access_token
const getAccessToken = () => {
return new Promise(async (resolve, reject) => {
const options = {
method: 'GET',
url: 'https://api.weixin.qq.com/cgi-bin/token',
params: {
grant_type: 'client_credential',
appid: 'wx64a8e5224672f70f',
secret: '949a00fe429032qe9f7f92a3771732bf'
},
headers: { 'content-type': 'application/json' }
};
try {
const response = await axios.request(options);
setToken(response.data.access_token);
resolve(response.data.access_token); // 返回access_token
} catch (error) {
console.error(error);
reject(error)
throw error;
}
})
}
注册链接:
微信公众平台 (qq.com)https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
注册完成后能可以自己先扫码关注一下,方便后面的模板消息推送。下面的信息我们一步一步来完成。
用express框架创建服务并配置接口信息
首先需要写好并启动一个服务,用express-generator能够快速搭建,这里不赘述。微信在接口信息配置的时候要求是有域名,我这里用的的cpolar内网穿透,免费的。其他内网穿透工具没试过,这个的缺点是隔断时间域名会变化,需要重新配置,但是也就是重新CV一下的事情。
cpolar官网-安全的内网穿透工具 | 无需公网ip | 远程访问 | 搭建网站
这里总共是需要四个接口,其中有两个是微信必须的。
第一个是微信要求验证对应配置信息,在进行接口信息配置的时候微信会按照你所填写的域名下发送get请求(请求是路径固定,为'/'),我这里是重新写了一个路由专门用来处理微信的接口。微信的官方文档给的是php的例子。
接口代码如下:
const TOKEN = 'dgutYe2022'
const signSHA1 = (signature, timestamp, nonce) => {
const tmpArr = [TOKEN, timestamp, nonce];
tmpArr.sort();
const tmpStr = tmpArr.join('');
const hash = crypto.createHash('sha1').update(tmpStr).digest('hex');
return hash === signature;
}
//微信验证
router.get('/', (req, res) => {
const { signature, timestamp, nonce, echostr } = req.query;
try {
if (signSHA1(signature, timestamp, nonce)) {
res.send(String(echostr));
} else {
console.error("加密字符串不等于微信返回字符串,验证失败!!!");
res.send("验证失败!");
}
} catch (error) {
res.send(`微信服务器配置验证出现异常: ${error}`);
}
});
这里的Token需要和接口配置信息里的一样,都是可以自己写的。微信官方也提供了调试工具,大家如果发现有问题照着一步一步调试就好了。调试工具需要token,就是第一步的接口拿到的。
获取二维码
微信官方文档有提供具体的解释,这里直接上链接。
代码如下:
//生成随机值作为场景值
const generateRandomString = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
const randomLength = Math.floor(Math.random() * 4) + 5; // 生成一个5到8之间的随机整数
let result = '';
for (let i = 0; i < randomLength; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
//获取二维码
const getTicket = (scene) => {
return new Promise((resolve, reject) => {
const options = {
method: 'POST',
url: 'https://api.weixin.qq.com/cgi-bin/qrcode/create',
params: {
access_token: access_token
},
headers: { 'content-type': 'application/json' },
data: {
expire_seconds: 604800,//有效时间
action_name: 'QR_STR_SCENE',//类型
action_info: { scene: { scene_str: scene } }//场景值,我用的是随机
}
};
axios.request(options).then(function (response) {
const qr = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + response.data.ticket;
resolve(qr);
}).catch(function (error) {
console.error(error);
reject(error);
});
});
};
按照文档是有能永久有效的二维码,但是之前试过好像扫码之后的事件推送没收到,而且这里需要场景值来判断谁登录了,所以用短期有效的二维码。这里返回的qr就是二维码的链接。
接收事件推送并返回模板信息
当有人扫了对应的二维码之后会服务端会收到事件推送,需要写接口来接收并对后续进行处理。
我们可以先设置好对应的模板消息。还是在微信公众平台测试页配置即可。
配置完成后我们能看到对应的模板id,可以先写好信息模板推送。这里写的是最简单的登录成功,大家根据自己需要做。
//发送模板信息
const sendTemplateMessage = async (template_id, dat, openid) => {
const options = {
method: 'POST',
url: 'https://api.weixin.qq.com/cgi-bin/message/template/send',
params: { access_token: access_token },
headers: { 'content-type': 'application/json' },
data: {
touser: openid,
template_id: template_id,
data: dat
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
接下来写接收消息推送的接口:
//扫描接收有无人登录
router.post('/', (req, res) => {
const xml = req.body;
let t = false
parser.parseString(xml, (err, result) => {
if (err) {
console.error("解析XML时出错:", err);
}
// 输出解析后的JSON对象
if (result.xml.Ticket) {
LoginMes.find({
Time: result.xml.CreateTime[0],
openId:result.xml.FromUserName[0]
}).then(res => {
console.log(res.length)
if (res.length == 0) {
sendTemplateMessage('-IBJ3la06Ooaw7npL7ocWsP8z5ThiPM9opgCslMrSaM', {}, result.xml.FromUserName[0])
LoginMes.create({
Time: result.xml.CreateTime[0],
openId:result.xml.FromUserName[0]
})
LoginScense.findOneAndUpdate(
{ Scense: result.xml.EventKey[0] },
{ $set: { openId: result.xml.FromUserName[0] } }
).then(res => {
// console.log(res)
}).catch(err => {
console.log(err)
})
}
}).catch(err => {
console.log(err)
})
}
res.send('success')
});
});
注意这里的路径需要和之前配置接口时候一样,但是把get请求换成post。每次扫码之后,微信的机制会推送很多很多次,所以去重和最后的返回success很重要,缺了去重就会一次发N多信息,缺了success就会最后推送来一句提供服务异常。我这里是用判断有无ticket和时间戳+openID来进行去重。
用的是mongodb数据库,如果这个时间戳和openID没有同时存在过的话就加入数据库并且进行操作。另一个数据库操作等有下面到登录再一起说。
//消息去重
let LoginMesSchem=new Schema({
Time:String,
openId:String
})
let LoginMes=mongoose.model('LoginMes',LoginMesSchem)
目前这样多次测试是能实现每次只推送一条信息且不会报异常。(之前没配置,扫一个码来了一堆信息,几次下来直接给整模板推送上限了。如果你也遇到这种情况,可以考虑直接把服务直接停了先,当然按实际情况哈)。
这样子获取二维码还有扫码后的操作我们就基本可以了。接下来就是如何使用他们了。
前端部分
这里用的是一个轮询来判断登录状态。
获取二维码,同时开启监听登录行为。
//获取登录二维码
export const getSrc = (data) => {
return request({
url: '/wx/login',
method: 'get',
params: data
})
}
//轮询查看是否登录成功
export const getLogin = (data) => {
return request({
url: '/wx/getToken',
method: 'get',
params: data
})
}
<script setup>
import { getSrc, getLogin } from '@/api/api'
import { onMounted, ref } from 'vue'
import { useTokenStore } from '@/stores'
import { useRouter } from 'vue-router'
let date = new Date()
const router = useRouter()
const currentTime = ref('')
const TokenStore = useTokenStore()
const generateRandomString = () => {
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
const randomLength = Math.floor(Math.random() * 4) + 5 // 生成一个5到8之间的随机整数
let result = ''
for (let i = 0; i < randomLength; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result
}
onMounted(async () => {
currentTime.value = date.getTime()
const data = {
RanDom: generateRandomString(),
Time: currentTime.value
}
const getqr = await getSrc(data)
src.value = getqr.msg
let T = setInterval(() => {
getLogin(data).then((res) => {
if (res.code == 200) {
clearInterval(T)
TokenStore.setAccessToken(res.data.access_token)
TokenStore.setRefreshToken(res.data.refresh_token)
router.push('/manger')
}
})
}, 2000)
})
const src = ref('')
</script>
<template>
<div>
<div class="card">
<img :src="src" alt="" srcset="" />
</div>
</div>
</template>
<style scoped>
img{
width: 400px;
height: 400px;
}
</style>
为了确定谁扫的码,这里用时间戳和一串生成的随机字符进行验证。
如果后端返回的是200说明已经登录,如果是400则说明还没有人扫码。
后端部分
//匹配表
let LoginScenseSchema=new Schema({
Scense:String,
Time:String,
RanDom:String,
openId:String
})
let LoginScense=mongoose.model('LoginScense',LoginScenseSchema)
//获取登录
router.get('/login', async (req, res) => {
const scense = generateRandomString()//生成随机的场景值
let qr = await getTicket(scense)
LoginScense.create({
Scense: scense,
Time: req.query.Time,
RanDom: req.query.RanDom,
openId: ''
})
res.status(200).json({
code: 200,
msg: qr
})
})
这里获取二维码的时候需要处理好异步,不然的话容易qr还是空串就完成下面操作了。
后将场景值、事件、随机串传入到数据库中,openId先留空着。
//扫描接收有无人登录
router.post('/', (req, res) => {
const xml = req.body;
let t = false
parser.parseString(xml, (err, result) => {
if (err) {
console.error("解析XML时出错:", err);
}
// 输出解析后的JSON对象
if (result.xml.Ticket) {
LoginMes.find({
Time: result.xml.CreateTime[0],
openId:result.xml.FromUserName[0]
}).then(res => {
console.log(res.length)
if (res.length == 0) {
sendTemplateMessage('-IBJ3la06Ooaw7npL7ocWsP8z5ThiPM9opgCslMrSaM', {}, result.xml.FromUserName[0])
LoginMes.create({
Time: result.xml.CreateTime[0],
openId:result.xml.FromUserName[0]
})
LoginScense.findOneAndUpdate(
{ Scense: result.xml.EventKey[0] },
{ $set: { openId: result.xml.FromUserName[0] } }
).then(res => {
// console.log(res)
}).catch(err => {
console.log(err)
})
}
}).catch(err => {
console.log(err)
})
}
res.send('success')
});
});
这里其实就是把上面接收消息模板的代码重新贴上来一次。要补充说明的是,这里要通过场景值找到数据库里面的那一项然后把openId补上去,或者进行权限判断等。这里是直接模板消息发送给微信登录成功。这里有openId是为了后面做个历史记录。
//接收登录请求+双token
router.get('/getToken', (req, res) => {
let { Time, RanDom } = req.query
LoginScense.findOne({ Time, RanDom }).then((r) => {
if (r == null || r.openId == '') {
res.status(200).json({
code: 400
})
} else {
let access_token = jwt.sign({ Time: Time, openID: r.openId }, 'DgutCycn_acc', {
expiresIn: '1min',
algorithm: 'HS256'
})
let refresh_token = jwt.sign({ Time: Time, openID: r.openId }, 'DgutCycn_refresh', {
expiresIn: '24hour',
algorithm: 'HS256'
})
res.status(200).json({
code: 200,
data: {
access_token,
refresh_token
}
})
}
})
})
这个很简单了,就是根据前端轮询时候的时间和随机值查找数据库是否有对应数据,如果有的话就返回登录成功和token(这里的双token也是为了后面无感刷新)。
到这里基本就完成了公众号扫码登录。