Skip to content
On this page

前言

最近发现别人的网站可以通过订阅号的发送验证码方式进行登录。感觉很不错,于是有了这篇文章。本文主要介绍了如何利用微信订阅号发送验证码的方式实现:登录集成到已有登录系统

👻效果展示👻

😍线上体验😍:http://www.xiaoyi.pub/login订阅号登录3.gif

整体实现思路

  1. 拥有自己的订阅号(个人、企业)免费申请
  2. 订阅号配置自己的服务器,用户生成验证
  3. 挂上订阅号二维码,让用户获取验证码
  4. 校验验证码是否正确、有效
  5. 通过验证码、did判断用户是否已经注册
    • 已注册就直接生成token,进而登录
    • 未注册,先默认注册,再进行登录
    • 弹弹窗告诉验证默认注册的账号密码

订阅号服务器处理消息流程图如下:

订阅号消息回复流程图 (1).jpg

一、订阅号申请及配置

  • 微信订阅号申请:https://mp.weixin.qq.com/,没有的可以先注册,选订阅号
  • 白名单配置:设置与开发 => 基本配置 => ip白名单
  • 服务器配置:设置与开发 => 基本配置 => 服务器配置(用于用户关注订阅号之后,发送消息进行处理回复的)还没有服务器,这里后面再配
javascript
url: 自己的服务器域名
Token: 自己随便填写 需要和代码里的保持一致
秘钥:随机生成
加密方式: 明文
  • 接下来需开发一个服务器,本文使用的express进行开发

二、前端部分

  • 订阅号的二维码
  • 表单校验、提交、做防抖
  • 登录成功展示 信息弹窗,页面跳转 image.png
  • 二维码组件代码如下
vue
<template>
  <div>
    <div style="display: flex; align-items: center; flex-direction: column">
      <div class="title">关注微信公众号:发送 <span>登录</span> 获取验证码</div>
      <img width="300" src="../../assets/imgs/qrcode.jpg" preview-disabled />
      <div style="display: flex; align-items: center">
        <Input
          autofocus
          clearable
          show-count
          v-model:value="codeValue"
          style="width: 70%"
          placeholder="请输入验证码"
          :maxlength="6"
        />
        <Button type="primary" @click="handleSendCode">
          <template #icon>
            <span class="dark:text-black">
              <SvgIcon icon="ri:send-plane-fill" />
            </span>
          </template>
          提交
        </Button>
      </div>
    </div>
  </div>
  <Modal v-model:open="showModal" title="登录成功,请牢记你账号密码" @cancel="onClose" footer="" :mask-closable="false">
    <Descriptions title="*密码只在注册时展示" layout="vertical" bordered>
      <DescriptionsItem label="账号">{{ state.username }}</DescriptionsItem>
      <DescriptionsItem label="密码">{{ state.password }}</DescriptionsItem>
      <DescriptionsItem label="uid">{{ state.uid }}</DescriptionsItem>
    </Descriptions>
  </Modal>
</template>

<script setup lang="ts">
import { Button, Input, message as ms, Modal, Descriptions, DescriptionsItem } from "ant-design-vue";
import { debounce } from "@/utils/function";
import { verifyCode } from '@/apis/index'
import { userInfoService } from '@/utils/auth'
import { useUserStore } from '@/store/userStore'

const router = useRouter()

const codeValue = ref();
const showModal = ref(false)
const state = ref({
  username: '',
  password: '',
  uid: '',
})
const handleSendCode = debounce(async function () {
  const reg = /^[0-9]{6}$/;
  if (!reg.test(codeValue.value)) {
    ms.error("请输入6位纯数字验证码");
    return;
  }
  try {
    const { userInfo = {}, msg } = await verifyCode({code: codeValue.value}) as any;
    if (userInfo?.uid) {
      state.value = userInfo
      const userStore = useUserStore()
      userStore.setUserInfo(userInfo)
      userInfoService.setUserInfo(userInfo)
      ms.success(msg);
      showModal.value = true
  }
  } catch (error: any) {
    // ms.error(error?.msg);
  }
}, 600);

