切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制

前言

这是一个 React+Nest 实现的全栈社区项目,在上一期中我们已经搭好了基础的架子以及实现了邮箱注册和登录态管理的功能。

在上一期中,我们把用户的登录态封装成了 JWT 的形式,并且把 JWT 写到了 cookie 中。今天我们要实现的是:

  • 在业务代码中便捷的获取用户的登录态
  • 以装饰器的方式区分是否需要登录态的接口
  • 对于需要登录态的接口,请求没有带上登录态,如何统一拦截

往期文章

仓库地址

切图仔做全栈:React&Nest.js 社区平台(一)------基础架构与邮箱注册、JWT 登录实现

中间件注入登录态

在上一期我们已经实现了 JWT 登录,并且把 JWT 存储在了 cookie 里面,这样每次前端请求的时候都会带上 cookie ,后端就知道当前请求的用户是谁。

可以预想到的是,我们会有很多个路由需要用到用户的 id 或者邮箱,那当我们需要用到这些信息的时候,需要如何解析呢?

大概的流程应该是这样的:

  1. cookie 中获取 token
  2. token 中解析出用户信息对象
  3. 获取对应的字段

Nest.js 中,中间件是一种用于处理HTTP请求的机制,它允许在请求到达处理程序之前或之后执行一些逻辑。中间件通常用于执行一些预处理、日志记录、授权等任务。

而中间件的生效范围,我自己把它归为以下三个范围:

  • 全局中间件:所有请求都会生效
  • 控制器中间件:只有对应控制器下的路由才会生效
  • 路由中间件:只有对应的路由才会生效

比如说我们实现了一个日志中间件,他会打印一些东西:

ts 复制代码
function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
  console.log('打印一些东西');
  next();
}

全局中间件可以这样应用:

ts 复制代码
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}
@Module({
  providers: [LoggerMiddleware],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

主要关注 forRoutes('*') 这个配置,它说的是所有路由都生效。那么对某个路由生效配置起来也大同小异:

ts 复制代码
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(
      { path: 'users/getUserInfo', method: RequestMethod.GET }, 
      { path: 'users/updateUserInfo', method: RequestMethod.POST },
    );
  }
}

通过上面的配置,这个中间件就只会在请求 users/getUserInfo 和 请求 users/updateUserInfo的时候生效。

然后也可以使中间件对某个路由生效:

ts 复制代码
import { Controller, Get, Patch, UseMiddleware } from '@nestjs/common';

function loggerMiddleware(req, res, next) {
  console.log('Request...');
  next();
}

@Controller('users')
@UseMiddleware(loggerMiddleware)
export class UsersController {
  @Get(':id')
  getUserInfo(): string {
    return 'getUserInfo';
  }

通过使用 UseMiddleware 装饰器,就可以使中间件在某个路由中生效。

鉴权中间件

我们可以写一个对所有路由都生效的中间件,在请求进入具体路由之前,就把用户信息从 token 中解析出来注入到请求对象里面。

这样每一个路由都能很轻易的获取到用户有没有登录、登录后的信息是什么。

新建一个 auth.middleware.ts 文件,使用到了cookie-parser这个库来解析 cookie

ts 复制代码
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as cookieParser from 'cookie-parser';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly authService: AuthService) {}
  async use(req: Request, res: Response, next: NextFunction) {
    cookieParser()(req, res, () => {});
    const token = req.cookies['token'];
    if (token) {
      const decoded = await this.authService.decodeJwtToken(token);
      req['user'] = decoded;
    }

    next();
  }
}

大致讲下上面的代码:

  1. 首先,使用 cookie-parser 来读取请求里的 cookie
  2. 查找 cookie 中的 token ,然后使用解析 token 获取到用户具体的信息,如 idemail 等。
  3. 注入请求对象中,方便后续获取

最后别忘了在 app.module.ts 中使用这个中间件

ts 复制代码
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(AuthMiddleware).forRoutes('*');
  }
}

鉴权守卫

我们所实现的接口,是有登录态权限之分的。比如说注册、登录这种接口,它不需要校验用户的登录态,但是获取用户信息、点赞、评论这种接口,它是需要用户的登录态的。

