【在线聊天室】前端进阶全栈开发

登录注册身份认证、私聊、聊天室

项目前端React18仓库:github.com/mcmcCat/mmc…[1]
项目后端Nestjs仓库:github.com/mcmcCat/mmc…[2]
语雀上的笔记:www.yuque.com/maimaicat/t…[3]

技术栈:
Nestjs企业级Node服务端框架+TypeOrm(Mysql)+JWT+Socket.IO🎉
React18/服务端渲染Nextjs+Redux-toolkit+styled-components🎉

Nestjs 是一个用于构建高效可扩展的一个基于Node js 服务端 应用程序开发框架。本文不过多赘述,网上的教程有很多。
(注意:对于聊天中user模块和message模块的接口可参考仓库代码,在这里只分析登录注册的身份认证)

下面可以放张图稍微感受一下,用nest写接口很方便。 @Post('auth/register')使用装饰器的方式,当你请求这个接口时,会自动调用下方函数AuthRegister。另外还可以加一大堆装饰器,用于生成swagger接口文档,做本地验证、jwt验证等等。

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Get,
  Post,
  Req,
  Res,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { AppService } from './app.service';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth/auth.service';
import { CreateUserDto } from './user/dto/create-user.dto';
import { LoginDTO } from './auth/dto/login.dto';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @ApiTags('JWT注册')
  @Post('auth/register')
  async AuthRegister(@Body() body: CreateUserDto) {
    return await this.appService.authRegister(body);
  }

  @UseInterceptors(ClassSerializerInterceptor) 
  @UseGuards(AuthGuard('local')) 
  @ApiTags('JWT登录')
  @Post('auth/login')
  async AuthLogin(@Body() body: LoginDTO, @Req() req) {
    
    return await this.appService.authLogin(req.user);
  }
}


密码加密 和 生成token

我们可以跟着代码仓库[4],带有详细的注释,一步步地走
app.service.ts 负责定义注册authRegister和登录authLogin

  • 在注册时,拿到用户输入的密码,使用**bcryptjs.hash()**将其转换为 hash加密字符串,并存入数据库

  • 在(身份认证的)登录时,先进行校验用户登录信息是否正确在这里我们使用的是[passport-local](http://nestjs.inode.club/recipes/passport#%E5%AE%9E%E7%8E%B0-passport-%E6%9C%AC%E5%9C%B0%E7%AD%96%E7%95%A5)本地策略来验证,@UseGuards(AuthGuard('local'))这个装饰器会在此处的post请求@Post('auth/login')后进行拦截,去local.strategy.ts中进行validate检索出该用户的信息,然后我们使用**bcryptjs.compareSync()**将 **用户输入的密码 **与数据库中用 **hash加密过的密码 **进行解析对比,若登录信息正确则接着调用AuthLogin,进而调用(认证成功的)登录接口authService.login(),即向客户端发送登录成功信息并且是携带有**token**的,

async login(user: any) {
    
    const payload = { username: user.username, sub: user.id };
    return {
      code: '200',
      
      access_token: this.jwtService.sign(payload), 
      msg: '登录成功',
    };
  }

校验token合法性

那么这个token我们在哪里去拦截它进行校验呢?

那就要提到我们 nest 的guard(守卫)这个概念。其实就好比我们在vue项目中,封装路由前置守卫拦截路由跳转,去获取存储在localStoragetoken一样。

在 nest 守卫中我们可以去获取到请求体req,从而获取到请求头中的Authorization字段,查看是否携带token,然后去校验token合法性,authService.verifyToken()中调用jwtService.verify()进行token的令牌格式校验、签名验证、过期时间校验,确保令牌的完整性、真实性和有效性

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from 'src/app.module';
import { AuthService } from './auth.service';
import { UserService } from 'src/user/user.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor() {
    super();

  }
  async canActivate(context: ExecutionContext): Promise<any> {
    const req = context.switchToHttp().getRequest();
  
   
    if (this.hasUrl(this.whiteList, req.url)) return true;
    try {
      const accessToken = req.get('Authorization');
      
      if (!accessToken) throw new UnauthorizedException('请先登录');

      const app = await NestFactory.create<NestExpressApplication>(AppModule);
      const authService = app.get(AuthService);
      const userService = app.get(UserService);
      
      const tokenUserInfo = await authService.verifyToken(accessToken);
      const resData = await userService.findOne(tokenUserInfo.username);
      
     if (resData[0].id) return true;
    } catch (e) {
      console.log('1h 的 token 过期啦!请重新登录');
      return false;
    }
  }
    
  private whiteList: string[] = ['/auth/register','/auth/login'];

  
  private hasUrl(whiteList: string[], url: string): boolean {
    let flag = false;
    if (whiteList.indexOf(url) !== -1) {
      flag = true;
    }
    return flag;
  }
}

guard中,当我们return true时,好比路由前置守卫的next(),就是认证通过了放行的意思

当然,别忘了注册守卫,我们这里可以采用全局守卫的形式注册,在main.tsapp.useGlobalGuards(new JwtAuthGuard());