const onClose = () => {
  ms.loading('即将跳转到首页...')
  showModal.value = false
  setTimeout(() => {
    router.push({
      path: '/createroom',
    })
  }, 2000)
}
</script>

<style lang='less' scoped>
.title {
  font-size: 14px;
  span {
    color: blue;
    font-weight: 700;
  }
}
</style>

二、后端部分

我们需要三个接口:微信认证处理订阅号消息校验验证码

1. 微信认证

  • 完整步骤:参考微信官方文档
  • token需要与微信服务器配置的token保持一致
  • 加密规则校验 signature, timestamp, nonce, echostr是认证接口携带的
  • 校验通过 返回 echostr
js
// router/wechat.js
const express = require('express');
const jsSHA = require('jssha');
const router = express.Router();

router.get('/wechat', (req, res, next) => {
	const token = 'xiaoyi1255'; // 这里的token需要和微信开发平台配置的token一致
	//1.获取微信服务器Get请求的参数 signature、timestamp、nonce、echostr
	const { signature, timestamp, nonce, echostr } = req.query;
	//2.将token、timestamp、nonce三个参数进行字典序排序
	const array = [token, timestamp, nonce].sort();

	//3.将三个参数字符串拼接成一个字符串进行sha1加密
	const tempStr = array.join('');
	const shaObj = new jsSHA('SHA-1', 'TEXT');
	shaObj.update(tempStr);
	const scyptoString = shaObj.getHash('HEX');

	//4.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
	if (signature === scyptoString) {
    // 验证成功
		res.send(echostr);
	} else {
		res.send('验证失败');
	}
});

2. 处理 登录 消息

  • 处理用户发送 登录 文案
  • 随机生成6位数字验证码
  • 存储验证码:redis => code: did(也可以使用其它方式)
  • 返回验证码
js
// router/wechat.js
const { parseString } = require('xml2js');

router.post('/wechat', function (req, res) {
	var buffer = [];
	req.on('data', function (data) {
		buffer.push(data);
	});
	// 内容接收完毕
	req.on('end', function () {
		var msgXml = Buffer.concat(buffer).toString('utf-8');
		parseString(msgXml, { explicitArray: false }, async function (err, result) {
			if (err) throw err;
			result = result.xml;
			const { ToUserName, FromUserName, MsgType, Content } = result;
			if (MsgType === 'text' && Content === '登录') {
				const code = randomCode();
				// 5分钟有效期
				// 这里的FromUserName就是用户的OpenID
				await redisCkient.setEX(code, FromUserName, 60 * 5);
				const sendXml = sendTextMsg(
					ToUserName,
					FromUserName,
					`您的登录验证码是:${code}  ,  有效期为5分钟`
				);
				res.send(sendXml);
			}
		});
	});
});

/**
 * 随机6位验证码
 */
function randomCode() {
	return Math.random().toString().slice(-6);
}

/**
 * 回复文字消息封装成xml
 */
function sendTextMsg(toUser, fromUser, content) {
	let resultXml =
		'<xml><ToUserName><![CDATA[' + fromUser + ']]></ToUserName>';
	resultXml += '<FromUserName><![CDATA[' + toUser + ']]></FromUserName>';
	resultXml += '<CreateTime>' + new Date().getTime() + '</CreateTime>';
	resultXml += '<MsgType><![CDATA[text]]></MsgType>';
	resultXml += '<Content><![CDATA[' + content + ']]></Content></xml>';
	return resultXml;
}

3. 校验验证码

