前言

Nestjs文档中有两种鉴权方式,一种是自定义一个守卫,在守卫中自己从上下文header中取出token信息,然后自己解析判定。

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(
        token,
        {
          secret: jwtConstants.secret
        }
      );
      // 💡 在这里我们将 payload 挂载到请求对象上
      // 以便我们可以在路由处理器中访问它
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

然后官方又引出了一个passport集成,本次文章也是主要讲解passport这个东西。

什么是passport

passport 是一个用于Express框架的身份验证中间件,由于没看它的具体实现,个人猜测应该使用了策略模式,它会有一个接口约束所有策略的实现,并且要求在验证成功后返回一个用户信息数据,然后这个数据会被赋值到express的request对象上去,且属性固定为user

然后他会有很多策略,google、facebook、weixin等等,而我们本次需要的只是jwt策略。

安装依赖

pnpm i @nestjs/passport passport passport-jwt
pnpm i @types/passport-jwt -D

实现一个jwt策略

创建一个jwt.strategy.ts文件,我是把这个文件放在了utils/passport目录中。

import { Strategy, ExtractJwt } from "passport-jwt";
import type { StrategyOptions } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "src/prisma/prisma.service";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
    constructor(
        private readonly config: ConfigService,
        private readonly prisma: PrismaService
    ) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            secretOrKey: config.get("TOKEN_SECRET")
        } as StrategyOptions);
    }

    /** token解析成功后通过sub(id)查询用户并返回 */
    async validate({ sub }) {
        return await this.prisma.user.findUnique({
            where: {
                id: sub
            }
        });
    }
}

我们创建一个JwtStrategy类,继承PassportStrategy(Strategy, "jwt"),其中"jwt"是用于命名的,后续通过AuthGuard("jwt")来使用。

既然是继承,在构造函数中就必须调用super(),传入两个参数:

一个是用于获取token信息的方法,这个方法passport-jwt已经提供了,它会获取头信息中Authorization字段,然后拿到里面的token,这个token配置被称为Bearer Token,是http规范中定义的一个属性,当然如果有的人不用也不是不行,如果不是这种就需要自己写一个函数去实现这个功能了。

查看jwtFromRequest这个属性的类型:

export interface StrategyOptions {
  ...
  jwtFromRequest: JwtFromRequestFunction;
  ...
}


export interface JwtFromRequestFunction {
    (req: express.Request): string | null;
}

可以看到接受了一个参数,参数就是express.Request对象,具体自定义怎么实现这个函数不细搞了,自己百度都有现成的。

第二个参数就是生成token用到的密匙,这个我们一样从ConfigService中取值。

当token被拿到后,Passport就会解析它,解析成功后调用validate方法,把token解析出的对象传参给它,此时我们需要做的就是返回查询到的用户对象,这个也是Passport所规范的,所有的策略都需要返回内容,至于这个内容是不是必须是用户信息,这个就取决于你需要使用什么信息,最终这个数据会被赋值到request的user属性上。

至此我们策略实现完成。

实现一个白名单功能(公共路由装饰器)

如果我们需要实现一个全局的jwt守卫,就表示所有的路由都需要鉴权,但是明显是不可能的,肯定会有访客接口的,虽然不多,所以我们要实现一个白名单功能给部分路由使用。

实现思路:我们给对象添加一个元数据,如果存在这个元数据就表示它是一个白名单,在安全守卫中就直接放行。

这个其实官方也有提供给我们,文档:安全守卫

我们创建一个文件:public.ts,我是存放在utils/decorators目录下。

import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

然后在我们在不需要鉴权的路径中添加:

import { Body, Controller, Get, Post, Request } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    /** 注册用户 */
    @Post("register")
    @Public()
    async register(@Body() data: RegisterDto) {
        const user = await this.authService.register(data);

        return user;
    }

    /** 登录用户 */
    @Post("login")
    @Public()
    async login(@Body() data: LoginDto) {
        return await this.authService.login(data);
    }
}

实现一个安全守卫

现在我们可以安心实现一个安全守卫了。

创建文件:jwtAuth.guard.ts,我是存放在utils/guards目录下。

import { ExecutionContext, Injectable, UnauthorizedException, NotFoundException } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_KEY } from "../decorators/public";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
    constructor(private reflector: Reflector) {
        super();
    }

    /** 验证token */
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        // 是否是公共路由
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
            context.getHandler(),
            context.getClass()
        ]);
        if (isPublic) return true;
        // 校验token
        return super.canActivate(context);
    }

    /**
     * @description: 验完成后调用
     * @param {*} error 这是 Passport 策略执行过程中发生的任何潜在错误。如果在验证过程中没有错误发生,这个值通常是 null
     * @param {*} user 这是 Passport 策略验证成功后返回的用户对象。如果验证失败,这个值可能是 false 或 null,具体取决于你使用的 Passport 策略
     * @param {*} info 如果验证失败,info通常是一个error对象
     * @Date: 2024-01-02 13:14:47
     * @Author: mulingyuer
     */
    handleRequest(error, user, info) {
        if (info || error) throw new UnauthorizedException("token校验失败");
        if (!user) throw new NotFoundException("用户不存在");

        return user;
    }
}

canActivate钩子函数负责验证token,我们加入白名单处理,剩下的直接使用父类的实现就行。

handleRequest看注释吧。

导入依赖

功能都实现完成后,我们将依赖注入到app.module.ts中。

