Skip to content

关于安全🔐

关于应用的安全 有许多方面

认证 与鉴权

关于认证的,我们已经在 之前的文章中有提到 link 在这里 认证问题(基于JWT) 关于 鉴权 我们来探讨一下

  1. 一个非常简单的鉴权 实现,请参考我之前的文章,在哪里 实现了一个非常简易的 鉴权Guard 简易的RBAC

  2. Claims-based authorization ,基于权限 的鉴权

这个仅仅是把 我们的1 中的操作 换了一下,1 中我们使用的是 基于角色的鉴权,比如什么角色 能有哪些权限能做什么 不能做什么,而 基于 权限 的鉴权,是平铺的分配权限 给用户 它看起来也许应该是这样的。虽然灵活,但是维护却并不简单

ts
@Post()
@RequirePermissions(Permission.CREATE_CAT)
create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  1. 与成熟的 第三方package 集成 比如 CASL (当然你可以选择使用别的)

注意:如果用于生产 请一定一定要读一下 CASL 的全文文档 CASL

shell
yarn add  @casl/ability

具体细节(基础使用

ts
// 先按照 CASL 文档 实现 Factory
export enum Action {
  Manage = 'manage', //属于 CASL关键字 表示什么操作都可以
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}
export class Article {
  id: number;
  isPublished: boolean;
  authorId: number;

  constructor(article: Article) {
    Object.assign(this, article);
  }
}
export class User {
  id: number;
  isAdmin: boolean;

  constructor(use: User) {
    Object.assign(this, use);
  }
}

import { Injectable } from '@nestjs/common';
import { User } from './entity/user.entiry';
import {
  AbilityBuilder,
  AbilityClass,
  InferSubjects,
  Ability,
  ExtractSubjectType,
} from '@casl/ability';
import { Article } from './entity/article.entiry';
import { Action } from './type';

type Subjects = InferSubjects<typeof Article | typeof User> | 'all';
// all 为 CASL 关键子,表示任何对象

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({
      // Read https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types for details
      detectSubjectType: (item) =>
        item.constructor as ExtractSubjectType<Subjects>,
    });
  }
}


// 如果要全局使用 记得在 module export 出去
import { CaslAbilityFactory } from './casAbility.factory';

@Module({
  imports: [],
  controllers: [CASLController],
  providers: [CaslAbilityFactory],
  exports: [CaslAbilityFactory],
})
export class CASLModule {}

// 基础使用(API的方式调用
@Controller('casl')
export class CASLController {
  constructor(private caslAbility: CaslAbilityFactory) {}

  // 简单的集成到Nest中直接使用 API
  @Get('t1')
  async t1() {
    const use = new User({
      id: 0,
      isAdmin: false,
    });
    const ability = this.caslAbility.createForUser(use);
    if (ability.can(Action.Read, 'all')) {
      log('2');
    }
  }
  +++
}

如果你希望 定义成 Decorator + Guard 请 看下面的实现

ts
// ~ type/index.ts
import { AppAbility } from '../casAbility.factory';

export enum Action {
  Manage = 'manage', //属于 CASL关键字 表示什么操作都可以
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

export interface IPolicyHandler {
  handle(ability: AppAbility): boolean;
}

type PolicyHandlerCallback = (ability: AppAbility) => boolean;

// 这个处理函数同时支持 使用 callback / Class 来处理 权限
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;


// ~ decorator/checkPolicies.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { PolicyHandler } from '../type';

export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
  SetMetadata(CHECK_POLICIES_KEY, handlers);

// ~ gourd/policies.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AppAbility, CaslAbilityFactory } from '../casAbility.factory';
import { PolicyHandler } from '../type';
import { CHECK_POLICIES_KEY } from '../decorator/CheckPolicies.decorator';
import { User } from '../entity/user.entiry';

// 实现一个自定义的 gourd( 很简单 如果你不懂,请认真 看我的文章https://juejin.cn/post/7230012114600886309#heading-6 )
@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(),
      ) || [];

    // mock
    const user = new User({
      id: 0,
      isAdmin: false,
    });
    // 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);
  }
}

// ~ casl.controller.ts
++++
class ReadArticlePolicyHandler implements IPolicyHandler {
  handle(ability: AppAbility) {
    return ability.can(Action.Read, Article);
  }
}

++++
  // 但是好像不够优雅?我们能不能做成 1 例子中这样 RBAC 装饰器呢?当然可以 可以结合 自定义装饰器 和 自定义Guard实现
  @Get('t2')
  @UseGuards(PoliciesGuard)
  @CheckPolicies((ability: AppAbility) => ability.can(Action.Create, Article))
  t2() {
    log('验证成功');
    return 0;
  }

  // 如果用类请使用 (不推荐)
  @Get('t3')
  @UseGuards(PoliciesGuard)
  @CheckPolicies(new ReadArticlePolicyHandler()) // 注意这里必须New 他不能 自动注入,如果要做 你需要实现 ModuleRef
  // 文档在这里 https://docs.nestjs.com/fundamentals/module-ref
  t3() {
    log('验证成功');
    return 0;
  }

加密🔐

在nodejs上最富有盛名 的就是 crypto 了这里不详细铺开说明,因为之前的 做认证的时候也有提及 ,不详细展开说明 link 在这里 认证问题(基于JWT)

其它应用安全防御措施

  1. Helmet

简单的来说这是一个package 功过合理的设置 header 信息来避免一些 来自外部的攻击 ,文档详见 helmetjs

ts
yarn add helmet
// 使用很简单 注意要在 app.use加到最“顶上” 也就是其它use中间价使用之前
import helmet from 'helmet';
app.use(helmet(
  {
    // 这里可以配置相关的参数 请参考 helmetjs 文档
  }
));

// 若说 fastify 那么请替换对应的包

yarn add fastify-helmet
import * as helmet from 'fastify-helmet';
// somewhere in your initialization file
app.register(helmet);
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,
});
  1. CORS

跨域问题 一键处理 还是非常不错的

ts
// 开启方式1
const app = await NestFactory.create(AppModule, { cors:true });

// 开启方式2
// app.enableCors({
//   // 需要传递一些参数  CORS 配置对象 或 回调函数 
//   // 文档这里 https://github.com/expressjs/cors#configuring-cors-asynchronously
// });

await app.listen(3000);
  1. CSRF

主要是处理 XSF 工具等 跨站站点请求伪造 之类的攻击

ts
// 直接使用 csurf 中间件
$ yarn add csurf
import * as csurf from 'csurf';
app.use(csurf());

// fastify 请参考下面
$ yarn add fastify-csrf
import fastifyCsrf from 'fastify-csrf';
app.register(fastifyCsrf);
  1. 限速(限流)

当我们遇到这样的攻击 “暴力访问 1s 1000w 次” ,不过大部分情况下 我们会把这个功能 交给Nginx这类的网关去做 而不是写在程序里...

ts
yarn add express-rate-limit
import rateLimit from 'express-rate-limit';
app.use(
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
  })
);

// 注意如果是 fastify 请用  fastify-rate-limit 替换

// 还需要注意 如果 你的程序 上层有反向代理 比如Nginx 请确保 程序能识别到 流量进来的IP
// Express 可能需要配置为信任 proxy 设置的头文件,从而保证最终用户得到正确的 IP 地址
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// see https://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', 1);