js
// 验证成功就去注册+登录
router.get('/verifyCode', async function (req, res) {
	const { code } = req.query;
  // redis 中取出 验证码及did  => code: did
	const OpenID = await redisCkient.get(code);
	if (OpenID) {
    /**
     * 1. 验证码有效 => 进一步判断用户是已注册
     * 2. 已注册 => 直接生成 token => 返回登录成功
     * 3. 未注册 => 使用did进行注册
     *  注册成功 直接生成 token => 返回登录成功
     */

	} else {
		res.json({
			code: 400,
			msg: '您输入的验证码有误或已过期,请重新输入!-_-'
		});
	}
});

验证码校验登录完整代码

  1. 先校验验证码是否正确、有效
  2. 验证码正确 => 查看用户是否存在 => 存在直接登录 (不存在=> 进行注册及登录)
  3. jwt生成token、返回登录结果 生成jwt可以参考:无感刷新token
js
// router/wechat.js
// 验证成功就去注册+登录
router.get('/verifyCode', async function (req, res) {
	const { code } = req.query;
	const OpenID = await redisCkient.get(code);
	if (OpenID) {
		const sql = `SELECT username,uid,gender,did FROM user_table WHERE did = ? `
		const queryhasUser = await db.query(sql, [OpenID])
		if (!queryhasUser.length) {
			const password =  randomCode()
			// 注册并登录
			const user = {
				username: `user${code}`,
				did: OpenID,
				uid: code,
				gender: '',
				password
			}
			const inset_sql = `INSERT INTO chat.user_table (did, username, password) VALUES (?, ?,?)`
        console.log(inset_sql)
        const results = await db.query(inset_sql, [user.did, user.username, password])
				if (results.affectedRows === 1) {
          console.log('Insertion successful.');
					createJwt(user, res)
					res.json({
						code: 0,
						data: { 
							userInfo: user,
							msg: "注册并登录成功!!!"
						}
					})
        }
		} else {
			// const token = '使用OpenID进行jwt鉴权颁发Token';
			const user = {
				username: queryhasUser[0]?.username,
				did: queryhasUser[0]?.did,
				uid: queryhasUser[0]?.uid,
				gender: queryhasUser[0]?.gender,
			}
			createJwt(user, res)
			res.json({
				code: 0,
				data: { 
					userInfo: queryhasUser[0],
					msg: '登录成功!'
				 }
			});
		}
	} else {
		res.json({
			code: 400,
			msg: '您输入的验证码有误或已过期,请重新输入!-_-'
		});
	}
});

恭喜你!👻👻看到这里,订阅号实现验证码登录的整个流程就结束了!是不是挺简单的?下面是我的踩坑实录~~

踩坑实录

1. 获取域名 借助内网穿透

ngrok.com (不行)

ngrok 内网穿透弄了一个https的,没有证书,报安全隐患,并且请求一直没有到达我的服务器。速度好像比下面那个快一点。

ngrok.cc (2元 可以) 最近好像满了一直掉线

ngrok.cc 这个弄了一个免费的,还可以,需要实名认证费2元

  • 注册 => 登录 => 隧道管理 => 开通隧道 => 滑到最下面 => 填写信息 => 提交 使用教程

image.png

2. 微信公众服务器配置 => token验证不通过

登录了微信公众平台:https://mp.weixin.qq.com/ 设置与开发 => 基本设置 => 服务器配置

  • 在这里配置服务器url,然后提交,一直报 token 验证不通过
  • url是 https 需要能正常访问(在浏览器访问不报 安全隐患)
  • 在腾讯云买的大陆服务器,域名:需要进行备案,,奈何备案提交了两次被工信部驳回。(注:内陆的都需要备案) image.png然后就是漫长的痛苦之路---微信开发社区求助image.png
    主要失败原因:
  1. htts需要有证书(浏览器访问不能有安全警告那个拦截)
  2. http不需要证书,正常访问就行
  3. 需要是域名(ip)不行
  4. 加密、校验规则

image.png

  • 绑定成功图如下: image.png

源码

xiaoyi1255

结语:

如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻

因为收藏===会了

如果有不对、可以优化的地方欢迎在评论区指出,谢谢👾👾👾