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"}