关于安全🔐
关于应用的安全 有许多方面
认证 与鉴权
关于认证的,我们已经在 之前的文章中有提到 link 在这里 认证问题(基于JWT) 关于 鉴权 我们来探讨一下
一个非常简单的鉴权 实现,请参考我之前的文章,在哪里 实现了一个非常简易的 鉴权Guard 简易的RBAC
Claims-based authorization ,基于权限 的鉴权
这个仅仅是把 我们的1 中的操作 换了一下,1 中我们使用的是 基于角色的鉴权,比如什么角色 能有哪些权限能做什么 不能做什么,而 基于 权限 的鉴权,是平铺的分配权限 给用户 它看起来也许应该是这样的。虽然灵活,但是维护却并不简单
ts
@Post()
@RequirePermissions(Permission.CREATE_CAT)
create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
- 与成熟的 第三方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)
其它应用安全防御措施
- 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,
});
- 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);
- 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);
- 限速(限流)
当我们遇到这样的攻击 “暴力访问 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);