前言
这是一个 React+Nest
实现的全栈社区项目,在上一期中我们已经搭好了基础的架子以及实现了邮箱注册和登录态管理的功能。
在上一期中,我们把用户的登录态封装成了 JWT
的形式,并且把 JWT
写到了 cookie
中。今天我们要实现的是:
- 在业务代码中便捷的获取用户的登录态
- 以装饰器的方式区分是否需要登录态的接口
- 对于需要登录态的接口,请求没有带上登录态,如何统一拦截
往期文章
仓库地址
切图仔做全栈:React&Nest.js 社区平台(一)——基础架构与邮箱注册、JWT 登录实现
中间件注入登录态
在上一期我们已经实现了 JWT
登录,并且把 JWT
存储在了 cookie
里面,这样每次前端请求的时候都会带上 cookie
,后端就知道当前请求的用户是谁。
可以预想到的是,我们会有很多个路由需要用到用户的 id
或者邮箱,那当我们需要用到这些信息的时候,需要如何解析呢?
大概的流程应该是这样的:
- 在
cookie
中获取token
- 在
token
中解析出用户信息对象 - 获取对应的字段
在 Nest.js
中,中间件是一种用于处理HTTP请求的机制,它允许在请求到达处理程序之前或之后执行一些逻辑。中间件通常用于执行一些预处理、日志记录、授权等任务。
而中间件的生效范围,我自己把它归为以下三个范围:
- 全局中间件:所有请求都会生效
- 控制器中间件:只有对应控制器下的路由才会生效
- 路由中间件:只有对应的路由才会生效
比如说我们实现了一个日志中间件,他会打印一些东西:
function loggerMiddleware(req: Request, res: Response, next: NextFunction) { console.log('打印一些东西'); next(); }
全局中间件可以这样应用:
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('*')
这个配置,它说的是所有路由都生效。那么对某个路由生效配置起来也大同小异:
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
的时候生效。
然后也可以使中间件对某个路由生效:
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
。
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(); } }
大致讲下上面的代码:
- 首先,使用
cookie-parser
来读取请求里的cookie
。 - 查找
cookie
中的token
,然后使用解析token
获取到用户具体的信息,如id
、email
等。 - 注入请求对象中,方便后续获取
最后别忘了在 app.module.ts
中使用这个中间件
export class AppModule { configure(consumer: MiddlewareConsumer) { consumer.apply(AuthMiddleware).forRoutes('*'); } }
鉴权守卫
我们所实现的接口,是有登录态权限之分的。比如说注册、登录这种接口,它不需要校验用户的登录态,但是获取用户信息、点赞、评论这种接口,它是需要用户的登录态的。
这里相关的权限判断可以利用 nest
的守卫机制,守卫的主要目的是在请求到达路由处理程序之前或之后执行某些逻辑,以决定请求是否继续处理。
它们通常用于身份验证、授权、日志记录等场景。 Nest.js
提供了一些内置的守卫,并允许开发者创建自定义守卫。
在这里我们可以实现一个全局守卫,基于上面的鉴权中间件获取到的登录态信息,来对用户的登录态进行校验。
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; } } }
解释一下上面的代码做了什么:
- 守卫都需要实现
CanActivate
接口的canActivate
方法,这是nest
中约定好的一种实现守卫的方式 ExecutionContext
是对特定请求的上下文环境的抽象,包含了关于运行时的所有信息。比如请求对象、响应对象,路由处理函数的参照,控制器的参照等等。上面的例子中使用它获取到了当前请求的处理器对象以及请求对象。Reflector
是一个帮助类,用于提取并获取元数据。在nest
中,元数据是用于被装饰器在运行时获取信息的一种机制,比如类、方法、参数等。- 首先判断一下这个接口需不需要登录态,如果不需要,直接放行;如果需要且当前登录态还没过期,则放行;否则抛出一个未登录的
401
异常。
自定义装饰器
从上面的代码可以看到, noAuth
变量是用来标识当前请求不需要鉴权的。要实现这个十分简单,可以实现一个自定义装饰器,代码如下:
import { SetMetadata } from '@nestjs/common'; export const NoAuth = () => SetMetadata('noAuth', true);
这里主要用到了 SetMetadata
,它会为被这个装饰器修饰的类或方法加上一个 noAuth
属性,值为true。
然后可以在不需要登录校验的地方加上这个装饰器修饰,比如注册接口:
@Post('register') @NoAuth() async register(@Body() user: CreateUserDto): Promise<boolean> { await this.userService.createUser(user); return true; }
参数装饰器
我们在上面的代码中,把用户信息 user
对象添加到了 request
请求对象中,在 controller
中获取的时候当然也可以使用 @Req
装饰器来获取一整个请求对象,再获取用户对象。
还有另外一种比较好的方式就是实现一个自定义的参数装饰器,比如说我需要实现一个获取用户信息的接口,希望拿到用户的 id
:
@Get('getUserInfo') async getUserInfo(@User('id') userId: number) { return await this.userService.getUserInfo(userId); }
这里我就实现了一个 @User
装饰器,用它来获取用户相关的参数,直接把参数注入到路由参数中,不需要再路由处理方法中再去获取一遍。
整个装饰器的实现起来也比较简单:
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; }, );
解释一下上面的代码:
- 使用
createParamDecorator
定义一个参数装饰器 context
是当前请求的上下文,可以通过它拿到请求对象data
是这个装饰器使用的时候传入的参数,如果传了就返回user
对应的字段,如果不传就返回一整个user
对象。
最后
以上就是本文的内容,主要介绍了中间件、守卫、自定义装饰器在鉴权以及获取用户信息的实战应用。如果你觉得有帮助的话,点点关注点点赞吧~欢迎评论区一起交流