身份验证是大多数现有应用程序的重要组成部分,有许多不同的方法,策略来实现用户的登录认证,nodejs 中认证用的比较多的是 passport, 该文章就来介绍一下采用 Nest 集成 passport 实现一个简单的 jwt 认证程序 (依然采用之前的 nest 项目).
# 安装 passport 及其他必要的库
$ yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt | |
$ yarn add -D @types/passport-local @types/passport-jwt |
# 创建认证模块与用户模块
$ nest g module auth | |
$ nest g service auth | |
$ nest g module users | |
$ nest g service users |
users.service.ts
用户这里为了简单处理就采用了固定用户的策略,且密码都是明文的,在实际项目中用户一般在数据库中存储,密码采用加密存储.
用户 service 实现一个 findOne
方法,根据用户名查找用户.
import { Injectable } from '@nestjs/common'; | |
export type User = { | |
userId:number; | |
username:string; | |
password:string; | |
}; | |
@Injectable() | |
export class UsersService { | |
private readonly users: User[]; | |
constructor() { | |
this.users = [ | |
{ | |
userId: 1, | |
username: 'john', | |
password: 'changeme', | |
}, | |
{ | |
userId: 2, | |
username: 'chris', | |
password: 'secret', | |
}, | |
{ | |
userId: 3, | |
username: 'maria', | |
password: 'guess', | |
}, | |
{ | |
userId: 4, | |
username: 'admin', | |
password: '111111', | |
}, | |
]; | |
} | |
async findOne(username: string): Promise<User | undefined> { | |
return this.users.find(user => user.username === username); | |
} | |
} |
# 编写 passport 策略
在 auth 模块下我们创建 local.strategy.ts
和 jwt.strategy.ts
2 个文件,即 passport 的策略类
// src/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 readonly 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-local
策略类需要实现一个 validate
接口,该接口接收 2 个参数 (username 和 password), 返回的值则会被加载到 Request 对象上
以下是 auth.service.ts
// auth.service.ts | |
import { Injectable } from '@nestjs/common'; | |
import { UsersService } from '../users/users.service'; | |
@Injectable() | |
export class AuthService { | |
constructor( | |
private readonly usersService: UsersService | |
) {} | |
async validateUser(username: string, pass: string): Promise<any> { | |
const user = await this.usersService.findOne(username); | |
if (user && user.password === pass) { | |
const { password, ...result } = user; | |
return result; | |
} | |
return null; | |
} | |
} |
通过 username 和 password 来找到相应的用户,将用户的信息返回
我们这下就可以测试登录了
# 测试登录
现在在 app.controller.ts
中加入一个控制器,用以测试登录
@UseGuards(AuthGuard('local')) | |
@Post('auth/login') | |
async login(@Request() req) { | |
return req.user; | |
} |
AuthGuard
来自 @nestjs/passport
.
使用 CURL 测试该接口 /api/auth/login
$ curl --location --request POST 'http://localhost:9000/api/auth/login' \ | |
--header 'Content-Type: application/json' \ | |
--data-raw '{"username":"admin","password":"111111"}' | |
{"userId":4,"username":"admin"} |
通过用户名与密码成功获取到了用户的其他信息.
# 实现 JWT 登录与认证
至此我们的工作只完成了一半,只实现了一个简单的登录,还没有认证.
在以前,登录之后需要将用户的信息存储到 session,cookie 或数据库中,返回用户一个令牌,用户之后访问则携带令牌进行认证,现在 JWT 策略已逐渐替代 session,cookie 变为更主流的认证方式.
接下来我们实现一个 JWT 的登录与认证服务.
# 添加 jwt 认证策略
首先添加 passport 的 jwt 认证策略
// src/auth/jwt.strategy.ts | |
import { ExtractJwt, Strategy } from 'passport-jwt'; | |
import { PassportStrategy } from '@nestjs/passport'; | |
import { Injectable } from '@nestjs/common'; | |
@Injectable() | |
export class JwtStrategy extends PassportStrategy(Strategy) { | |
constructor() { | |
super({ | |
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),// 使用 header 中 bearer token 的方式 | |
ignoreExpiration: false, | |
secretOrKey: "secretKey",// 这里的 key 需要小新保管,通常采用配置文件中引入 | |
}); | |
} | |
async validate(payload: any) { | |
return { userId: payload.sub, username: payload.username }; | |
} | |
} |
同样的, validate
方法的返回值会加载进 Request 对象上.
auth.module.ts
中需要引入 passport 并将 passport 策略作为 providers
引入.
import { Module } from '@nestjs/common'; | |
import { AuthService } from './auth.service'; | |
import { UsersModule } from '../users/users.module'; | |
import { PassportModule } from '@nestjs/passport'; | |
import { LocalStrategy } from './local.strategy'; | |
import { JwtModule } from '@nestjs/jwt'; | |
import { jwtConstants } from './constants'; | |
import { JwtStrategy } from './jwt.strategy'; | |
@Module({ | |
imports: [ | |
UsersModule, | |
PassportModule.register({ defaultStrategy: 'jwt' }), | |
JwtModule.register({ | |
secret: jwtConstants.secret, | |
signOptions: { expiresIn: '3600s' }, | |
}), | |
], | |
providers: [AuthService, LocalStrategy, JwtStrategy], | |
exports: [AuthService], | |
}) | |
export class AuthModule {} |
# 添加 login
方法
修改 auth.service.ts
添加 login 方法,使用 jwt 生成 access_token
constructor( | |
private readonly usersService: UsersService, | |
private readonly jwtService: JwtService | |
) {} | |
async login(user: any) { | |
const payload = { username: user.username, sub: user.userId }; | |
return { | |
access_token: this.jwtService.sign(payload), | |
}; | |
} |
JwtService
来自 @nestjs/jwt
# 修改登录接口
@UseGuards(AuthGuard('local')) | |
@Post('auth/login') | |
async login(@Request() req) { | |
return this.authService.login(req.user); | |
// return req.user; | |
} |
此时我们调用登录接口时则返回给我们 access_token
$ curl --location --request POST 'http://localhost:9000/api/auth/login' \ | |
--header 'Content-Type: application/json' \ | |
--data-raw '{"username":"admin","password":"111111"}' | |
{"access_token":"xxxxxxxxxxx"} |
# 实现 jwt 认证
此时我们就可以采用 jwt 来进行认证了,在 app.controller.ts
中添加我们的接口,用以获得用户信息
@UseGuards(AuthGuard('jwt')) | |
@Get('profile') | |
getProfile(@Request() req) { | |
return req.user; | |
} |
现在访问 /api/auth/profile
接口需要带着之前获取到的 access_token
$ curl --location --request GET 'http://localhost:9000/api/profile' \ | |
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjo0LCJpYXQiOjE2NjM1Njk4MDIsImV4cCI6MTY2MzU3MzQwMn0.GwFm4YpmzfJljsQPPSB3YW6Ha3hQ3TipPJbkVExPF30' | |
{"userId":4,"username":"admin"} |
至此,简单的 jwt 登录与认证已经实现了.