Nest中WebSocket网关的作用

使用 @WebSocketGateway[5] 装饰器配置 WebSocket 网关在 Nest.js 应用中具有以下作用

  1. 提供 WebSocket 的入口点:WebSocket 网关允许客户端通过 WebSocket 协议与后端建立实时的双向通信。通过配置网关,你可以定义用于处理 WebSocket 连接、消息传递和事件的逻辑。

  2. 处理跨域请求:在 WebSocket 中,默认存在跨域限制,即只能与同源的 WebSocket 服务进行通信。通过设置 origin 选项,WebSocket 网关可以解决跨域请求问题,允许来自指定源的请求进行跨域访问。

关于Socket.IO是怎么通讯的可以看看官网给出的图

socketIO是通过事件监听的形式,我们可以很清晰的区分出消息的类型,方便对不同类型的消息进行处理,客户端和服务端双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以把各种消息进行有序管理了

下面是一个简单的通讯事件示例:

import {
  WebSocketGateway,
  SubscribeMessage,
  WebSocketServer,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { SocketService } from './socket.service';
import { CreateSocketDto } from './dto/create-socket.dto';
import { UpdateSocketDto } from './dto/update-socket.dto';
import { Socket } from 'socket.io';

const roomList = {};
let roomId = null;
let user = '';
@WebSocketGateway(3001, {
  allowEIO3: true, 
  
  cors: {
    
    origin: 'http://localhost:8080', 
    
    credentials: true,
  },
})
export class SocketGateway {
  constructor(private readonly socketService: SocketService) {}
  
  @SubscribeMessage('ToClient')
  ToClient(@MessageBody() data: any) {
    
    const forwardMsg: string = '服务端=>客户端';
    return {
      
      event: 'forward',
      data: forwardMsg, 
    };
  }

  
  @SubscribeMessage('toServer')
  handleServerMessage(client: Socket, data: string) {
    console.log(data + ' (让我服务端来进行一下处理)');
    client.emit('ToClient', data + '(处理完成给客户端)');
  }
}


私聊模块中的 socket 事件

通过使用client.broadcast.emit('showMessage')client.emit('showMessage'),你可以实现多人实时聊天的功能。
当一个客户端发送一条消息时,通过 client.broadcast.emit('showMessage') 将该消息广播给其他客户端,让其他客户端可以接收到这条消息并进行相应的处理,从而实现多人实时聊天的效果。
同时,使用 client.emit('showMessage') 可以将消息发送给当前连接的客户端,这样当前客户端也会收到自己发送的消息,以便在界面上显示自己发送的内容。

@SubscribeMessage('sendMessage')
sendMessage(client: Socket) {
  
  client.broadcast.emit('showMessage');
  
  client.emit('showMessage');
  return;
}

前端中会在UserList.tsx监听该事件showMessage,并触发更新信息逻辑

useEffect(() => {
    socket.on('showMessage', getCurentMessages)
    return () => {
        socket.off('showMessage')
    }
})

房间模块中的 socket 事件

@SubscribeMessage('sendRoomMessage')
sendRoomMessage(client: Socket, data) {
  console.log('服务端接收到了');

  
  this.socketIO.to(roomId).emit('sendRoomMessage', data);
  return;
}

在需要发送消息给指定房间时,即我们需要在全局中找到指定房间,所以我们需要整个 WebSocket 服务器的实例

@WebSocketServer()
socketIO: Socket; 


加入和退出房间的 socket API

client.join(roomId);

client.leave(roomId);

注意这个socket API的作用只是会被用于this.socketIO.to(roomId).emit('sendRoomMessage', data)时的指定to房间去发送信息,而对于房间人员的变动情况得自己准备一个对象来记录,如roomList

踩坑

  1. socket实例的创建写在了函数组件内,useState中的变量的频繁改变,导致的组件不断重新渲染,socket实例也被不断创建,形成过多的连接,让websocket服务崩溃!!!

解决:
把socket实例的创建拿出来放在单独的文件中,这样在各个函数组件中若使用的话只用引用这一共同的socket实例,仅与websocket服务器形成一个连接

  1. socket事件的监听没有及时的停止,导致对同一事件的监听不断叠加(如sys事件),当触发一次这一事件时,会同时触发到之前叠加的所有监听函数!!!
    项目中的效果就是不断重新进入房间时,提示信息的渲染次数会递增的增加,而不是只提示一次

解决:
在离开房间后要socket.off('sys');要停止事件监听,另外最好是在组件销毁时停止所有事件的监听(此处为Next/React18,即项目前端的代码[6])

useEffect(() => {
  console.log('chat组件挂载');
  
  socket.on('connect', () => {
    socket.emit('connection');
    
  });
  return () => {
    console.log('chat组件卸载');
    socket.off();
  };
}, []);


@SubscribeMessage('connection')
connection(client: Socket, data) {
  console.log('有一个客户端连接成功', client.id);
 
  client.on('disconnect', () => {
    console.log('有一个客户端断开连接', client.id);
    
  });
  return;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值