使用nest.js实现sms短信验证码登录功能

1,050 阅读6分钟

前言

个人博客上线已经有段时间了,网站的登录方式一直都还是采用的原始的账号密码登录,这对于博客网站来说极不人性,毕竟谁有时间去记住我一个个人博客网站的帐号密码,于是乎趁着周末徒手撸了一个短信登录,游客只需要记住自己的手机号即可,这比密码登录人性多了。

前置准备

1,在开始开发之前,需要到腾讯云开通短信服务,具体流程腾讯云有着详细的介绍:开通腾讯云短信服务

开通后,拿到下面这些信息:应用appid,appkey,秘钥secretid,secretkey,短信模版id

2,同时,需要在服务器部署好redis数据库。

开始开发

1,nest连接redis数据库并且封装

app.modules.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RedisModule } from '@liaoliaots/nestjs-redis'; // redis nestjs-redis包不支持nest8+
import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { CacheModule } from './cache/cache.module';
import customConfig from './config';


@Module({
    imports: [
        ConfigModule.forRoot({
            isGlobal: true,
            // envFilePath,
            load: [customConfig],
        }),
        RedisModule.forRootAsync({
            imports: [ConfigModule], // 数据库配置项依赖于ConfigModule,需在此引入
            useFactory: (configService: ConfigService) => ({ config: { ...configService.get('redisConfig') } }),
            inject: [ConfigService], // 记得注入服务,不然useFactory函数中获取不到ConfigService
        }),
        CacheModule,
    ],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

cache.modules

import { Module } from '@nestjs/common';
import { CacheService } from './cache.service';
import { CacheController } from './cache.controller';


@Module({
    controllers: [CacheController],
    providers: [CacheService],
    exports: [CacheService],
})
export class CacheModule {}

cache.service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import Redis from 'ioredis';


@Injectable()
export class CacheService {
    constructor(@InjectRedis() private readonly client: Redis) {}


    async setCache(key: string, value: any, second: number = 30) {
        return await this.client.set(key, value, 'EX', second);
    }
    async getCache(key: string) {
        if (!key.length) {
            throw new HttpException('key不能为空', HttpStatus.BAD_REQUEST);
        }
        return await this.client.get(key);
    }
    async delCache(key: string) {
        if (!key.length) {
            throw new HttpException('key不能为空', HttpStatus.BAD_REQUEST);
        }
        return await this.client.del(key);
    }
}

2,引入腾讯云sdk

// 腾讯云sdk
const tencentcloud = require("tencentcloud-sdk-nodejs")

// 导入对应产品模块的client models。
const smsClient = tencentcloud.sms.v20210111.Client

  3,实例化sms对象,并且通过sms发送短信请求

/* 官方文档:https://cloud.tencent.com/document/product/382/43197 */
import { SmsOptions } from 'waylon-blog-ts-types';
const tencentcloud = require('tencentcloud-sdk-nodejs');
const smsClient = tencentcloud.sms.v20210111.Client;


export const tencentSms = (options: SmsOptions) => {
    const { appId, appKey, loginTempId, registerTempId, secretId, secretKey } = options.config;
    const tempArr = [registerTempId, loginTempId];
    // 导入对应产品模块的client models。


    /* 实例化要请求产品(以sms为例)的client对象 */
    const client = new smsClient({
        credential: {
            /* 必填:腾讯云账户密钥对secretId,secretKey。
             * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
             * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
             * 以免泄露密钥对危及你的财产安全。
             * SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
            secretId,
            secretKey,
        },
        /* 必填:地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
        region: 'ap-guangzhou',
        /* 非必填:
         * 客户端配置对象,可以指定超时时间等配置 */
        profile: {
            /* SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段 */
            signMethod: 'HmacSHA256',
            httpProfile: {
                /* SDK默认使用POST方法。
                 * 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
                reqMethod: 'POST',
                /* SDK有默认的超时时间,非必要请不要进行调整
                 * 如有需要请在代码中查阅以获取最新的默认值 */
                reqTimeout: 30,
                /**
                 * 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com
                 */
                endpoint: 'sms.tencentcloudapi.com',
            },
        },
    });


    /* 请求参数,根据调用的接口和实际情况,可以进一步设置请求参数
     * 属性可能是基本类型,也可能引用了另一个数据结构
     * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */


    /* 帮助链接:
     * 短信控制台: https://console.cloud.tencent.com/smsv2
     * 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */
    const params = {
        /* 短信应用ID: 短信SmsSdkAppId在 [短信控制台] 添加应用后生成的实际SmsSdkAppId,示例如1400006666 */
        // 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
        SmsSdkAppId: appId,
        /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
        // 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
        SignName: 'Waylon的树洞个人网',
        /* 模板 ID: 必须填写已审核通过的模板 ID */
        // 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
        TemplateId: tempArr[options.sense],
        /* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空 */
        TemplateParamSet: [options.msg],
        /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
         * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
        PhoneNumberSet: [options.phone],
        /* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
        SessionContext: '',
        /* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */
        ExtendCode: '',
        /* 国际/港澳台短信 senderid(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手] */
        SenderId: '',
    };
    return new Promise((resolve, reject) => {
        client.SendSms(params, function (err, response) {
            // 请求异常返回,打印异常信息
            if (err) {
                console.log(err);
                reject(err);
            }
            // 请求正常返回,打印response对象
            resolve(response);
        });
    });
    // 通过client对象调用想要访问的接口,需要传入请求对象以及响应回调函数

其中SmsOptions配置如下:

export interface SmsOptions {
    config: {
        appId: string; // 应用appid
        appKey: string; // 应用appkey
        loginTempId: string; // 登录短信模版id
        registerTempId: string; // 注册短信模版id
        secretId: string; // 秘钥id
        secretKey: string; // 秘钥key
    };
    phone: string; // 发送的手机号
    msg: string; // 发送的信息 这里传的是一个6位随机数字字符串
    sense: number; // 区分不同的短信场景
}

4,编写sms服务,调用刚刚写好的发送短信服务,并且在回调中将短信以 【手机号】-【验证码】的形式存入redis数据库

核心代码如下:

// 发短信
    async sms(phone: string, sense: number) {
        if (!phone) {
            throw new HttpException('手机号不能为空!', HttpStatus.BAD_REQUEST);
        }
        // 查下redis缓存 2分钟内无法再发短信
        const cache = await this.cacheService.getCache(phone);
        if (cache) {
            throw new HttpException('发短信要钱的喂![○・`Д´・ ○]', HttpStatus.BAD_REQUEST);
        }
        // 生成验证码
        let code = ('000000' + Math.floor(Math.random() * 999999)).slice(-6);
        // 发送短信
        const smsOptions: SmsOptions = {
            config: this.configService.get('smsConfig'),
            phone,
            sense,
            msg: code,
        };
        try {
            const res = await tencentSms(smsOptions);
            if ((res as any).SendStatusSet[0].Code === 'Ok') {
                // 回调OK 则将验证码存入redis 缓存2分钟
                this.cacheService.setCache(phone, code, 600);
            }
            return res;
        } catch (err) {
            throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    // 短信登录
    async smsLogin(params: { phone: string; code: string }) {
        const { phone, code } = params;
        const cache = await this.cacheService.getCache(phone);
        // 检查用户输入的二维码是否与redis缓存中的一致
        if (cache === code) {
            const user = await this.userService.findOneByPhone(phone);
            if (user) {
                this.cacheService.delCache(phone);
                return await this.login(user);
            }
            // 后续逻辑和登录一样
        } else {
            throw new HttpException('再确认下你的验证码![○・`Д´・ ○]', HttpStatus.BAD_REQUEST);
        }
    }

后续的登录流程这里就不过多赘述,自此,一个基于nest.js、redis、腾讯云短信服务打造的登录/注册功能就开发完成了,欢迎大家戳一戳右边旋转的地球🌏体验该功能~(#^.^#)

参考文档:腾讯云Node.js SDK