import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { AuthModule } from "./auth/auth.module";
import { PrismaModule } from "./prisma/prisma.module";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./utils/passport/jwt.strategy";
import { JwtAuthGuard } from "./utils/guards/jwtAuth.guard";
import { APP_GUARD } from "@nestjs/core";

const NODE_ENV = process.env.NODE_ENV;

@Module({
    imports: [
        ConfigModule.forRoot({
            isGlobal: true,
            envFilePath: NODE_ENV === "development" ? ".env.development" : `.env.${NODE_ENV}`
        }),
        PrismaModule,
        JwtModule.registerAsync({
            global: true,
            inject: [ConfigService],
            useFactory: (configService: ConfigService) => {
                return {
                    secret: configService.get("TOKEN_SECRET"),
                    signOptions: {
                        expiresIn: "30d"
                    }
                };
            }
        }),
        AuthModule
    ],
    controllers: [],
    providers: [
        JwtStrategy,
        {
            provide: APP_GUARD,
            useClass: JwtAuthGuard
        }
    ]
})
export class AppModule {}

providers注入我们的策略和守卫,其中守卫通过import引入的APP_GUARD注册为全局的守卫,当然全局守卫是可以多个的。

providers: [
    {
        provide: APP_GUARD,
        useClass: JwtAuthGuard
    },
    {
        provide: APP_GUARD,
        useClass: xxx
    },
    {
        provide: APP_GUARD,
        useClass: xxx
    },
]

如果你ctrl+鼠标左键点进去APP_GUARD,你会发现他就是Nestjs提供的一个字符串常量。

依赖导入后就行了,现在我们的全局守卫也生效了,白名单也有了。

非全局使用

如果不想使用全局,可以通过@UseGuards(AuthGuard('jwt')装饰器方式使用:

import { Body, Controller, Get, Post, Request, UseGuards } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";
import { JwtAuthGuard } from "src/utils/guards/jwtAuth.guard";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    /** 注册用户 */
    @Post("register")
    @Public()
    async register(@Body() data: RegisterDto) {
        const user = await this.authService.register(data);

        return user;
    }

    /** 登录用户 */
    @Post("login")
    @Public()
    async login(@Body() data: LoginDto) {
        return await this.authService.login(data);
    }

    @Get("test")
    @UseGuards(JwtAuthGuard)
    test() {
      return "test";
    }
}

由于我们已经自定义了守卫,所以在使用的时候需要在UseGuards中传入我们自定义的守卫,不要使用@UseGuards(AuthGuard('jwt'))的形式,它只会触发实现的jwt策略,报错信息就会变成默认值,而不是我们自定义的信息了。

简化操作

@UseGuards(JwtAuthGuard)这种用法有点复杂,我们可以封装成一个装饰器实现和@Public()一样简洁的用法。

创建jwtAuth.ts

import { applyDecorators, UseGuards } from "@nestjs/common";
import { JwtAuthGuard } from "src/utils/guards/jwtAuth.guard";

export function JwtAuth() {
    return applyDecorators(UseGuards(JwtAuthGuard));
}

然后使用的时候:

import { Body, Controller, Get, Post, Request } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";
import { LoginDto } from "./dto/login.dto";
import { AuthService } from "./auth.service";
import { Public } from "src/utils/decorators/public";
import { JwtAuth } from "src/utils/decorators/jwtAuth";

@Controller("auth")
export class AuthController {
    constructor(private readonly authService: AuthService) {}

    /** 注册用户 */
    @Post("register")
    @Public()
    async register(@Body() data: RegisterDto) {
        const user = await this.authService.register(data);

        return user;
    }

    /** 登录用户 */
    @Post("login")
    @Public()
    async login(@Body() data: LoginDto) {
        return await this.authService.login(data);
    }

    @Get("test")
    @JwtAuth()
    test() {
      return "test";
    }
}

效果也是ok的。

实现获取request.user属性的参数装饰器

这个其实在Nestjs的官方文档有提供,当我们使用了passport策略后,它会在request的user上赋值我们return的值,但是我们去获取的话每次都要先获取req再来req.user获取,有点麻烦,我们可以实现一个更便捷的方式。

官方文档:参数装饰器

创建文件:user.ts,我是存放在utils/decorators目录下。

import { createParamDecorator } from "@nestjs/common";
import type { ExecutionContext } from "@nestjs/common";

export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
});

使用时:

import { Controller, Get, Req } from "@nestjs/common";
import { User } from "src/utils/decorators";

@Controller("auth")
export class AuthController {

    @Get("test")
    async test(@Req() req, @User() user) {
        console.log("🚀 ~ file: auth.controller.ts:28 ~ req:", req.user, user);
        return "";
    }
}

除了req.user,通过@User也可以拿到user。但是此时user的类型是any,我们可以从prisma中引入user的类型。

import { Controller, Get, Req } from "@nestjs/common";
import { User } from "src/utils/decorators";
import type { user as UserType } from "@prisma/client";

@Controller("auth")
export class AuthController {

    @Get("test")
    async test(@Req() req, @User() user: UserType) {
        console.log("🚀 ~ file: auth.controller.ts:28 ~ req:", req.user, user);
        return "";
    }
}

因为user重名,只能as重新定义个新类型名字。

分类: Nest.js 标签: 白名单Nestjspassportjwt策略全局守卫

评论

暂无评论数据

暂无评论数据

目录