chenlong-io/blog

nestjs身份验证

Opened this issue · 7 comments

一般业务流程是:验证用户登录信息没问题后,会签发一个 token 给用户 用于之后的接口请求。

给用户签发 JWT

nest 中使用 @nestjs/jwt 来给用户签发 jwt

yarn add @nestjs/jwt

在 auth 模块中引入 jwt 模块

// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    UsersModule,
    // 引入 Jwt 模块并配置秘钥和有效时长
    JwtModule.register({
      secretOrPrivateKey: 'll@feifei',
      signOptions: { expiresIn: '60s' }
    }),
  ],
  providers: [AuthService],
  exports: [AuthService],
  controllers: [AuthController]
})
export class AuthModule {}

在 auth.controller 中新建一个 login 路由用于用户登录

import { Body, Controller, Post, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './auth.dto'

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

  @Post('/login')
  async getHello(@Body() data: LoginDto) {
    return await this.authService.login(data);
  }
}

再看看 service 中怎么使用 jwt

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
     // 引入 JwtService
    private readonly jwtService: JwtService,
  ) {}

  async login(data) {
    const { username, password } = data;
    const user = (await this.usersService.findOne(username))[0];
    if (!user) {
      throw new UnauthorizedException('用户不存在');
    }

    if (user.password !== password) {
      throw new UnauthorizedException('密码不匹配');
    }

    const { id } = user;
    const payload = { id, username };
    // 生成token
    const token = this.signToken(payload);

    return {
      ...payload,
      token,
    };
  }

	signToken(data) {
    return this.jwtService.sign(data);
  }
}

现在使用请求localhost:3000/login

正常返回了当期登录信息 token,

接下来,按照登录流程,成功发放了 token 给前端后,前端在请求其他数据时需要把 token 给后端,后端经过审核 token 有效才会正常返回接口数据。

使用 Jwt 审核 token

一般情况下,前端把 token 放在请求头的 Authorization 字段中,使用 Authorization = 'Bearer tokenString'的方式请求数据

passport 是一个非常好的处理 jwt 的包,在 nestjs 中使用 passport-jwt 策略来完成 token 的安检,现在我们来添加这个策略:

在 auth/jwt.strategy.ts 中添加 JwtStrategy 策略,需要继承 PassportStrategy(Strategy) ,注意:Strategy 是 passport-jwt 包里的

import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';
import { SECRET } from './secret';

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // 配置从头信息里获取token
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 忽略过期: false
      ignoreExpiration: false,
      // secret必须与签发jwt的secret一样
      secretOrKey: SECRET,
    });
  }

  // 实现 validate,在该方法中验证 token 是否合法
  async validate(payload: any) {
    console.log('payload:', payload);
    return payload;
  }
}

写好 jwt 策略后,需要在模块中引入 PassportModule,在 providers 中加入 JwtStrategy,否则无法使用策略:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { SECRET } from './secret';

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      secret: SECRET,
      signOptions: { expiresIn: '60s' },
    }),
    // 引入并配置PassportModule
    PassportModule.register({
      defaultStrategy: 'jwt',
    }),
  ],
  controllers: [AuthController],
  // 引入JwtStrategy
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

现在添加一个路由来验证一下

import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

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

  @Post('login')
  async getHello(@Body() data) {
    return await this.authService.login(data);
  }

  @Get('test')
  // 使用路由级守卫
  @UseGuards(AuthGuard())
  async test() {
    return 'test';
  }
}

需要注意的是,在路由中需要添加 @nestjs/passport 中的 AuthGuard 守卫,否则会直接请求到控制器里面,需要 AuthGuard 守卫来检测 jwt 是否合法,如果合法会放行到控制器中

看看结果:

请求成功了,控制台也成功打印了 payload 信息:

payload: { id: 2, username: 'admin', iat: 1619766245, exp: 1619766305 }

使用全局守卫处理 JWT

上文使用的是路由级别的守卫 使用 AuthGuard 来处理 JWT,但一般项目中,绝大多数接口都是要处理 JWT 的,如果每个接口都写上一遍无疑是一个较大的工程

所以我要使用全局的 AuthGuard 来完成这个功能,但也有些接口不需要 jwt 验证(比如 login、register),所以我们不直接使用全局的 AuthGuard,而是创建一个新的守卫:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

新建的守卫需要实现 CanActivate,这就遇到一个麻烦,怎么把 AuthGuard 拿进来使用呢?

