温馨提示:本文运用 ChatGPT 润饰。
参阅链接:
服务器端事情发送(
SSE
):developer.mozilla.org/en-US/docs/…Nest.js:docs.nestjs.cn/9/introduct…
uni-app:uniapp.dcloud.net.cn/api/system/…
扫码登录
最近想给自己的网站添加一个扫码登录功用,这样就不必再输入暗码登录了,所以就去研究了下怎么完成扫码登录。
扫码登录的基本介绍
扫码登录是一种快速、快捷的登录方式,用户只需用手机扫描二维码即可完成登录。
相较于相关于传统的账号暗码登录,扫码登录具有以下优势:
-
便利快捷:用户只需运用手机扫描二维码即可完成登录,防止了输入杂乱的账号和暗码,提高了用户体会;
-
安全可靠:扫码登录不需求用户输入账号和暗码,防止了暗码走漏的风险;
-
适用范围广:扫码登录能够应用于多种场景,比方企业 OA 体系、电商网站、社交软件等。
而关于网站运营方而言,扫码登录也有以下优点:
-
提高用户体会:传统的账号暗码登录方式需求用户输入杂乱的账号和暗码,简略让用户感到繁琐和不便。而扫码登录只需求用户运用手机扫描二维码即可完成登录,简略、便利,能够提高用户体会。
-
提高用户留存率:账号暗码登录需求用户输入账号和暗码,关于新用户来说,可能会由于忘记暗码、输入过错等原因而放弃注册,而扫码登录能够防止这种状况的产生,提高用户注册和留存率。
另外,对我个人而言,完成扫码登录也是一次技能性的实验,对了解怎么完成扫码登录也大有优点。
扫码登录的基本流程
- 用户在电脑端打开需求登录的网站或应用,选择扫码登录选项;
- 体系生成二维码,并在电脑端显现;
- 用户运用手机扫描电脑端的二维码;
- 手机端承认登录,将登录信息发送到服务端;
- 服务端验证登录信息,验证通往后完成登录。】
技能选型
很显然的是,在扫码登录的过程中,需求从服务端向浏览器端推送音讯,所以需求一项能够完成该功用的技能。
通过一番搜索,有以下几个常见技能计划:
-
WebSocket:WebSocket 是一种新型的双向通讯协议,能够在客户端和服务端之间树立持久性的衔接,支撑实时双向通讯。WebSocket 能够直接在浏览器和服务端之间树立衔接,完成服务端向浏览器端推送音讯。
-
Server-Sent Events(SSE):SSE 是一种依据 HTTP 的单向推送技能,它答应服务端向客户端发送事情流(Event Stream),客户端通过 EventSource API 进行监听并处理。SSE 能够完成服务端向浏览器端的单向音讯推送。
-
Long Polling:Long Polling 是一种依据 HTTP 的轮询技能,客户端向服务端发送恳求,服务端在没有音讯的状况下将恳求挂起,当有音讯时再呼应恳求。客户端接收到呼应后当即再次发送恳求,从而完成不间断的音讯推送。
-
WebRTC:WebRTC 是一种实时通讯技能,能够在浏览器之间进行直接的点对点通讯,无需通过服务器中转。WebRTC 能够完成浏览器端之间的实时双向通讯,也能够完成服务端向浏览器端的音讯推送。
通过一番比对后,我以为 WebSocket 虽然能够完成这个需求,但扫码登录只需求服务端向浏览器端的单向推送,无需双向推送,所以选 WebSocket 的话技能上有些重了。
而 Long Polling (长轮询)则较为消耗服务端功用,对服务端而言无法主动操控推送和断开,也有些缺乏。
WebRTC 虽然也能够完成双向推送,但完成起来稍微杂乱,也有些过重了。
故归纳考虑,我以为运用 SSE 来完成扫描登录是比较合理的。
Server-Sent Events(SSE)
SSE 的基本概念
SSE 是一种依据 HTTP 的单向推送技能,它答应服务端向客户端发送事情流(Event Stream),客户端通过 EventSource API 进行监听并处理。SSE 能够完成服务端向浏览器端的单向音讯推送。
在后端,Nest.js 中完成一个 SSE 只需求如下代码:
@Sse('sse')
sse(): Observable<MessageEvent> {
return interval(1000).pipe(map((_) => ({ data: { hello: 'world' } })));
}
然后在浏览器端执行以下代码:
const eventSource = new EventSource('/sse');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
就能够在操控台看到每秒打印一次的日志了。
SSE 的优势和局限性
SSE 的优势有:
- 树立简略:SSE 依据 HTTP 协议,无需像 WebSocket 那样进行握手协议,树立衔接比较简略。
- 兼容性好:SSE 对浏览器的支撑性很好,大部分浏览器都能支撑 SSE。
- 对服务器资源要求低:SSE 树立的是单向衔接,推送服务端只需向客户端发送数据流,比较 WebSocket 而言,对服务器资源的要求较低。
- 实时性好:SSE 能够在服务端有新数据时当行将数据推送到客户端,实时性较好。
而 SSE 的局限性包含:
- 单向通讯:SSE 是一种单向通讯协议,只能由服务端向客户端推送数据,无法完成客户端到服务端的双向通讯。
- 无法处理很多数据:SSE 的数据传输方式是依据文本的,关于很多数据传输功率较低,简略形成延迟和卡顿。
- 无法处理杂乱的业务场景:SSE 的运用场景比较简略,只适用于一些简略的数据推送和告诉场景,无法处理杂乱的业务场景。
- 对浏览器的支撑有限:虽然大部分浏览器都支撑 SSE,可是有些浏览器对 SSE 的支撑还不是很好,需求开发者进行额定的兼容性处理。
总的来说,SSE 适合于一些简略的实时告诉和音讯推送场景,关于一些杂乱的业务场景和很多数据传输场景,SSE 的功用和功用都比较有限。
由于扫描登录只需求从服务端向客户端推送数据,所以采用 SSE 来完成扫码登录是能够的。
用 SSE 完成扫码登录 – 后端部分
既然选定了 SSE 作为技能计划,那么就要开端具体的完成了。
通过一番思考,我以为扫码登录需求完成以下几个接口:
- 获取/生成二维码的接口
- 提交扫码二维码成果的接口
- 从服务端向浏览器端推送扫码成果的接口
获取/生成二维码的接口
该部分的技能栈为:Nest.js
生成二维码其实是比较简略的,本质上仍是要由服务端随机生成一段 code,然后发送给前端,前端烘托为二维码即可。
在生成 token 这一段,我采用了uuid
来完成,在满足随机的状况下,撞uuid
的概率仍是很小的。
参阅代码如下:
@Get('getQrCode')
@ApiOperation({ summary: '获取 登录二维码' })
async getQrCode(@Ip() ip: string) {
const qrCode = uuid()
const key = `login-by-qr-code:${qrCode}`
const data = {
status: "notUsed",
uid: null,
}
await client.hmset(key, data) // client 为 ioredis 实例
await client.expire(key, 5 * 60)
return {
qrCode,
expiryTime: Date.now() + await client.pttl(key),
}
}
在生成 code 之后,显然是需求存到数据库的,这儿自然选择了运用 redis 作为数据库,无他,只是 redis 特别快罢了。
我选择了在 redis 中存一个 hash,其间 status 字段为当时二维码的状况,而 uid 则用于存扫码的用户 id。
有关 redis 的运用请参阅:github.com/luin/ioredi…
在生成 code 之后,就要在前端烘托为二维码了,这一部分稍后在网页端部分具体阐明。
提交扫码二维码成果的接口
在手机端扫描二维码之后,会得到之前生成的 code,再提交到服务器就能够告知服务器扫码成功了。
参阅代码如下:
export class QrCodeData {
@IsNotEmpty({ message: 'qrCode不能为空' })
@ApiProperty({ description: '二维码 id', example: uuid() })
qrCode: string
@IsNotEmpty({ message: 'action不能为空' })
@ApiProperty({ description: '操作:扫码scan/同意approve/撤销cancel', example: 'scan' })
action: QrCodeAction
}
@Post('scanQrCode')
@UseJwt()
@ApiOperation({ summary: '扫描/授权/撤销 二维码' })
async scanQrCode(@Body() body: QrCodeData, @CurrentUser() user: UserDocument) {
const { qrCode, action = 'scan' } = body
const key = `login-by-qr-code:${qrCode}`
const result = await client.hgetall(key)
if (!result) {
new HttpError(400, '扫码失利!该二维码已过期,请改写网页后从头扫码!')
}
if (action === 'cancel') {
await client.del(key)
return new ResponseDto({
message: '撤销扫码成功!',
})
}
let data = {}
if (action === 'scan') {
data = {
status: "scanned",
uid: String(user._id),
}
} else if (action === 'approve') {
data = {
status: "used",
uid: String(user._id),
}
}
await client.hmset(key, data)
await client.expire(key, 5 * 60)
return new ResponseDto({
message: '扫码成功!',
})
}
}
在这儿我定义了手机端的三个操作:扫码、同意和撤销。
一般来讲,在用户扫码之后就应该在浏览器端有所表现,能够提示扫码成功
等,但用户实践登录要等到点击确认
授权登录之后,所以这儿还需求有一个同意登录的操作。
从服务端向浏览器端推送扫码成果的接口
最终便是想浏览器端推送成果了。
在 Nest.js 中运用 SSE, 需求回来一个Observable
流,例如:
export class HandleQrCodeData {
@IsNotEmpty({ message: 'qrCode不能为空' })
@ApiProperty({ description: '二维码 id', example: uuid() })
qrCode: string
}
export interface MessageEvent {
data: string | object;
id?: string;
type?: string;
retry?: number;
}
@Sse('handleQrCode')
@ApiOperation({ summary: '服务器推送扫描二维码的成果' })
handleQrCode(@Query() query: HandleQrCodeData): Observable<MessageEvent> {
const { qrCode } = query
const key = `login-by-qr-code:${qrCode}`
const start = Date.now()
return interval(1000)
.pipe(
map(async () => {
const data = await client.hgetall(key)
if (!data) {
throw new HttpError(400, '扫码失利!该二维码已过期,请改写网页后从头扫码!')
}
if (data?.status === "used" && isMongoId(data?.uid)) {
await client.del(key)
const { token } = this.getAuthToken(data?.uid)
return {
data: {
token,
status: "used"
},
}
}
if (data?.status === QrCodeStatus.scanned) {
return {
data: {
// token: null,
status: "scanned"
},
}
}
return {
data: {
// token: null,
},
}
}),
mergeAll(), // 在这儿需求用 mergeAll 操作符来获取到 Promise 的 resolve 值,原因是在 map 中回来了一个 Promise。更具体的内容可参阅:https://blog.cmyr.ltd/archives/84a41459.html
)
}
这样一来就完成了一个每秒查询一次扫码成果并向浏览器推送的 SSE。
留意,这儿的完成有以下几个问题:
- 轮询 redis 对 redis 的功用损耗较大,更合理的计划是运用 redis 的事情订阅。这儿为了简略起见就采用了轮询。
- 由于查询 redis 是一个异步操作,会回来一个 Promise,需求用 RxJS 的 mergeAll 操作符才干获取到 Promise 的 resolve 值。
用 SSE 完成扫码登录 – 浏览器端部分
该部分的技能栈为:TypeScript(纯 JavaScript 也可完成)
在浏览器端要做的事情比较简略,总共以下几步:
- 获取登录二维码的 code
- 依据 code 烘托二维码
- 监听扫码成果
获取登录二维码的 code
这一步实践上比较简略,发起一个 ajax 恳求即可
export interface IQrCode {
/**
* 二维码 的 uuid
*/
qrCode: string
/**
* 过期时间
*/
expiryTime: number
}
/**
* 获取 登录二维码
*/
export async function getQrCode() {
return ajax<IQrCode>({
url: '/auth/getQrCode',
})
}
依据 code 烘托二维码
这一步需求用到一些第三方包来完成了,这儿我采用了qrcode
这个包来生成二维码。
import QRCode from 'qrcode'
/**
* 生成二维码
*/
export async function createQRCode(text: string | QRCode.QRCodeSegment[]) {
try {
return await QRCode.toDataURL(text, { errorCorrectionLevel: 'Q' })
} catch (err) {
console.error(err)
return ''
}
}
const qrCodeUrl = = await createQRCode(qrCode)
这儿会生成一个 base64 格式的图片,在 html 顶用img
标签烘托即可。
<img
class="qr-code-img"
:src="qrCodeUrl"
width="250"
height="250"
/>
监听扫码成果
然后则是要监听扫码成果了,由于运用了 SSE,这儿自然也是用 SSE 相关接口了。
const e = new EventSource(`/handleQrCode?qrCode=${qrCode.value}`)
e.onmessage = async ({ data }) => {
try {
data = JSON.parse(data) // 这儿的 data 是 string 类型的,所以需求 JSON.parse
// console.log('data', data)
const token = data?.token
status.value = data?.status || ''
if (token) {
// 登录成功后操作……
return
}
// 无事产生,持续监听
} catch (error) {
// 处理过错
console.error(error)
}
}
e.onerror = async (event) => {
console.error(event)
Message.error('扫码登录出现过错,请稍后改写网页重试。')
}
用 SSE 完成扫码登录 – 手机端部分
该部分的技能栈为:uni-app
最终就到了手机端部分的完成了。
实践上这最终一步也是最好完成的,原因在于扫码模块直接调用第三方包即可,有老练的第三方包能够运用。
手机端部分主要有以下几个步骤:
- 扫描二维码,获取二维码成果
- 用户授权登录
- 告诉服务端用户授权登录
留意:
-
扫码成功之后应该还要告诉服务端二维码扫描成功。但这儿为了简略起见,就不完成了告诉扫码成功的部分了。
-
在等候用户授权登录的时分,用户也能够撤销授权。但这儿为了简略起见,就不完成撤销登录的部分了。
扫描二维码,获取二维码成果
在 uni-app 中,运用uni.scanCode
能够非常轻松的在除 H5 外的环境下完成扫码登录。
参阅代码如下:
uni.scanCode({
scanType: ['qrCode'],
// autoZoom: false, // 禁用主动扩大。主动扩大有时分会帮倒忙,如果不太爽的话能够手动封闭
success: function (res) {
console.log('条码类型:' + res.scanType);
console.log('条码内容:' + res.result);
}
});
其间的res.result
便是二维码中的 code。
用户授权登录
然后需求等候用户授权,这儿弹一个模态框出来
uni.showModal({
title: '提示',
content: '确认授权登录?',
success: function (res) {
if (res.confirm) {
console.log('用户点击确认');
} else if (res.cancel) {
console.log('用户点击撤销');
}
}
});
告诉服务端用户授权登录
用户授权登录之后发一次 ajax 恳求即可。
interface QrCodeData {
qrCode: string
action: 'scan' | 'approve' | 'cancel'
}
export async function scanQrCode(data: QrCodeData) {
return ajax({
url: '/scanQrCode',
method: 'POST',
data,
})
}
scanQrCode({ qrCode, action: 'approve' })
前后端联调
在完成了以上几个部分之后,还需求最终调试才干确认逻辑是否正确。
由于一起涉及到电脑端和手机端,因而需求两者在同一局域网下才干联调。
手机端要通过局域网访问电脑的话还需求电脑开放对应的端口。
如果想省点事的话能够直接部署到公网联调。
总结
本文旨在介绍怎么运用 Server-Sent Events(SSE)技能完成扫码登录,并供给了完整的技能选型和流程。文章分为后端、浏览器端和手机端三个部分,介绍了各自的技能栈和完成办法。其间,后端部分运用 Nest.js 技能完成获取/生成二维码的接口、提交扫码二维码成果的接口、从服务端向浏览器端推送扫码成果的接口;浏览器端运用 TypeScript 技能获取登录二维码的 code、依据 code 烘托二维码、监听扫码成果;手机端运用 uni-app 技能扫描二维码、获取二维码成果、用户授权登录,并告诉服务端用户授权登录。最终,文章介绍了前后端联调过程。
【总结由 ChatGPT 生成】
本文作者:草梅友仁
本文地址:blog.cmyr.ltd/archives/63…
版权声明:转载请注明出处!