温馨提示:本文运用 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 的优势有:

  1. 树立简略:SSE 依据 HTTP 协议,无需像 WebSocket 那样进行握手协议,树立衔接比较简略。
  2. 兼容性好:SSE 对浏览器的支撑性很好,大部分浏览器都能支撑 SSE。
  3. 对服务器资源要求低:SSE 树立的是单向衔接,推送服务端只需向客户端发送数据流,比较 WebSocket 而言,对服务器资源的要求较低。
  4. 实时性好:SSE 能够在服务端有新数据时当行将数据推送到客户端,实时性较好。

而 SSE 的局限性包含:

  1. 单向通讯:SSE 是一种单向通讯协议,只能由服务端向客户端推送数据,无法完成客户端到服务端的双向通讯。
  2. 无法处理很多数据:SSE 的数据传输方式是依据文本的,关于很多数据传输功率较低,简略形成延迟和卡顿。
  3. 无法处理杂乱的业务场景:SSE 的运用场景比较简略,只适用于一些简略的数据推送和告诉场景,无法处理杂乱的业务场景。
  4. 对浏览器的支撑有限:虽然大部分浏览器都支撑 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…
版权声明:转载请注明出处!