仔细一想, AuthGuard 也是守卫,它内部已经实现了 CanActivate,现在我要用 AuthGuard 的功能,只需要让 JwtAuthGuard 来继承 AuthGuard() 就好了

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();

    const whitelist = ['/login'];

    if (whitelist.find((url) => url === request.url)) {
      return true;
    }

    return super.canActivate(context);
  }
}

代码中通过 request 拿到当前请求的 url,通过与白名单(whitelist)对比达到排除不需要 jwt 认证的接口,非白名单内的接口仍需要通过 super.canActivate(context)。 ps: 这里白名单的处理不全面,建议配合 path-to-regexp 来使用

全局使用 JwtAuthGuard 有两种方式,一种在 app.module.ts 中通过 providers 注册全局提供者:

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

还有一种是在 main.js 中添加全局使用:

app.useGlobalGuards(new JwtAuthGuard());

对于两种方式,官网上是这么说的:

相关链接:https://docs.nestjs.com/guards

这里我们随便用哪种方式都能满足 , Ps: 记得把路由级别的 AuthGuard 去掉~

export class JwtAuthGuard extends AuthGuard('jwt') 这样也拿不到 token 里面的数据

export class JwtAuthGuard extends AuthGuard('jwt') 这样也拿不到 token 里面的数据

你拿token干啥呢,JwtAuthGuard只是个守卫,在auth module 注入PassportStrategy

export class JwtAuthGuard extends AuthGuard('jwt') 这样也拿不到 token 里面的数据

你拿token干啥呢,JwtAuthGuard只是个守卫,在auth module 注入PassportStrategy

忘了当初为啥提出这个问题了。 现在我在请求头里面拿到了

export class JwtAuthGuard extends AuthGuard('jwt') 这样也拿不到 token 里面的数据

你拿token干啥呢,JwtAuthGuard只是个守卫,在auth module 注入PassportStrategy

忘了当初为啥提出这个问题了。 现在我在请求头里面拿到了

记起来了。 是因为这个

@Injectable()
export class RbacAuthGuard extends AuthGuard('jwt') {
    constructor(
        @InjectRedis() private readonly redis: Redis,
        private authService: AuthService,
        private reflector: Reflector,
    ) {
        super();
    }
    async canActivate(context: ExecutionContext): Promise<any> {
        const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
        if (isPublic) return true;

        const request = context.switchToHttp().getRequest();
        const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
        return this.authService.validate(token);
        // 原来会在 req 上挂载一个 user 属性, 自定义的canActive 的时候。 后续的 controller拿不到这个 user 字段
    }
}

export class JwtAuthGuard extends AuthGuard('jwt') 这样也拿不到 token 里面的数据

你拿token干啥呢,JwtAuthGuard只是个守卫,在auth module 注入PassportStrategy

忘了当初为啥提出这个问题了。 现在我在请求头里面拿到了

记起来了。 是因为这个

@Injectable()
export class RbacAuthGuard extends AuthGuard('jwt') {
    constructor(
        @InjectRedis() private readonly redis: Redis,
        private authService: AuthService,
        private reflector: Reflector,
    ) {
        super();
    }
    async canActivate(context: ExecutionContext): Promise<any> {
        const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
        if (isPublic) return true;

        const request = context.switchToHttp().getRequest();
        const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
        return this.authService.validate(token);
        // 原来会在 req 上挂载一个 user 属性, 自定义的canActive 的时候。 后续的 controller拿不到这个 user 字段
    }
}

感觉对AuthGurad理解不到位哈,我习惯通过SetMetadata进行设置变量,然后从this.reflector.getAllAndOverride(NO_AUTH,[]),然后在各自的模块内进行你上面的操作

export const noAuth = () => SetMetadata('NO_AUTH', true);

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

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const noAuthInterception = this.reflector.getAllAndOverride(NO_AUTH, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (noAuthInterception) return true;
    return super.canActivate(context);
  }

  handleRequest(err, user) {
    if (err || !user) {
      throw new ApiException('登录状态已过期', 401);
    }
    return user;
  }
}

然后通过通过noAuth进行给控制器

登录之后,业务中需要拿到jwt中payload包含的用户信息。这个怎么获取呢?request中没有user这个属性。

@Azleal

登录之后,业务中需要拿到jwt中payload包含的用户信息。这个怎么获取呢?request中没有user这个属性。

首先你要确定 jwt 策略中的 validate 有返回用户信息

//  jwt.strategy.ts
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }

确定有了,授权登录后通过 @Request 就可以拿到

  getUser(@Request() req) {
    return req.user;
  }

要优雅一点,可以写个获取用户的装饰器 @User

// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
  @Post('GetUserInfo')
  //通过 @User 获取用户信息
  getUser(@User() user) {
    return user;
  }