NestJS02:Authentication用户信息、jwt认证

2023年 7月 14日 31.7k 0

前言

一、认证和鉴权

认证和鉴权是后端系统很常见的功能了,这里主要讲一下认证的实现和常见策略(userinfo和jwt认证)

  • 认证(Authentication):主要是对用户信息的一个认证,比如用户登录行为,密码/token验证没问题,则认证用户登录成功
  • 鉴权(Authorization):一般在认证之后,对用户的权限进行鉴定,允许权限内可允许的操作;比如校园系统里,学生可以选课,老师可以进行设置课程时间等;

本文章主要讲认证的部分;

二、nestjs中的认证(Strategy)

生活中认证的方式有很多种,比如你在跟别人打电话过程中,别人要确定你是不是xxx,而不是骗子,那你有几种方式可以认证

  • 身份证号码
  • 学校名称、所在班级、学号
  • 公司信息、职位待遇
  • 身体特征等

以上特征,如果对方匹配无误,就可以完成一次简单的认证,选择其中一种方式认证,也称作“策略(Strategy)”

对应nestjs中的认证,也是对应不同的认证策略,举比较常见的认证策略

  • local策略(账号/密码匹配)
  • token策略(生成的jwt没过期)

简单的流程图

graph LR
前端请求 --> nestjs服务;
--> 对应的策略guards守卫 --> |YES| yse;
graph TD
前端请求 --> nestjs服务 --> 对应路由 --> B{对应策略guard守卫} --> 通过 --> 返回业务信息;
B --> 策略失败 --> 返回401未授权

image.png

三、账号/密码策略实现

首先快速创建项目和代码

nest new nest-auth
cd nest-auth
yarn run start:dev

# 创建auth和users目录
nest g module auth
nest g service auth

nest g module users
nest g service users

执行完毕后,项目会出现authusers目录以及里面的代码,auth对应存储我们实现认证的代码,users对应用户信息的逻辑;正常来说,用户信息会从数据库拿,这里为了demo简单,会写死在代码里面;

然后我们完善一个auth和users一些基础代码的编写:获取用户校验用户,比较简单,直接贴代码

auth.module.ts文件

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

auth.service.ts文件,主要实现了validateUser方法进行用户校验,依赖了userService

import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  async validateUser(username: string, pass: string) {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

users.module.ts文件

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  // 因为Auth模块需要到,所以要exports出去
  exports: [UsersService],
})
export class UsersModule {}

users.service.ts文件

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

export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: 'john',
      password: 'changeme',
    }
  ];

  async findOne(username: string) {
    return this.users.find((user) => user.username === username);
  }
}

基础部分我们实现了,接下来要引入我们的本地策略,nestjs对于认证这块专门维护了一个包@nestjs/passport,另外我们还需要passport-local来实现本地策略,所以需要先安装依赖

yarn add @nestjs/passport passport-local passport
yarn add @types/passport-local -D

接着在auth文件夹中新建local.strategy.ts;我们讲一下这段代码,比较重要

  • 首先我们继承了PassportStrategy方法,传进去了passport-local的Strategy
    • 如果传错了策略构造函数,会导致不生效
  • 其次在构造函数调用了super(),此时super可以传递参数给passport-local的Strategy完成初始化
  • 定义了一个validate函数处理认证(nestjs/passport里面就是通过挨个调用策略的validate处理逻辑)函数参数根据策略不同,这里是username和password
  • 如果匹配成功,返回用户信息塞到当前请求对象上的user,如req.user,否则返回401

local.strategy.ts文件

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

// @nestjs/passport和passport是必须的,前者是基于后者在nest中的实现扩展, 后者是依赖
// passport-local是一种具体的验证策略,比如local可以自定义验证用户密码,还有其他的如passport-jwt

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      // usernameField: 'email',
      // passwordField: 'password',
      // 对应的post请求的时候,参数名称要为email和password
    });
  }

  async validate(username: string, password: string): Promise {
    // console.log(username, password);
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    // based on the value we return from the validate() method, and assigns it to the Request object as req.user.
    // 返回值会被作为一个对象,设置到req.user上面
    return user;
  }
}

策略的具体认证部分我们完成了:用户名和密码匹配;路由进来了怎么进行策略认证呢?答案是:路由守卫;我们在auth里面新建一个local-auth.guard.ts文件

代码如下,比较简单。需要注意的是!!!AuthGuard('local')这里的local不是乱填的,而是因为passport-local导出的Strategy的name就是local;

nestjs需要通过name去找到对应的Strategy,完成认证;我们在继承PassportStrategy(Strategy, 'local')的时候,也可以手动指定name

image.png

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

最后把依赖完善,如下

  • LocalStrategy作为providers添加到 AuthModule
  • 引入PassportModule
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    UsersModule,
    PassportModule,
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})

把路由添加到需要认证的控制器上,我们在app.controller.ts新建一个路由
app.controller.ts

import { Controller, Post, Get, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { Request } from 'express';
import { AuthService } from './auth/auth.service';

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

  // @UseGuards(AuthGuard('local'))
  // 更加推荐下面这种,前者有魔法字符串,不太好维护
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Req() req: Request) {
    console.log(req.user);
    // 认证通过后,req上会有一个user;
    return this.authService.login(req.user);
  }
}

app模块导入对应的service

路由测试

curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme", "email": "john"}' -H "Content-Type: application/json"

image.png

四、jwt策略实现

jwt策略也是类似的,主要是校验用户信息,生成用户token;凭借token完成认证;

首先安装相关依赖

yarn add @nestjs/jwt passport-jwt passport
yarn add @types/passport-jwt -D

在auth里面新建文件

jwt-auth.guard.ts

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

export class JwtGuard extends AuthGuard('jwt') {}

jwt.strategy.ts

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

export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    // 对应初始化passport-jwt策略的参数
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'anysecret',
    });
  }

  async validate(payload: any) {
    // jwt鉴权通过后,会返回鉴权信息,然后将对象设置在req.user上面
    return { userId: payload.sub, username: payload.username };
  }
}

改一下我们的user.service.ts,多了一个login方法

 async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

引入jwt相关依赖,修改auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: 'anysecret',
      signOptions: {
        expiresIn: '60s',
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

修改我们的app.controller.ts,其中login由返回user改成返回token(通过调用authService.login生成),新建profile路由需要token认证;

import { Controller, Post, Get, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from '@nestjs/passport';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { Request } from 'express';
import { AuthService } from './auth/auth.service';
import { JwtGuard } from './auth/jwt-auth.guard';

@Controller()
export class AppController {
  constructor(
    private readonly authService: AuthService,
  ) {}
  
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Req() req: Request) {
    // return req.user;
    // 修改这里
    return this.authService.login(req.user);
  }

  @UseGuards(JwtGuard)
  @Get('profile')
  getProfile(@Req() req) {
    // 这里可以返回鉴权后的用户信息,也可以做其他操作
    return req.user;
  }
}

测试流程:进行login ->> 凭借token调用profile获取信息

头部没有携带token或者错误过期token,直接401

image.png

五、其他

通过这两个策略,大概可以摸清楚nestjs认证是怎么实现的了,首先nestjs需要找到项目中providers注册的策略,然后完成初始化;

通过路由守卫的方式,找到对应name的Strategy完成认证;

希望可以帮助大家理解,大部分内容来自于nestjs官网

相关资料

  • 官网Authentication

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论