blog icon indicating copy to clipboard operation
blog copied to clipboard

nestjs身份验证

Open chenlong-io opened this issue 4 years ago • 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 去掉~

chenlong-io avatar May 06 '21 09:05 chenlong-io

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

abigmiu avatar Sep 09 '22 06:09 abigmiu

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

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

devmsg avatar Oct 19 '22 09:10 devmsg

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

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

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

abigmiu avatar Oct 19 '22 09:10 abigmiu

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 字段
    }
}

abigmiu avatar Oct 20 '22 07:10 abigmiu

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进行给控制器

devmsg avatar Oct 20 '22 08:10 devmsg

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

Azleal avatar Oct 19 '23 02:10 Azleal

@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;
  }

wenfujie avatar Nov 15 '23 09:11 wenfujie