时间:2023-04-19 01:06:02 | 来源:网站运营
时间:2023-04-19 01:06:02 来源:网站运营
加固你的网站安全–nestjs文档更新:在最新的Nestjs文档中,作者将安全内容作为单独的章节撰写,以强调网络安全的重要性,依据官方文档的做法,可以有效提高网站安全性。$ npm install --save @nestjs/passport passport passport-local$ npm install --save-dev @types/passport-local
对于您选择的任何 Passport 策略,都需要 @nestjs/Passport 和 Passport 包。然后,需要安装特定策略的包(例如,passport-jwt 或 passport-local),它实现您正在构建的特定身份验证策略。此外,您还可以安装任何 Passport策略的类型定义,如上面的 @types/Passport-local 所示,它在编写 TypeScript 代码时提供了帮助。$ nest g module auth$ nest g service auth
当我们实现 AuthService 时,我们会发现在 UsersService 中封装用户操作是很有用的,所以现在让我们生成这个模块和服务:$ nest g module users$ nest g service users
替换这些生成文件的默认内容,如下所示。对于我们的示例应用程序,UsersService 只是在内存中维护一个硬编码的用户列表,以及一个根据用户名检索用户列表的 find 方法。在真正的应用程序中,这是您使用选择的库(例如 TypeORM、Sequelize、Mongoose等)构建用户模型和持久层。users/users.service.ts
import { Injectable } from '@nestjs/common';export type User = any;@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', }, ]; } async findOne(username: string): Promise<User | undefined> { return this.users.find(user => user.username === username); }}
在 UsersModule 中,惟一需要做的更改是将 UsersService 添加到 @Module 装饰器的 exports 数组中,以便提供给其他模块外部可见(我们很快将在 AuthService 中使用它)。users/users.module.ts
import { Module } from '@nestjs/common';import { UsersService } from './users.service';@Module({ providers: [UsersService], exports: [UsersService],})export class UsersModule {}
我们的 AuthService 的任务是检索用户并验证密码。为此,我们创建了 validateUser() 方法。在下面的代码中,我们使用 ES6 扩展操作符从 user 对象中提取 password 属性,然后再返回它。稍后,我们将从 Passport 本地策略中调用 validateUser() 方法。auth/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; }}
当然,在实际的应用程序中,您不会以纯文本形式存储密码。 取而代之的是使用带有加密单向哈希算法的 bcrypt 之类的库。使用这种方法,您只需存储散列密码,然后将存储的密码与输入密码的散列版本进行比较,这样就不会以纯文本的形式存储或暴露用户密码。为了保持我们的示例应用程序的简单性,我们违反了这个绝对命令并使用纯文本。不要在真正的应用程序中这样做!
现在,我们更新 AuthModule 来导入 UsersModule 。
auth/auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { UsersModule } from '../users/users.module';@Module({ imports: [UsersModule], providers: [AuthService],})export class AuthModule {}
现在我们可以实现 Passport 本地身份验证策略。在auth文件夹中创建一个名为 local.strategy.ts 文件,并添加以下代码: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 用例中,没有配置选项,因此我们的构造函数只是调用 super() ,没有 options 对象。auth/auth.module.ts
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 {}
app.controller.ts
import { Controller, Request, Post, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Controller()export class AppController { @UseGuards(AuthGuard('local')) @Post('auth/login') async login(@Request() req) { return req.user; }}
对于 @UseGuard(AuthGuard('local')),我们使用的是一个 AuthGuard ,它是在我们扩展护照-本地策略时 @nestjs/passportautomatic 为我们准备的。我们来分析一下。我们的 Passport 本地策略默认名为"local" 。我们在 @UseGuards() 装饰器中引用这个名称,以便将它与护照本地包提供的代码关联起来。这用于消除在应用程序中有多个 Passport 策略时调用哪个策略的歧义(每个策略可能提供一个特定于策略的 AuthGuard )。虽然到目前为止我们只有一个这样的策略,但我们很快就会添加第二个,所以这是消除歧义所需要的。$ # POST to /auth/login$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"$ # result -> {"userId":1,"username":"john"}
如果上述内容可以正常工作,可以通过直接将策略名称传递给AuthGuard()来引入代码库中的魔术字符串。作为替代,我们推荐创建自己的类,如下所示:auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Injectable()export class LocalAuthGuard extends AuthGuard('local') {}@UseGuards(LocalAuthGuard)@Post('auth/login')async login(@Request() req) { return req.user;}
$ npm install @nestjs/jwt passport-jwt$ npm install @types/passport-jwt --save-dev
@nest/jwt 包是一个实用程序包,可以帮助 jwt 操作。passport-jwt 包是实现 JWT 策略的 Passport包,@types/passport-jwt 提供 TypeScript 类型定义。auth/auth.service.ts
import { Injectable } from '@nestjs/common';import { UsersService } from '../users/users.service';import { JwtService } from '@nestjs/jwt';@Injectable()export class AuthService { constructor( private readonly usersService: UsersService, private readonly jwtService: JwtService ) {} 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; } 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中。auth/constants.ts
export const jwtConstants = { secret: 'secretKey',};
我们将使用它在 JWT 签名和验证步骤之间共享密钥。auth/auth.module.tsJSimport { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { LocalStrategy } from './local.strategy';import { UsersModule } from '../users/users.module';import { PassportModule } from '@nestjs/passport';import { JwtModule } from '@nestjs/jwt';import { jwtConstants } from './constants';@Module({ imports: [ UsersModule, PassportModule, JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }), ], providers: [AuthService, LocalStrategy], exports: [AuthService],})export class AuthModule {}
我们使用 register() 配置 JwtModule ,并传入一个配置对象。有关 Nest JwtModule 的更多信息请参见此处,有关可用配置选项的更多信息请参见此处。app.controller.ts
import { Controller, Request, Post, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';import { AuthService } from './auth/auth.service';@Controller()export class AppController { constructor(private readonly authService: AuthService) {} @UseGuards(AuthGuard('local')) @Post('auth/login') async login(@Request() req) { return this.authService.login(req.user); }}
让我们继续使用 cURL 测试我们的路由。您可以使用 UsersService 中硬编码的任何用户对象进行测试。$ # POST to /auth/login$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}$ # Note: above JWT truncated
auth/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 ,我们遵循了前面描述的所有 Passport 策略的相同配方。这个策略需要一些初始化,因此我们通过在 super() 调用中传递一个 options 对象来实现。您可以在这里阅读关于可用选项的更多信息。在我们的例子中,这些选项是:auth/auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { LocalStrategy } from './local.strategy';import { JwtStrategy } from './jwt.strategy';import { UsersModule } from '../users/users.module';import { PassportModule } from '@nestjs/passport';import { JwtModule } from '@nestjs/jwt';import { jwtConstants } from './constants';@Module({ imports: [ UsersModule, PassportModule, JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }), ], providers: [AuthService, LocalStrategy, JwtStrategy], exports: [AuthService],})export class AuthModule {}
通过导入 JWT 签名时使用的相同密钥,我们可以确保 Passport 执行的验证阶段和 AuthService 执行的签名阶段使用公共密钥。app.controller.ts
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';import { AuthService } from './auth/auth.service';@Controller()export class AppController { constructor(private readonly authService: AuthService) {} @UseGuards(AuthGuard('local')) @Post('auth/login') async login(@Request() req) { return this.authService.login(req.user); } @UseGuards(AuthGuard('jwt')) @Get('profile') getProfile(@Request() req) { return req.user; }}
同样,我们将应用在配置 passport-jwt 模块时 @nestjs/passport 模块自动为我们提供的 AuthGuard 。这个保护由它的默认名称 jwt 引用。当我们请求GET /profile 路由时,保护程序将自动调用我们的 passport-jwt 自定义配置逻辑,验证 JWT ,并将用户属性分配给请求对象。$ # GET /profile$ curl http://localhost:3000/profile$ # result -> {"statusCode":401,"error":"Unauthorized"}$ # POST /auth/login$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }$ # GET /profile using access_token returned from previous step as bearer code$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."$ # result -> {"userId":1,"username":"john"}
注意,在 AuthModule 中,我们将 JWT 配置为 60 秒过期。这个过期时间可能太短了,而处理令牌过期和刷新的细节超出了本文的范围。然而,我们选择它来展示JWT 的一个重要品质和 jwt 护照战略。如果您在验证之后等待 60 秒再尝试 GET /profile 请求,您将收到 401 未授权响应。这是因为 Passport 会自动检查 JWT 的过期时间,从而省去了在应用程序中这样做的麻烦。auth.module.ts
import { Module } from '@nestjs/common';import { AuthService } from './auth.service';import { LocalStrategy } from './local.strategy';import { UsersModule } from '../users/users.module';import { PassportModule } from '@nestjs/passport';import { JwtModule } from '@nestjs/jwt';import { jwtConstants } from './constants';import { JwtStrategy } from './jwt.strategy';@Module({ imports: [ PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }), UsersModule ], providers: [AuthService, LocalStrategy, JwtStrategy], exports: [AuthService],})export class AuthModule {}
constructor(private moduleRef: ModuleRef){ super({ passReqToCallback:true; })}
注意: ModuleRef 类需要从@nestjs/core中导入。
要保证passReqToCallback属性和上述示例中一样配置为true。
在下一步中,请求的实例将被用于获取一个当前上下文标识,而不是生成一个新的(更多关于请求上下文的内容见这里)。
现在,在LocalStrategy类的validate()方法中,使用ContextIdFactory类中的getByRequest()方法来创建一个基于请求对向的上下文id,并将其传递给resolve()调用:
async validate( request: Request, username: string, password: string,) { const contextId = ContextIdFactory.getByRequest(request); // "AuthService" is a request-scoped provider const authService = await this.moduleRef.resolve(AuthService, contextId); ...}
在上述例子中,resolve()方法会异步返回AuthService提供者的请求范围实例(我们假设AuthService被标示为一个请求范围提供者)。import { ExecutionContext, Injectable, UnauthorizedException,} from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Injectable()export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { // 在这里添加自定义的认证逻辑 // 例如调用 super.logIn(request) 来建立一个session return super.canActivate(context); } handleRequest(err, user, info) { // 可以抛出一个基于info或者err参数的异常 if (err || !user) { throw err || new UnauthorizedException(); } return user; }}
PassportModule.register({ session: true });
您还可以在策略的构造函数中传递一个 options 对象来配置它们。至于本地策略,你可以通过例如:constructor(private readonly authService: AuthService) { super({ usernameField: 'email', passwordField: 'password', });}
看看Passport Website官方文档吧。export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
然后,通过一个像 @AuthGuard('myjwt') 这样的装饰器来引用它。@Injectable()export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; }}
要使用上述结构,请确保在 GraphQL 模块设置中将 request (req)对象作为上下文字的一部分传递:GraphQLModule.forRoot({ context: ({ req }) => ({ req }),});
要在 graphql 解析器中获得当前经过身份验证的用户,可以定义一个@CurrentUser()装饰器:import { createParamDecorator, ExecutionContext } from '@nestjs/common';import { GqlExecutionContext } from '@nestjs/graphql';export const CurrentUser = createParamDecorator( (data: unknown, context: ExecutionContext) => { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req.user; },);
要在解析器中使用上述装饰器,请确保将其作为查询的参数:@Query(returns => User)@UseGuards(GqlAuthGuard)whoAmI(@CurrentUser() user: User) { return this.userService.findById(user.id);}
role.enum.ts
export enum Role { User = 'user', Admin = 'admin',}
在更复杂的系统中,角色信息可能会存储在数据库里,或者从一个外部认证提供者那里获取。
然后,创建一个@Roles()的装饰器,该装饰器允许某些角色拥有获取特定资源访问权。
roles.decorator.ts
import { SetMetadata } from '@nestjs/common';import { Role } from '../enums/role.enum';export const ROLES_KEY = 'roles';export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
现在可以将@Roles()装饰器应用于任何路径处理程序。cats.controller.ts
@Post()@Roles(Role.Admin)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
最后,我们创建一个RolesGuard类来比较当前用户拥有的角色和当前路径需要的角色。为了获取路径的角色(自定义元数据),我们使用Reflector辅助类,这是个@nestjs/core提供的一个开箱即用的类。roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); }}
参见应用上下文章节的反射与元数据部分,了解在上下文敏感的环境中使用Reflector的细节。
该例子被称为“基础的”是因为我们仅仅在路径处理层面检查了用户权限。在实际项目中,你可能有包含不同操作的终端/处理程序,它们各自需要不同的权限组合。在这种情况下,你可能要在你的业务逻辑中提供一个机制来检查角色,这在一定程度上会变得难以维护,因为缺乏一个集中的地方来关联不同的操作与权限。
在这个例子中,我们假设request.user包含用户实例以及允许的角色(在roles属性中)。在你的应用中,需要将其与你的认证守卫关联起来,参见认证。
要确保该示例可以工作,你的User类看上去应该像这样:
class User { // ...other properties roles: Role[];}
最后,在控制层或者全局注册RolesGuard。providers: [ { provide: APP_GUARD, useClass: RolesGuard, },],
当一个没有有效权限的用户访问一个终端时,Nest自动返回以下响应:{ "statusCode": 403, "message": "Forbidden resource", "error": "Forbidden"}
如果你想返回一个不同的错误响应,需要抛出特定异常来代替返回一个布尔值。
cats.controller.ts
@Post()@RequirePermissions(Permission.CREATE_CAT)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
在这个例子中,许可(和RBAC部分的角色类似)是一个TypeScript的枚举,它包含了系统中所有的许可。
$ npm i @casl/ability
在本例中,我们选择CASL,但也可以根据项目需要选择其他类似库例如accesscontrol或者acl。
安装完成后,为了说明CASL的机制,我们定义了两个类实体,User和Article。
class User { id: number; isAdmin: boolean;}
User类包含两个属性,id是用户的唯一标识,isAdmin代表用户是否有管理员权限。class Article { id: number; isPublished: boolean; authorId: string;}
Article类包含三个属性,分别是id、isPublished和authorId,id是文章的唯一标识,isPublished代表文章是否发布,authorId代表发表该文章的用户id。export enum Action { Manage = 'manage', Create = 'create', Read = 'read', Update = 'update', Delete = 'delete',}
manage是CASL的关键词,代表任何操作。
要封装CASL库,需要创建CaslModule和CaslAbilityFactory。
$ nest g module casl$ nest g class casl/casl-ability.factory
创建完成后,在CaslAbilityFactory中定义creteForUser()方法。该方法将为用户创建Ability对象。type Subjects = typeof Article | typeof User | Article | User | 'all';export type AppAbility = Ability<[Action, Subjects]>;@Injectable()export class CaslAbilityFactory { createForUser(user: User) { const { can, cannot, build } = new AbilityBuilder< Ability<[Action, Subjects]> >(Ability as AbilityClass<AppAbility>); if (user.isAdmin) { can(Action.Manage, 'all'); // read-write access to everything } else { can(Action.Read, 'all'); // read-only access to everything } can(Action.Update, Article, { authorId: user.id }); cannot(Action.Delete, Article, { isPublished: true }); return build(); }}
all是CASL的关键词,代表任何对象。
Ability,AbilityBuilder,和AbilityClass从@casl/ability包中导入。
在上述例子中,我们使用AbilityBuilder创建了Ability实例,如你所见,can和cannot接受同样的参数,但代表不同含义,can允许对一个对象执行操作而cannot禁止操作,它们各能接受4个参数,参见CASL文档。
最后,将CaslAbilityFactory添加到提供者中,并在CaslModule模块中导出。
import { Module } from '@nestjs/common';import { CaslAbilityFactory } from './casl-ability.factory';@Module({ providers: [CaslAbilityFactory], exports: [CaslAbilityFactory],})export class CaslModule {}
现在,只要将CaslModule引入对象的上下文中,就可以将CaslAbilityFactory注入到任何标准类中。constructor(private caslAbilityFactory: CaslAbilityFactory) {}
在类中使用如下:const ability = this.caslAbilityFactory.createForUser(user);if (ability.can(Action.Read, 'all')) { // "user" has read access to everything}
Ability类更多细节参见CASL 文档。
例如,一个非管理员用户,应该可以阅读文章,但不允许创建一篇新文章或者删除一篇已有文章。
const user = new User();user.isAdmin = false;const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Read, Article); // trueability.can(Action.Delete, Article); // falseability.can(Action.Create, Article); // false
虽然Ability和AlbilityBuilder类都提供can和cannot方法,但其目的并不一样,接受的参数也略有不同。
依照我们的需求,一个用户应该能更新自己的文章。
const user = new User();user.id = 1;const article = new Article();article.authorId = user.id;const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Update, article); // truearticle.authorId = 2;ability.can(Action.Update, article); // false
如你所见,Ability实例允许我们通过一种可读的方式检查许可。AbilityBuilder采用类似的方式允许我们定义许可(并定义不同条件)。查看官方文档了解更多示例。import { AppAbility } from '../casl/casl-ability.factory';interface IPolicyHandler { handle(ability: AppAbility): boolean;}type PolicyHandlerCallback = (ability: AppAbility) => boolean;export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
如上所述,我们提供了两个可能的定义策略处理程序的方式,一个对象(实现了IPolicyHandle接口的类的实例)和一个函数(满足PolicyHandlerCallback类型)。export const CHECK_POLICIES_KEY = 'check_policy';export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers);
现在创建一个PoliciesGuard,它将解析并执行所有和路径相关的策略程序。@Injectable()export class PoliciesGuard implements CanActivate { constructor( private reflector: Reflector, private caslAbilityFactory: CaslAbilityFactory, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const policyHandlers = this.reflector.get<PolicyHandler[]>( CHECK_POLICIES_KEY, context.getHandler(), ) || []; const { user } = context.switchToHttp().getRequest(); const ability = this.caslAbilityFactory.createForUser(user); return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability), ); } private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) { if (typeof handler === 'function') { return handler(ability); } return handler.handle(ability); }}
在本例中,我们假设request.user包含了用户实例。在你的应用中,可能将其与你自定义的认证守卫关联。参见认证章节。
我们分析一下这个例子。policyHandlers是一个通过@CheckPolicies()装饰器传递给方法的数组,接下来,我们用CaslAbilityFactory#create方法创建Ability对象,允许我们确定一个用户是否拥有足够的许可去执行特定行为。我们将这个对象传递给一个可能是函数或者实现了IPolicyHandler类的实例的策略处理程序,暴露出handle()方法并返回一个布尔量。最后,我们使用Array#every方法来确保所有处理程序返回true。
为了测试这个守卫,我们绑定任意路径处理程序,并且注册一个行内的策略处理程序(函数实现),如下:
@Get()@UseGuards(PoliciesGuard)@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))findAll() { return this.articlesService.findAll();}
我们也可以定义一个实现了IPolicyHandler的类来代替函数。export class ReadArticlePolicyHandler implements IPolicyHandler { handle(ability: AppAbility) { return ability.can(Action.Read, Article); }}
并这样使用。@Get()@UseGuards(PoliciesGuard)@CheckPolicies(new ReadArticlePolicyHandler())findAll() { return this.articlesService.findAll();}
由于我们必须使用 new关键词来实例化一个策略处理函数,CreateArticlePolicyHandler类不能使用注入依赖。这在ModuleRef#get方法中强调过,参见这里)。基本上,要替代通过@CheckPolicies()装饰器注册函数和实例,你需要允许传递一个Type<IPolicyHandler>,然后在守卫中使用一个类型引用(moduleRef.get(YOUR_HANDLER_TYPE)获取实例,或者使用ModuleRef#create方法进行动态实例化。
import { createCipheriv, randomBytes } from 'crypto';import { promisify } from 'util';const iv = randomBytes(16);const password = 'Password used to generate key';// The key length is dependent on the algorithm.// In this case for aes256, it is 32 bytes.const key = (await promisify(scrypt)(password, 'salt', 32)) as Buffer;const cipher = createCipheriv('aes-256-ctr', key, iv);const textToEncrypt = 'Nest';const encryptedText = Buffer.concat([ cipher.update(textToEncrypt), cipher.final(),]);
接下来,解密encryptedText值。import { createDecipheriv } from 'crypto';const decipher = createDecipheriv('aes-256-ctr', key, iv);const decryptedText = Buffer.concat([ decipher.update(encryptedText), decipher.final(),]);
$ npm i bcrypt$ npm i -D @types/bcrypt
依赖安装后,可以使用哈希函数。import * as bcrypt from 'bcrypt';const saltOrRounds = 10;const password = 'random_password';const hash = await bcrypt.hash(password, saltOrRounds);
使用genSalt函数来生成哈希需要的盐。const salt = await bcrypt.genSalt();
使用compare函数来比较/检查密码。const isMatch = await bcrypt.compare(password, hash);
更多函数参见这里。要在全局使用Helmet,需要在调用app.use()之前或者可能调用app.use()函数之前注册。这是由平台底层机制中(EXpress或者Fastify)中间件/路径的定义决定的。如果在定义路径之后使用helmet或者cors中间件,其之前的路径将不会应用这些中间件,而仅在定义之后的路径中应用。
$ npm i --save helmet
安装完成后,将其应用为全局中间件。import * as helmet from 'helmet';// somewhere in your initialization fileapp.use(helmet());
如果在引入helmet时返回This expression is not callable错误。你可能需要将项目中tsconfig.json文件的allowSyntheticDefaultImports和esModuleInterop选项配置为true。在这种情况下,将引入声明修改为:import helmet from 'helmet'。
$ npm i --save fastify-helmet
fastify-helmet需要作为Fastify插件而不是中间件使用,例如,用app.register()调用。import * as helmet from 'fastify-helmet';// somewhere in your initialization fileapp.register(helmet);
在使用apollo-server-fastify和fastify-helmet时,在GraphQL应用中与CSP使用时可能出问题,需要如下配置CSP。
app.register(helmet, { contentSecurityPolicy: { directives: { defaultSrc: [`'self'`], styleSrc: [`'self'`, `'unsafe-inline'`, 'cdn.jsdelivr.net', 'fonts.googleapis.com'], fontSrc: [`'self'`, 'fonts.gstatic.com'], imgSrc: [`'self'`, 'data:', 'cdn.jsdelivr.net'], scriptSrc: [`'self'`, `https: 'unsafe-inline'`, `cdn.jsdelivr.net`], }, },});// If you are not going to use CSP at all, you can use this:app.register(helmet, { contentSecurityPolicy: false,});
const app = await NestFactory.create(ApplicationModule);app.enableCors();await app.listen(3000);
enableCors()方法使用一个可选的配置对象参数。该对象的可用属性在其官方CORS文档中有所描述。const app = await NestFactory.create(ApplicationModule, { cors: true });await app.listen(3000);
$ npm i --save csurf
正如 csurf 中间件页面所解释的,csurf 模块需要首先初始化会话中间件或 cookie 解析器。有关进一步说明,请参阅该文档。
安装完成后,将其应用为全局中间件。
import * as csurf from 'csurf';// somewhere in your initialization fileapp.use(csurf());
$ npm i --save fastify-csrf
安装完成后,将其注册为fastify-csrf插件。import fastifyCsrf from 'fastify-csrf';// somewhere in your initialization fileapp.register(fastifyCsrf);
$ npm i --save express-rate-limit
安装完成后,将其应用为全局中间件。import * as rateLimit from 'express-rate-limit';// somewhere in your initialization fileapp.use( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs }),);
如果在服务器和以太网之间存在负载均衡或者反向代理,Express可能需要配置为信任proxy设置的头文件,从而保证最终用户得到正确的IP地址。要如此,首先使用NestExpressApplication平台接口来创建你的app实例,然后配置trust proxy设置。const app = await NestFactory.create<NestExpressApplication>(AppModule);// see https://expressjs.com/en/guide/behind-proxies.htmlapp.set('trust proxy', 1);
如果使用 FastifyAdapter,用 fastify-rate-limit替换。
关键词:更新,安全