Nest.js 身份验证

Jaxson Wang 编程技术 阅读量

0x0 前言

身份验证是大部分系统重要部分,一个系统实现身份验证有很多方法,不过我在 Nest.js 中使用 Passport , 他是 Node.js 中最流行的身份验证库,实现起来很简单并且有很多策略模式。Nest.jsPassport 进行二次封装,使得使用起来更加简便,一个带有身份验证的系统步骤如下:

  • 用户使用用户名和密码、JSON Web Token (JWT) 或者 身份 Token 等相关信息登录
  • 管理身份验证状态
  • 将经过身份验证的用户信息添加到 Request 对象里,方便路由进一步使用

0x1 安装依赖

从最简单的登录开始,使用登录账号和密码登录成功后获取到 Token 身份验证码,然后使用 Token 访问具有 JWT 的请求路由。

安装依赖:

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

Passport 提供 本地护照 的策略,可以实现对用户名和密码身份的验证机制。

0x2 编写策略

使用 @nestjs/passport 实现步骤如下:

  • 在特定的策略模式中,例如 JWT 策略,提供一个密钥对令牌进行签名。
  • 在验证回调中,可以利用 Passport 进行交互验证用户是否存在,然后给客户端发送对应的信息

同样可以用利用 PassportStrategy 类来扩展 Passport 策略,添加自己想要的东西。使用 nest-cli 生成 auth 业务:

nest g module auth
nest g service auth

验证需要用到 UserService ,对于 UserService 业务不再详细描述,这边就利用之前的例子完成,然后在 user.module.ts 需要导出 UserService ,因为需要在 AuthService 使用到:

import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

import { UserController } from './user.controller'
import { UserService } from './user.service'
import { UserEntity } from './user.entity'

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

AuthService 业务主要处理的检索用户并且验证用户密码,创建 validateUser() 方法来处理上述任务,更新auth.service.ts

import { Injectable } from '@nestjs/common'
import { UserService } from '../user/user.service'

@Injectable()
export class AuthService {
  constructor(private userService: UserService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.userService.findOne(username)
    if (user && user.password === pass) {
      const { password, ...result } = user
      return result
    }
    return null
  }
}

然后更新 auth.module.ts 导入 UserModule

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { UserModule } from '../user/user.module'

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

0x3 生成身份验证

新建 auth/local.strategy.ts

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super()
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password)
    if (!user) {
      throw new UnauthorizedException()
    }
    return user
  }
}

上述表示实施 Passport 本地身份验证策略,默认接受 usernamepassword 属性,如果需要指定不同的属性名称,可以构造函数调用:super({ usernameField: 'email' })。大部分验证工作在 AuthService 之下完成,找到用户并且 Token 有效,则会奇偶性下一步任务,否则抛出异常。

更新 auth.module.ts 支持功能:

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { UserModule } from '../users/user.module'
import { PassportModule } from '@nestjs/passport'
import { LocalStrategy } from './local.strategy'

@Module({
  imports: [UserModule, PassportModule],
  providers: [AuthService, LocalStrategy]
})
export class AuthModule {}

0x4 认证状态

对于身份验证的角度下有俩种状态:

  • 用户未来登陆(没有被认证)
  • 用户登录(已验证)

在第一个情况下(未登陆),需要执行俩个不同的功能:

  • 限制未经过身份验证可以访问的路由,Nestjs 可以使用 Guard 注解来支持受限路由。
  • 当未经过身份验证尝试登录时候,启动身份验证步骤需要处理的业务。

0x5 受限访问

新建一个登录路由控制器来进行处理上述的业务:

nest g module login
nest g controller login

在路由控制器添加登录请求:

import { Controller, Request, Post, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport';

@Controller('login')
export class LoginController {
  @UseGuards(AuthGuard('local'))
  @Post('')
  async login(@Request() req) {
    return req.user
  }
}

Passport 本地策略的默认名称为 local 不过为了方便后期扩展,新建新的策略来替代,新建 local-auth.guard.ts

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

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

更新控制器:

@UseGuards(LocalAuthGuard)
@Post('')
async login(@Body() loginUserDto: LoginUserDto) {
  return this.authService.validateUser(loginUserDto)
}

0x6 JWT 生成

下面开始编写 JWT 验证和校验 JWT 路由:

  • 登录校验成功后,生成 JWT 返回客户端
  • 创建基于有效 JWT 作为承载验证受保护的路由

安装依赖:

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

@nestjs/jwt 可以对 JWT 进行管理操作。上述使用 AuthFGuard 本地护照策略就可以做到:

  • 只有验证成功后的用才能调用路由控制器
  • 请求参数包含当前用户属性信息

继续处理 auth.service.ts

import { Injectable } from '@nestjs/common'
import { UserService } from '../user/user.service'
import { JwtService } from '@nestjs/jwt'

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.userService.findOne(username)
    if (user && user.password === pass) {
      const { password, ...result } = user
      return result
    }
    return null
  }

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

使用 JwtService 中的 sign 方法来生成 JWT 作为参数自然是他的用户名和用户编号,方便后期查询当前用户信息,更新 AuthModule 导入 JwtModuleJwtModule 需要密钥,新建 constants.ts 文件:

export const jwtConstants = {
  secret: 'secretKey'
}

注意这个密钥不能公开,可以使用 .env 来管理这个密钥信息。

更新 auth.module.ts

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { LocalStrategy } from './local.strategy'
import { UserModule } from '../user/user.module'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { jwtConstants } from './constants'

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

JwtModule 可以配置更多选项:参考

然后更新控制器返回 JWT,修改 login.controller.ts

import { Controller, Post, UseGuards } from '@nestjs/common'
import { LocalAuthGuard } from './auth/local-auth.guard'
import { AuthService } from './auth/auth.service'

@Controller('login')
export class LoginController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post('')
  async login(@Request() req) {
    return this.authService.login(req.user)
  }
}

0x7 JWT 访问受限路由

新建 jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import { jwtConstants } from './constants'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret
    })
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username }
  }
}

JwtStrategy 注意下面几个选项:

  • jwtFromRequest :提取请求头中的 Authorization 承载的 Token 信息
  • ignoreExpiration :默认 false ,对于没有过期的 JWT 信息继续委托 Passport 下的任务,过期则提示 401http 状态码
  • secretOrKey:签名所需要的密钥信息

validate() 方法是用于 Passport 解密后会调用 validate() 方法,将解码的 JSON 作为参数传递,确保给客户端发送是有效期的 token 信息。

JwtStrategy 加入 AuthModule

import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { LocalStrategy } from './local.strategy'
import { JwtStrategy } from './jwt.strategy'
import { UserModule } from '../user/user.module'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { jwtConstants } from './constants'

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

定义 JwtAuthGuard 扩展内置类,新建jwt-auth.guard.ts

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

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

0x8 关联受限路由

下面继续关联要受限的路由,打开 login.controller.ts 文件,定义新的路由,这个路由需要 JWT 才能访问:

import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common'
import { JwtAuthGuard } from './auth/jwt-auth.guard'
import { LocalAuthGuard } from './auth/local-auth.guard'
import { AuthService } from './auth/auth.service'

@Controller('login')
export class LoginController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post('')
  async login(@Request() req) {
    return this.authService.login(req.user)
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user
  }
}

具体效果如下:

$ # GET /login/profile
$ curl http://localhost:3000/login/profile
$ # result -> {"statusCode":401,"error":"Unauthorized"}

$ # POST /login
$ curl -X POST http://localhost:3000/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }

$ # GET /login/profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/login/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # result -> {"userId":1,"username":"john"}

本案例代码:nest.js-authentication

喵~
Jaxson Wang
阅读此作者的更多文章
Nest.js 授权验证
Nest.js 授权验证

系统授权指的是登录用户执行操作过程,比如管理员可以对系统进行用户操作、网站帖子管理操作,非管理员可以进行授权阅读帖子等操作,所以实现需要对系统的授权需要身份验证机制,下面来实现最基本的基于角色的访问控制系统。

6 分钟阅读
Nest.js 环境变量配置和序列化
Nest.js 环境变量配置和序列化

程序在不同的环境下需要不同的环境变量,例如生产环境、测试环境以及开发环境所需要不同的数据库信息:链接地址、链接端口号、登录用户名和密码相关信息。为了解决这个问题需要进行相关操作。

3 分钟阅读