这里相关的权限判断可以利用 nest 的守卫机制,守卫的主要目的是在请求到达路由处理程序之前或之后执行某些逻辑,以决定请求是否继续处理。

它们通常用于身份验证、授权、日志记录等场景。 Nest.js 提供了一些内置的守卫,并允许开发者创建自定义守卫。

在这里我们可以实现一个全局守卫,基于上面的鉴权中间件获取到的登录态信息,来对用户的登录态进行校验。

ts 复制代码
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    // 是否不需要登录
    const noAuth = this.reflector.get<boolean>('noAuth', context.getHandler());
    if (noAuth) {
      return true;
    } else {
      const isExpire = request.user && request.user.exp < Date.now();
      if (!request.user || isExpire) {
        throw new UnauthorizedException('未登录');
      }

      return true;
    }
  }
}

解释一下上面的代码做了什么:

  1. 守卫都需要实现 CanActivate 接口的 canActivate 方法,这是 nest 中约定好的一种实现守卫的方式
  2. ExecutionContext 是对特定请求的上下文环境的抽象,包含了关于运行时的所有信息。比如请求对象、响应对象,路由处理函数的参照,控制器的参照等等。上面的例子中使用它获取到了当前请求的处理器对象以及请求对象。
  3. Reflector是一个帮助类,用于提取并获取元数据。在 nest 中,元数据是用于被装饰器在运行时获取信息的一种机制,比如类、方法、参数等。
  4. 首先判断一下这个接口需不需要登录态,如果不需要,直接放行;如果需要且当前登录态还没过期,则放行;否则抛出一个未登录的401异常。

自定义装饰器

从上面的代码可以看到, noAuth 变量是用来标识当前请求不需要鉴权的。要实现这个十分简单,可以实现一个自定义装饰器,代码如下:

ts 复制代码
import { SetMetadata } from '@nestjs/common';
export const NoAuth = () => SetMetadata('noAuth', true);

这里主要用到了 SetMetadata ,它会为被这个装饰器修饰的类或方法加上一个 noAuth 属性,值为true。

然后可以在不需要登录校验的地方加上这个装饰器修饰,比如注册接口:

ts 复制代码
  @Post('register')
  @NoAuth()
  async register(@Body() user: CreateUserDto): Promise<boolean> {
    await this.userService.createUser(user);
    return true;
  }                        

参数装饰器

我们在上面的代码中,把用户信息 user 对象添加到了 request 请求对象中,在 controller 中获取的时候当然也可以使用 @Req 装饰器来获取一整个请求对象,再获取用户对象。

还有另外一种比较好的方式就是实现一个自定义的参数装饰器,比如说我需要实现一个获取用户信息的接口,希望拿到用户的 id

ts 复制代码
  @Get('getUserInfo')
  async getUserInfo(@User('id') userId: number) {
    return await this.userService.getUserInfo(userId);
  }

这里我就实现了一个 @User 装饰器,用它来获取用户相关的参数,直接把参数注入到路由参数中,不需要再路由处理方法中再去获取一遍。

整个装饰器的实现起来也比较简单:

ts 复制代码
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, context: ExecutionContext) => {
    const request = context.switchToHttp().getRequest();
    if (!data) {
      return request.user;
    }
    return request.user ? request.user[data] : null;
  },
);

解释一下上面的代码:

  1. 使用 createParamDecorator 定义一个参数装饰器
  2. context 是当前请求的上下文,可以通过它拿到请求对象
  3. data 是这个装饰器使用的时候传入的参数,如果传了就返回 user 对应的字段,如果不传就返回一整个 user 对象。

最后

以上就是本文的内容,主要介绍了中间件、守卫、自定义装饰器在鉴权以及获取用户信息的实战应用。如果你觉得有帮助的话,点点关注点点赞吧~欢迎评论区一起交流

相关推荐
木子020436 分钟前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing38 分钟前
React核心功能详解(一)
前端·react.js·前端框架
endingCode1 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
Myli_ing2 小时前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
I_Am_Me_2 小时前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
℘团子এ3 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z3 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁3 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜3 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4043 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html