Nest.js 身份验证

0x0 前言

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

  • 用户使用用户名和密码、 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
本地身份验证策略,默认接受 username
password
属性,如果需要指定不同的属性名称,可以构造函数调用: 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
导入 JwtModule
JwtModule
需要密钥,新建 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
    下的任务,过期则提示 401
    http
    状态码
  • 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"}
淮城一只猫
我还没有学会写个人说明!
上一篇

5种数据同分布的检测方法!

下一篇

暴雪:《魔兽争霸3》重制版犯了很多错误 《暗黑2》重制版不再犯

你也可能喜欢

评论已经被关闭。

插入图片