【nestjs+mongodb】- 全栈- passport策略的用户登录

1,193

github代码

GitHub仓库地址 需要的可以git clone,喜欢的可以点个star

思路

要实现登录模块,首先需要一个auth权限模块去做

auth权限模块

要开发auth用户权限模块

  1. 首先先要有注册用户模块,其中,libs的user模型对应的用户名和密码的定义如下。 其中select: false是为了不向客户端返回密码,尽管我们对其进行散列加密,但还是不要传回客户端为好。

user.module.ts

作用:主要供auth注册时,需要注入user 模型到auth模块上

import { prop, modelOptions, DocumentType } from '@typegoose/typegoose';
import { ApiProperty } from '@nestjs/swagger';
import { hashSync } from 'bcryptjs';

export type UserDocument = DocumentType<User>;

@modelOptions({
  schemaOptions: {
    timestamps: true,
  },
})
export class User {
  @ApiProperty({ description: '用户名', example: 'user1' })
  @prop()
  username: string;

  @ApiProperty({ description: '密码', example: 'pass1' })
  @prop({
    select: false,
    get(val) {
      return val;
    },
    set(val) {
      return val ? hashSync(val) : val;
    },
  })
  password: string;
}

自定义装饰器

有上面的代码可知,我们需要取到jwt token校验后的user属性,因此我们需要封装对应装饰器去取到,由于该currentUser装饰器的逻辑简单,只需要取对应req的user即可

import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { Request } from "express";

export const CurrentUser = createParamDecorator((data, ctx: ExecutionContext) => {
  const req: Request = ctx.switchToHttp().getRequest()
  return req.user
})

passport 策略

校验用户名和密码,通过model.findOne找到对应的name

安装对应的包

pnpm i passport-jwt passport passport-local

local.strategy

import { Strategy, IStrategyOptions } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { InjectModel } from 'nestjs-typegoose';
import { User } from '@libs/db/models/user.model';
import { ReturnModelType } from '@typegoose/typegoose';
import { BadRequestException } from '@nestjs/common';
import {compareSync} from 'bcryptjs'

export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
  constructor(
    @InjectModel(User) private userModel: ReturnModelType<typeof User>,
  ) {
    super({
      usernameField: 'username',
      passwordField: 'password',
    } as IStrategyOptions);
  }

  async validate(username: string, password: string) {
    const user = await this.userModel.findOne({ username }).select('+password');
    if (!user) {
      throw new BadRequestException('用户名不正确');
    }
    if (!compareSync(password, user.password)) {
      throw new BadRequestException('密码不正确')
    }
    return user;
  }
}

小结

用户使用流程,登录时,会在调用登录接口时,用一个守卫guard去拦截用户信息,通过passport去检验用户是否存在在数据库。

auth模块 控制器 (auth.controller.ts)

auth模块 用于用户登录,使用到了passport策略去进行校验

import { Controller, Post, Body, Get, UseGuards, Req } from '@nestjs/common';
import {
  ApiTags,
  ApiOperation,
  ApiProperty,
  ApiBearerAuth,
} from '@nestjs/swagger';
import { InjectModel } from 'nestjs-typegoose';
import { User, UserDocument } from '@libs/db/models/user.model';
import { ReturnModelType, DocumentType } from '@typegoose/typegoose';
import { AuthGuard } from '@nestjs/passport';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
import { CurrentUser } from './current-user.decorator';

@Controller('auth')
@ApiTags('用户')
export class AuthController {
  constructor(
    private jwtService: JwtService,
    @InjectModel(User) private userModel: ReturnModelType<typeof User>,
  ) {}

  @Post('register')
  @ApiOperation({ summary: '注册' })
  async register(@Body() dto: RegisterDto) {
    const { username, password } = dto;
    const user = await this.userModel.create({
      username,
      password,
    });
    return user;
  }

  @Post('login')
  @ApiOperation({ summary: '登录' })
  @UseGuards(AuthGuard('local'))
  async login(@Body() dto: LoginDto, @CurrentUser() user: UserDocument) {
    return {
      token: this.jwtService.sign(String(user._id)),
    };
  }

  @Get('user')
  @ApiOperation({ summary: '获取个人信息' })
  @UseGuards(AuthGuard('jwt'))
  @ApiBearerAuth()
  async user(@CurrentUser() user: UserDocument) {
    return user;
  }
}

libs/common 通用模块

import { Module, Global } from '@nestjs/common';
import { CommonService } from './common.service';
import { ConfigModule } from '@nestjs/config'
import { DbModule } from '@libs/db';
import {JwtModule} from '@nestjs/jwt'

@Global()
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    JwtModule.registerAsync({
      useFactory(){
        return {
          secret: process.env.SECRET
        }
      }
    }),
    DbModule,
  ],
  providers: [CommonService],
  exports: [CommonService, JwtModule],
})
export class CommonModule {}

小结

通过passport策略检验成功后,返回一个jwt配合本地秘钥生成token,供用户调用其他接口时使用,因此我们需要一个jwt策略去构造token,我们先了解一下JWT

什么是 JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。 是一种认证授权机制。
  • JWT是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。
  • JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
  • 可以去看阮一峰老师的JSON Web Token 入门教程,讲的非常通俗易懂 在这里插入图片描述

JWT 认证流程:

  1. 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
  2. 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
  3. 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样
Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

jwt.strategy

import { Strategy, StrategyOptions, ExtractJwt } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { InjectModel } from 'nestjs-typegoose';
import { User } from '@libs/db/models/user.model';
import { ReturnModelType } from '@typegoose/typegoose';


export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectModel(User) private userModel: ReturnModelType<typeof User>,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.SECRET
    } as StrategyOptions);
  }

  async validate(id) {
    return await this.userModel.findById(id)
  }
}

至此,我们基于passport策略完成了企业级的用户登录过程实现,具体可以查看我GitHub源码