Mahiru's Humble Abode       博  客   时 间 线   归  档   关 于 与 友 链



NestJS 认证与认证初步
Published on Fri May 27 2022 15:30:00 GMT+0000

Local 认证

首先,准备包 Passport

1
2
yarn add @nestjs/passport passport passport-local
yarn add @types/passport-local

创建 Auth 模块和 User 模块

UserService需要做什么?

提供用户数据。

User模块的创建没有太多标准,只需要是一个单例,提供用户数据即可。在实际应用中应该再这里构建用户模型和持久层。

其中,User模块需要提供用户数据并提供方法来返回用户数据,并且需要在模块中导出。

1
2
3
4
5
6
7
8
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

AuthService需要做什么?

提供认证的具体实现,提供JWT等……

提供认证实现的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

在实际的应用程序中,我们不会以纯文本形式存储密码。 取而代之的是使用带有加密单向哈希算法的 bcrypt 之类的库。

Auth 模块需要导入用户模块和PassportModule,用于认证。

Auth 模块的Provider 是 AuthService 和 LocalStrategy(用于提供认证策略)。

1
2
3
4
5
6
7
8
9
10
11
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';

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

实现策略

local.strategy.ts 中实现策略,策略的具体实现依赖于 AuthService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

local 认证

别忘了导入 AuthModule !

在Controller 中引入 @UseGuards

1
2
3
4
5
6
7
8
9
10
11
12
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('testguard')
export class TestguardController {
// 在这里引入需要的认证守卫!
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}

也可以将要使用的AuthGuard 写成一个类

1
2
3
4
5
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

引入方式变为 @UseGuards(LocalAuthGuard)

JWT 方式

JWT 生成

引入包

1
2
yarn add @nestjs/jwt passport-jwt
yarn add @types/passport-jwt

在 AuthService 中加上 JWT

1
2
3
4
5
6
7
8
9
10
11
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService, // 别忘了在这里实例化 JwtService
) {}

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

我们使用 @nestjs/jwt 库,该库提供了一个 sign() 函数,用于从用户对象属性的子集生成 jwt,然后以简单对象的形式返回一个 access_token 属性。注意:我们选择 sub 的属性名来保持我们的 userId 值与JWT 标准一致。不要忘记将 JwtService 提供者注入到 AuthService中。

我们使用 register() 配置 JwtModule ,并传入一个配置对象。

修改 AuthService

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}

别忘了导出 AuthService 这样使用这个模块的Module 才能获取 AuthService

更新 Controller

因为 Passport 定义的 所有策略 都是将validate() 方法执行的结果作为 user 属性存储在当前 HTTP Request 对象 上,所以我们可以得到 usernameuserId

1
2
3
4
5
6
7
8
9
@Controller('testguard')
export class TestguardController {
constructor(private readonly authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}

这样,我们就能得到 AccessToken

POST localhost:3000/testguard/auth/login

x-www-form-urlencoded:username=john&passwoed=changeme

返回:

1
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG4iLCJzdWIiOjEsImlhdCI6MTY1MzY0MDUyMiwiZXhwIjoxNjUzNjQwNTgyfQ.gvMROxXrL_ywMDNf2IbReCxgrh6_FQYh10M34A8JxwM"}

JWT 认证

编写 JWT 策略

auth/jwt.strategy.ts

对于 JWT 策略,Passport 首先验证 JWT 的签名并解码 JSON 。然后调用我们的 validate() 方法,该方法将解码后的 JSON 作为其单个参数传递。

所以,实际上我们的 validate 是拿到了解码后的 JSON,这个 payload 正是我们之前通过 sign 生成的。

所以我们只需要返回其内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 };
}
}

AuthModule 中添加新的 JwtStrategy 作为提供者,因为现在我们提供了 JWT 作为认证方式

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

然后,引入这个策略

jwt-auth.guard.ts

1
2
3
4
5
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

修改 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
import { LocalAuthGuard } from '../auth/local-auth.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('testguard')
export class TestguardController {
constructor(private readonly authService: AuthService) {}

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

@UseGuards(JwtAuthGuard)
@Get('info')
async getInfo(@Request() req) {
return req.user;
}
}

测试

在 Get 时加上 请求头

1
Authorization: Bearer [Access Token]

即可访问被 JwtAuthGuard 保护的数据。

总结

实现认证总是需要编写以下模块:

1、UserModule ,作为用户数据的提供者(提供账号密码等)

2、AuthModule,用于提供认证策略,实现认证服务。

UserModule 需要编写什么?

UserService

获取用户数据的逻辑,这个模块需要导出并由AuthService引入

AuthModule 需要编写什么?

AuthService

这个服务用于:具体实现local认证(因为密码可能散列),生成JWT(一般来说在生成JWT前已经由Local认证,并得到用户信息,这时候根据用户信息来生成JWT

这个服务需要导入:UserSercice(用于Local认证),JwtService(用于生成JWT

AuthModule

需要导入的:UserModule(用于让Service可以拿到用户数据)、PassportModule (我们实现认证的库)、JWTModule(用于生成JWT,并配置JWT参数)

这个模块的提供者:AuthService(提供认证具体实现)、LocalStrategyJwtStrategy(具体策略)

需要导出的:AuthService(为什么?因为我们需要这个服务来生成JWT,Controller 需要这个服务来获得 JWT并响应。

编写策略

所有的策略都是继承 PassportStrategy(Strategy),实现 validate 方法。

validate 方法总是接受来自 req 的属性,返回的属性也会附加到 req 。一旦认证结束后,我们就可以从 req对象中拿到认证的结果。

策略的核心在于在发生错误的时候抛出 UnauthorizedException() 来告知无法认证,在认证通过的时候往 req对象上附加一些属性(这些属性可以用于返回,也可以用于生成 JWT)。

策略会调用 AuthService ,因为那里有认证的具体实现。

收尾

可以导出 jwt-auth.guard.tslocal-auth.guard.ts 简化在Controller 使用 @UseGuards注解时要编写的代码。