前言
扫码登录是应用中很常见的需求,比如微信,平时你都会使用手机端,当你需要在电脑上登录了,就会展示一个二维码给你,你打开手机微信扫码,进到一个页面,点确认登录即可。
电脑端登录就像是这样的:
手机端扫了以后的页面是这样的:
这中间的过程其实很简单,pc端轮询查二维码的状态,h5端更改二维码状态并在点确认登录动作的时候将已有的登录态,也就是token传给服务端,服务端做一层校验,校验通过把二维码状态改掉,把用户信息返回给正在轮询的PC端即可。
扫码登录的系统工作图就像这样:
下面我们开始用Nest
来实现。
先起一个Nest
项目。
arduino
nest new qrcode-project
把项目跑起来,执行命令,把接口跑通。
arduino
npm run start:dev
然后用Nest CLI
创建一个通用的扫码模块,执行命令:
nest n mo qrcode
nest n s qrcode
nest n co qrcode
安装一下生成二维码的依赖包。
css
npm i qrcode --save-dev
然后开始业务部分的开发,基于上面的时序图,我们再梳理一下,总共需要三个接口就OK了。
对于PC端,我们需要准备创建二维码、查询二维码状态两个动作。
对于H5端,我们只需要在进行了某一步操作一下,把二维码状态同步给服务端即可。
- create,创建二维码,提供给PC端扫码。
- getQrcodeStatus,获取二维码状态。
- updateQrcodeStatus,更新二维码状态。
我们来把这三个接口实现掉吧。
typescript
import { BadRequestException, Controller, Post, Get } from '@nestjs/common';
import { randomUUID } from 'crypto';
import * as qrcode from 'qrcode';
const map = new Map<string, QrCodeInfo>();
type QrCodeStatus =
| 'noscan' // 未扫码
| 'scan-wait-confirm' // 已扫码,等待确认
| 'scan-confirm' // 确认
| 'scan-cancel' // 取消
| 'expired'; // 过期
interface QrCodeInfo {
status: QrCodeStatus;
userInfo?: {
userId: number;
};
}
/**
* @description: PC端获取二维码
* @return {*}
*/
@Controller('qrcode')
export class QrcodeController {
@Get('/create')
async create() {
const url = 'http://localhost:8000/h5Login';
const qrcodeId = randomUUID();
const qrcodeUrl = await qrcode.toDataURL(url);
map.set(qrcodeId, {
status: 'noscan',
});
return {
qrcodeId,
qrcodeUrl,
};
}
/**
* @description: PC端轮询获取
* @return {*}
*/
@Get('/getQrcodeStatus')
async getQrcodeStatus(qrcodeId: string) {
const info = map.get(qrcodeId);
if (info.status === 'scan-confirm') {
return info.userInfo;
}
return {
status: info.status,
};
}
/**
* @description: H5端修改二维码状态(扫码、确认登录)
* @return {*}
*/
@Post('/updateQrcodeStatus')
async updateQrcodeStatus(qrcodeId: string, status: QrCodeStatus) {
const info = map.get(qrcodeId);
if (!info) {
throw new BadRequestException('二维码已过期');
}
info.status = status;
return {
status: info.status,
};
}
}
/qrcode/create
会返回一个二维码图片地址和二维码的ID
,PC端展示即可,二维码的链接就是H5登录的页面,可以选择把二维码ID带到url query
里去查询。
然后PC端就开始跑轮询任务,每次带着这个二维码ID去查就行,可以做一些更新的交互动作,拿掘金举例子,就像是这样:
扫码成功:
登录成功:
对于/qrcode/updateQrcodeStatus
,在H5端如果点了取消或者确认,就传不同的二维码status
枚举给服务端同步即可。
前端准备了一个类似的测试页面:
当点击确认登录,二维码更新到确认的状态后,我们还需要返回用户信息给PC端,那这里就需要做登录鉴权操作。
对于jwt
还会涉及到伪造请求的安全问题,可以看这篇文章:
我们安装jwt
包。
bash
npm install @nestjs/jwt
在app
模块中导入:
然后我们需要改造/qrcode/updateQrcodeStatus
、/qrcode/getQrcodeStatus
,当H5确认登录,服务端需要先做一层token
校验,如果验证通过,就把用户信息存在二维码表中,就像这样:
typescript
/**
* @description: H5端修改二维码状态(扫码、确认登录)
* @return {*}
*/
@Post('/updateQrcodeStatus')
async updateQrcodeStatus(
qrcodeId: string,
status: QrCodeStatus,
@Headers('Authorization') auth: string,
) {
const info = map.get(qrcodeId);
if (!info) {
throw new BadRequestException('二维码已过期');
}
if (status === 'scan-confirm' && auth) {
// 确认登录
try {
const [, token] = auth.split(' ');
const info = await this.jwtService.verify(token);
const user = users.find((item) => item.id == info.userId);
info.userInfo = user;
return user;
} catch (e) {
throw new UnauthorizedException('token 过期,请重新登录');
}
}
info.status = status;
return {
status: info.status,
};
}
如果二维码更新成功了,PC端下一次轮询就可以拿到确认的二维码状态,并且可以拿到userInfo
,在基于用户信息返回一个token
给PC端即可,这样就登录成功了。
typescript
/**
* @description: PC端轮询获取
* @return {*}
*/
@Get('/getQrcodeStatus')
async getQrcodeStatus(qrcodeId: string) {
const info = map.get(qrcodeId);
if (info.status === 'scan-confirm') {
const accsess_token = this.jwtService.sign(info.userInfo);
return { userInfo: info.userInfo, accsess_token };
}
return {
status: info.status,
};
}
扫码登录就实现了,其实原理很简单,只是涉及到了跨端,会觉得比较复杂。
总结
扫码登录是一种便捷的身份验证方式,用户通过扫描屏幕上显示的二维码(QR码)来登录应用或网站。这种登录方式在移动设备普及的背景下变得越来越流行,因为它提供了一个无需记忆密码的安全登录选项。
扫码登录的主要思路可以概括为以下几个步骤:
-
二维码的展示
当用户想要登录时(通常在Web应用或者PC软件中),服务器会生成一个包含特定登录信息的二维码并展示给用户。
-
二维码的扫描
用户使用手机上的应用(例如移动端的App内置扫码功能或通用的二维码扫码应用)扫描二维码。扫码功能得以识别出二维码内包含的登录信息。
-
登录验证
移动应用识别二维码中的信息后,一般包含认证服务器的URL和一段用于两端验证的token。移动应用通过HTTPS协议将token发送到服务器进行验证。
-
会话创建
服务器验证token的有效性,若验证通过,则会在服务器端创建一个新的登录会话,并返回会话信息给Web应用或PC软件。
-
登录确认
桌面端或Web端接收到服务器响应的登录会话之后,更新用户界面,确认用户已经登录成功。
如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」,将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想。