【NestJS应用从0到1】4.使用passport封装自定义登录认证

923 阅读6分钟

简介 认证 JWT令牌 登录 @nestjs/passport passport-jwt 微信登录

为了让用户更便捷的登录,我们都会采取多种登录方式进行登录,在微信环境下直接使用微信的授权登录,H5则使用密码或验证码登录,内部应用使用JSBridge传递token。当然现在也有一些统一登录的三方可以接入,但是实现一个自己的登录认证也并不麻烦。

其实登录和认证就是两个部分:

  • 登录 - 根据登录方式进行成功后则换取JWT令牌(access-token) 传递给前端
  • 认证JWT - 前端Headers里携带access-token,nestjs后端认证JWT令牌是否正确

其实你可以这么理解认证:认证也是登录。使用JWT令牌方式进行登录。

准备

在前面一节中已经对NestJS的Guard守卫有了一定的了解,在Nest登录和认证同样是用守卫实现并且引入passport-jwt @nestjs/passport

在这之前肯定要对JWT及passport有一定的了解

关于passport

在使用 NestJS 和 Passport 做认证的时候,*.guard.ts*.strategy.ts 是两个关键的文件,它们分别负责不同的任务:

值得注意的是:passport有默认的几个认证方式 如 jwtlocal。当我们使用jwt和local(用户名和密码)的时候,你可以直接使用passport-jwt 和 passport-local,尤其是jwt你可以直接参考官方示例使用即可,它默认给你验证了jwt和返回payload。

*.guard.ts:

定义了一个守卫,用于在请求到达处理程序之前进行验证。

职责 :AuthGuard 是一个守卫(guard),它负责在请求到达处理程序(controller)之前拦截请求,并执行身份验证检查。

实现方式:继承自 AuthGuard('yourAuthName'),其中 'yourAuthName' 是在 *.strategy.ts 中定义的策略名称。

    import { Injectable } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';

    @Injectable()
    export class JwtAuthGuard extends AuthGuard('yourAuthName') {}

*.strategy.ts:

职责:Strategy 是一个策略(strategy),描述如何提取和验证以及如何处理验证后的用户信息。确保只有经过验证的请求才能访问受保护的资源。

在 passport 策略中,定义 validate 方法;返回值会直接影响到认证过程。如果 validate 方法返回一个用户对象,那么认证将被视为成功,用户信息将被附加到请求对象 req.user上。如果 validate 方法返回 null 或抛出异常,则认证将被视为失败。

    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { ExtractJwt, Strategy } from 'passport-jwt';
    import { ConfigService } from '@nestjs/config';

    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy,"yourAuthName") {
      constructor(private readonly configService: ConfigService) {
        super();
      }

      async validate(req: any) {
       // TODO 这里要求返回一个user,表示认证成功并默认会附加到请求req.user上,如果返回 null或者抛出异常则认证失败
      }
    }

Code it

整体逻辑:

  1. 我们先实现JWT的认证再处理登录,一般大多数接口都是要求登录,那我们就把JWT的认证守卫挂载到全局app.module.ts上。
  2. 接着我们要将登录接口屏蔽处理掉,不然登录接口也会要求携带JWT令牌访问。
  3. 实现具体的登录方法,在具体的controller接口上,增加具体的守卫。接下来以微信为例,其他用户名密码登录做对应调整。

流程图

image.png

准备工作

安装 passport

npm install --save @nestjs/passport passport passport-local passport-jwt passport-custom
npm install --save-dev @types/passport-local

JWT认证+允许屏蔽JWT令牌

我们把第一步和第二步一起做的原因是因为为了屏蔽掉JWT令牌的守卫,需要在JWT中增加逻辑。

定义屏蔽

使用SetMetadata设置isPublic = true; 如果Controller带着@Public(),则会将其isPublic=true

import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

重头戏:JWT守卫

  • 这个守卫实现了canActivate方法,判断isPublic直接返回,达到屏蔽的作用
  • 否则检测JWT是否存在,如果存在则尝试使用@nest/jwt进行验证

为什么不直接用passport-jwt的默认strategy?因为除了要处理public屏蔽JWT验证外,我想在token中仅存储有用的信息,而经过auth验证后,会根据token中的payload查询对应用户的权限信息,不会将过多的权限信息存放在JWT-token中,也是为了数据安全。

import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from './jwt.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
  async canActivate(context: ExecutionContext): Promise<any> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }
}
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }
  // 重点是这里,validate 其实是已经验证通过了,return的对象将绑定在req.user上
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

jwt守卫注册

  • 在auth.module中引入

import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      global: true,
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: '2days' },
      }),
      inject: [ConfigService],
    }),
    PassportModule,
  ],
providers:[JwtStrategy]})
import { JwtAuthGuard } from './auth/jwt.guard';
@Module({
      providers: [
        {
          provide: APP_GUARD,
          useClass: JwtAuthGuard,
        },
    ]
    })

全局绑定jwt.guard

import { JwtAuthGuard } from './auth/jwt.guard';
import { AuthModule } from './auth/auth.module';
@Module({

  imports: [AuthModule],
      providers: [
        {
          provide: APP_GUARD,
          useClass: JwtAuthGuard,
        },
    ]
    })

微信登录认证守卫(自定义)

  • 定义微信守卫
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class WechatAuthGuard extends AuthGuard('wechat') {}
  • 实现微信策略
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-custom';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
// CustomException 是自定义的一个错误,可以参考上一篇文章
import { CustomException } from 'src/common/exception/custom-exception';

@Injectable()
export class WechatStrategy extends PassportStrategy(Strategy, 'wechat') {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(req: any): Promise<any> {
    const { code } = req.query;
    if (code) {
    // 这里调用了微信认证方法,通过从url.query中找到code字段进行微信认证
    // 具体实现方式,按照自己业务逻辑来,这里只是示例
      const user = await this.authService.validateWechat(code);
      if (!user) {
        throw new UnauthorizedException();
      }
      return user;
    }
    throw new CustomException({ message: '缺少code' });
  }
}

  • 在auth.module.ts中注册策略
import { WechatStrategy } from './wechat.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
  imports: [ 
      PassportModule,
      JwtModule.register({
          global: true,
          secret: process.env.JWT_SECRET,
          signOptions: { expiresIn: '1days' },
        }),
       ],
  providers: [
    WechatStrategy,
    ]
  • 在auth.controller.ts中使用守卫 请务必注意使用守卫以及装饰器的顺序
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
  // 注意执行顺序
  @Public() // 屏蔽JWT认证
  @UseGuards(WechatAuthGuard) // 使用微信认证
  @Post('login-wechat')
  async loginByWechat(@Request() req) {
    // 生成token,这一步可以直接在具体的WechatAuthGuard中做,这里直接return req.token即可
    return this.authService.generateToken(req.user); 
  }
}

需要注意:

各个模块的执行顺序(生命周期)必须要注意。

为什么无法需要在Controller中需要按照顺序定义,如装饰器,守卫,如果顺序错了,那可能导致代码结果和你预想的不一样,甚至路由都到达不了你所自定义的守卫或者拦截器。所以清楚生命周期顺序是很有必要的。

  • 定义位置的顺序 全局(app.module.ts / main.ts) -> controller -> route方法

  • 各个模块的顺序 (从其他作者偷的图,我就懒得画了) go偷图出处

image.png

完结撒花 ❀❀❀❀❀❀