node.js+vue+mongodb实现通过微信公众号扫码登录(完整易懂)

前言

        这肯定不是最优解,本人也技术有限,但是能很简单地理解并完整实现扫码登录功能。        

        主要学的是前端,后端属于是能用就行,没有深入。

注册并配置测试号

        这里采用测试号进行。

        如果是正常商用需要服务号加微信认证(每年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)icon-default.png?t=N7T8https://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,就是第一步的接口拿到的。

微信调试工具 (qq.com)

获取二维码

微信官方文档有提供具体的解释,这里直接上链接。

账号管理 / 生成带参数的二维码 (qq.com)

代码如下:

//生成随机值作为场景值
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也是为了后面无感刷新)。

        到这里基本就完成了公众号扫码登录。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值