Redon

一心的小屋

NESTJS: BUILDING THE BASE

发布于 # Nest # TypeScript

原文来自:Rashintha Maduneth,这是中文译文。

NestJS 是一个用于构建应用程序服务器端的框架。这篇文章介绍如何构建具有基础安全功能的应用程序。

开始项目

首先,使用以下命令安装 NestJS CLI。可能需要 root 权限才能执行此命令。

 npm i -g @nestjs/cli

然后使用以下命令创建一个新项目。

nest new project_name
cd project_name

登录验证

npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local

使用以下命令生成带有服务的 AuthUser 模块。

nest g module auth
nest g service auth
nest g module users
nest g service users

现在让我们使用以下命令生成一个代表用户的 interface

nest g interface interfaces/user

添加以下代码

export interface UserData {
  id: number;
  username: string;
  password: string;
}

现在打开 users/users.service.ts 文件并输入以下代码。在这里,我创建了两个示例用户和一个用于查询用户的函数。

import { Injectable } from "@nestjs/common";
import { UserData } from "src/interfaces/user.interface";

@Injectable()
export class UsersService {
  private readonly users: Array<UserData> = [
    {
      id: 1,
      username: "admin",
      password: "1234",
    },
    {
      id: 2,
      username: "user",
      password: "4567",
    },
  ];

  async find(username: string): Promise<UserData | undefined> {
    return this.users.find((user) => user.username === username);
  }
}

导出 users/users.module.ts 文件中的 UsersService ,以便模块外部可见。

import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";

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

打开 auth/auth.service.ts 文件并输入以下代码。这里创建了一个函数来在 UsersService 的支持下验证用户。

import { Injectable } from "@nestjs/common";
import { UsersService } from "src/users/users.service";

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.find(username);

    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }

    return null;
  }
}

编辑 auth/auth.module.ts 文件将 UsersService 导入到身份验证模块。

import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { UsersModule } from "../users/users.module";

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

现在在 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 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;
  }
}

导入 PassportModule 并引入 LocalStrategy 来配置 auth 模块以使用本地策略,如下所示。

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 {}

auth 文件夹中生成一个名为 local-auth.guard.ts 的文件来实现将在登录请求中启动的本地防护,如下所示。

nest g gu auth/guards/local-auth

输入以下代码。

import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class LocalAuthGuard extends AuthGuard("local") {}

使用以下命令在 auth 中创建一个控制器。

nest g co auth

然后在其中创建 POST 请求并启动 LocalAuthGuard,如下所示。

import { Controller, Post, Request, UseGuards } from "@nestjs/common";
import { LocalAuthGuard } from "./guards/local-auth.guard";
@Controller("auth")
export class AuthController {
  constructor() {}
  @UseGuards(LocalAuthGuard)
  @Post("login")
  async login(@Request() req) {
    return req.user;
  }
}

至此基本的登录验证码就完成了。使用包含用户名和密码的 JSON 正文创建对 http://localhost:3000/auth/loginPOST 请求。

curl -X POST http://localhost:3000/auth/login -d '{"username": "admin", "password": "1234"}' -H "Content-Type: application/json"

如果登录成功,你应该获得用户信息,如果登录失败,它应该给你一条错误消息。

集成 JWT 身份验证

在将 JWT 集成到上述项目之前,需要安装以下依赖项和 dev 依赖项。

npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

然后打开 auth/auth.service.ts 文件并创建一个方法来使用所需的详细信息对用户进行签名,如下所示。

import { Injectable } from "@nestjs/common";
import { UsersService } from "src/users/users.service";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.find(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  async sign(user: any) {
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

创建一个密钥和过期时间以在 JWT 方法中使用。在名为 auth.config.tssrc/config 文件夹中创建一个新的配置文件,并按如下所示输入密钥。

export const jwtConfig = {
  secret: "secretKey",
  expireTime: "1h",
  expireIgnore: false,
};

按如下方式更新 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";
import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { jwtConfig } from "src/config/auth.config";
@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConfig.secret,
      signOptions: {
        expiresIn: jwtConfig.expireTime,
      },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

按如下方式更新 auth.controller.ts 文件。

import { Controller, Post, Request, UseGuards } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { LocalAuthGuard } from "./guards/local-auth.guard";
@Controller("auth")
export class AuthController {
  constructor(private authService: AuthService) {}
  @UseGuards(LocalAuthGuard)
  @Post("login")
  async login(@Request() req) {
    return this.authService.sign(req.user);
  }
}

现在再次发送之前的 POST 请求,你应该会获得 JWT 令牌作为响应。

curl -X POST http://localhost:3000/auth/login -d '{"username": "admin", "password": "1234"}' -H "Content-Type: application/json"

接下来,我将为每个请求启动 JWT 令牌保护。

现在,你必须在 auth 文件夹中创建一个名为 jwt.strategy.ts 的文件,并在其中实现 JWT 验证策略,如下所示。

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy, ExtractJwt } from "passport-jwt";
import { jwtConfig } from "src/config/auth.config";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: jwtConfig.expireIgnore,
      secretOrKey: jwtConfig.secret,
    });
  }
  async validate(payload: any) {
    return { id: payload.sub, username: payload.username };
  }
}

然后将 JwtStrategy 添加到 auth.module.ts 文件的 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 { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { JwtStrategy } from "./jwt.strategy";
import { jwtConfig } from "src/config/auth.config";
@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConfig.secret,
      signOptions: {
        expiresIn: jwtConfig.expireTime,
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

现在在 auth 文件夹中生成 jwt-auth.guard.ts 文件,如下所示。

nest g gu auth/guards/jwt-auth

并实现将在每个请求中启动的 JWT 防护,如下所示。

import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}

现在让我们为用户创建一个控制器,并使用 JWT 防护在其中实现受保护的路由。首先,按如下方式创建控制器。

nest g co users

然后按如下方式更新 users.service.ts 文件。

import { Injectable } from "@nestjs/common";
import { UserData } from "src/interfaces/user.interface";
@Injectable()
export class UsersService {
  private readonly users: Array<UserData> = [
    {
      id: 1,
      username: "admin",
      password: "1234",
    },
    {
      id: 2,
      username: "user",
      password: "1234",
    },
  ];
  async find(username: string): Promise<UserData | undefined> {
    return this.users.find((user) => user.username === username);
  }
  async findByID(id: number): Promise<UserData | undefined> {
    return this.users.find((user) => user.id == id);
  }
  async findAll(): Promise<Array<UserData>> {
    return this.users;
  }
}

然后在 users.controller.ts 文件中创建一些受保护的 API 调用,如下所示。

import {
  Controller,
  Get,
  NotFoundException,
  Param,
  UseGuards,
} from "@nestjs/common";
import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard";
import { UsersService } from "./users.service";
@Controller("users")
export class UsersController {
  constructor(private userService: UsersService) {}
  @UseGuards(JwtAuthGuard)
  @Get()
  async getAllUsers() {
    const users = await this.userService.findAll();
    let results = [];
    users.forEach((user) => {
      const { password, ...result } = user;
      results.push(result);
    });
    return results;
  }
  @UseGuards(JwtAuthGuard)
  @Get(":id")
  async getUser(@Param() params) {
    const user = await this.userService.findByID(Number(params.id));
    if (user) {
      const { password, ...result } = user;
      return result;
    } else {
      throw new NotFoundException();
    }
  }
}

现在你需要使用从登录请求中获取的令牌来发送请求,如下所示。

curl http://localhost:3000/users/1 -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR..."

集成授权

这里我就来讲解一下如何给项目添加 Permission-based 授权。

首先,让我们创建一些权限。在 src/enums 文件夹中创建一个名为 permission.enum.ts 的文件,并设置一些权限,如下所示。

export enum Permission {
  GET_USER = "get_user",
  GET_ALL_USERS = "get_all_users",
}

user.interface.ts 文件引入如下类型,如下所示。

import { Permission } from 'src/enums/permission.enum';

export interface UserData {
  id: number;
  username: string;
  password: string;
  permissions: Permission[];
}

users.service.ts 文件添加如下代码:

import { Injectable } from "@nestjs/common";
import { Permission } from "src/enums/permission.enum";
import { UserData } from "src/interfaces/user.interface";
@Injectable()
export class UsersService {
  private readonly users: Array<UserData> = [
    {
      id: 1,
      username: "admin",
      password: "1234",
      permissions: [Permission.GET_USER, Permission.GET_ALL_USERS],
    },
    {
      id: 2,
      username: "user",
      password: "1234",
      permissions: [Permission.GET_USER],
    },
  ];
  async find(username: string): Promise<UserData | undefined> {
    return this.users.find((user) => user.username === username);
  }
  async findByID(id: number): Promise<UserData | undefined> {
    return this.users.find((user) => user.id == id);
  }
  async findAll(): Promise<Array<UserData>> {
    return this.users;
  }
}

然后,你应该创建一个装饰器,如下所示,用于指定访问资源所需的权限。

nest g d decorators/permissions

并实现代码如下。

import { SetMetadata } from "@nestjs/common";
import { Permission } from "src/enums/permission.enum";
export const PERMISSIONS_KEY = "permissions";
export const Permissions = (...permissions: Permission[]) =>
  SetMetadata(PERMISSIONS_KEY, permissions);

现在让我们生成一个权限保护,如下所示。

nest g gu auth/guards/permissions

permissions.guard.ts 文件中,实现用户的权限比较方法如下。

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { PERMISSIONS_KEY } from "src/decorators/permissions.decorator";
import { Permission } from "src/enums/permission.enum";
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
      PERMISSIONS_KEY,
      [context.getHandler(), context.getClass()]
    );
    if (!requiredPermissions) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredPermissions.some((permission) =>
      user.permissions?.includes(permission)
    );
  }
}

现在打开 jwt.strategy.ts 文件,并在 validate 函数的 return 中添加权限,这样 PermissionGuard 就会获得权限进行比较。

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy, ExtractJwt } from "passport-jwt";
import { jwtConfig } from "src/config/auth.config";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: jwtConfig.expireIgnore,
      secretOrKey: jwtConfig.secret,
    });
  }
  async validate(payload: any) {
    return {
      id: payload.sub,
      username: payload.username,
      permissions: payload.permissions,
    };
  }
}

然后打开 auth.service.ts 文件并在 payload 中输入 permissions ,这样签名的有效负载就有权限。

import { Injectable } from "@nestjs/common";
import { UsersService } from "src/users/users.service";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.find(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  async sign(user: any) {
    const payload = {
      username: user.username,
      sub: user.id,
      permissions: user.permissions,
    };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

现在打开 users.controllers.ts 文件并根据需要将 @Permission(Permission.PERMISSION) 装饰器添加到请求中。

由于 JwtAuthGuardPermissionsGuard 都在此处的每个请求中使用,因此将它们移至类的顶部。

import {
  Controller,
  Get,
  NotFoundException,
  Param,
  UseGuards,
} from "@nestjs/common";
import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard";
import { PermissionsGuard } from "src/auth/guards/permissions.guard";
import { Permissions } from "src/decorators/permissions.decorator";
import { Permission } from "src/enums/permission.enum";
import { UsersService } from "./users.service";
@Controller("users")
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class UsersController {
  constructor(private userService: UsersService) {}
  @Get()
  @Permissions(Permission.GET_ALL_USERS)
  async getAllUsers() {
    const users = await this.userService.findAll();
    let results = [];
    users.forEach((user) => {
      const { password, ...result } = user;
      results.push(result);
    });
    return results;
  }
  @Get(":id")
  @Permissions(Permission.GET_USER)
  async getUser(@Param() params) {
    const user = await this.userService.findByID(Number(params.id));
    if (user) {
      const { password, ...result } = user;
      return result;
    } else {
      throw new NotFoundException();
    }
  }
}

你也可以在模块中提供这些应用程序防护,如下所示。

providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard
    },
    {
      provide: APP_GUARD,
      useClass: PermissionsGuard
    }
  ],

但在这里我没有这样做,因为 UsersModule 也被导入到 AuthModule 中。因此,如果我在模块中提供它们,它将应用于身份验证模块,并且还将检查登录请求的 JWT 访问权限。

Integrating Helmet

Helmet 包通过设置各种 HTTP 标头来帮助保护应用程序。首先,让我们安装该软件包。

npm i --save helmet

现在让我们使用以下命令创建一个全局中间件。

nest g mi app

现在将以下代码放入 app.middleware.ts 文件中。

import { INestApplication } from "@nestjs/common";
import * as helmet from "helmet";

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === "production";

  app.use(helmet({ contentSecurityPolicy: isProduction }));

  return app;
}

并打开 main.ts 文件并调用中间件函数。

import { NestFactory } from "@nestjs/core";
import { middleware } from "./app.middleware";
import { AppModule } from "./app.module";

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  middleware(app);

  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

现在要设置环境,打开 package.json 并修改 start:devstart:prod 脚本,如下所示。

"start:dev": "NODE_ENV=development nest build --webpack --webpackPath webpack-hmr.config.js --watch",
"start:prod": "NODE_ENV=production node dist/main"

跨站请求伪造保护

由于我们不使用 cookie,因此实际上并不需要 CSRF

防止暴力请求

为了防止暴力请求,让我们安装 throttler 包。

npm i --save @nestjs/throttler

然后将 TTL 和请求限制时间包含在 auth.config.ts 文件中,如下所示。

export const jwtConfig = {
  secret: "secretKey",
  expireTime: "1h",
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};

然后修改 app.module.ts 如下。

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { bruteForceLimits } from "./config/auth.config";
import { APP_GUARD } from "@nestjs/core";

@Module({
  imports: [
    AuthModule,
    UsersModule,
    ThrottlerModule.forRoot({
      ttl: bruteForceLimits.ttl,
      limit: bruteForceLimits.requestLimit,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

CORS(跨域)启用

CORS 配置插入 auth.config.ts 中,如下所示。

export const jwtConfig = {
  secret:
    "l0XNT38sJ/OkVVpKTPNixGyH2SW5yF1NJqB9BAGczf1JKHIjenyqaQPOX9Tt9LBC3WHCoG08yhZe+MkQtATYgr5dLhFoTifsczfXNNMBTrXmJN5DV+WzesYS/daFMDC0vXmG/20ImrFsw22EKDWl4+VDAE1slVx/B41t5gNGt4ffd0UPE0wpekd23FOECD0EoTCLYsM7nSnMhUlKB4ONvAlOgObXLAgCgMkDe1g69kspxT1ev7/MyXv+xDRUikJgvPOuy7lZVMQ5eOC4ouELNT5L18yc9hbYEfQXDsUe6zCN6DNbASCYt2Eg/ki2nwpJb+NUT69ObWzxG9ZGJpgrqA==",
  expireTime: "1h",
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};

export const corsConfig = {
  origin: "*",
  methods: "GET, PUT, POST, DELETE",
  allowedHeaders: "Content-Type, Authorization",
};

然后修改 app.middleware.ts 如下。

import { INestApplication } from "@nestjs/common";
import * as helmet from "helmet";
import { corsConfig } from "./config/auth.config";

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === "production";

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.enableCors({
    origin: corsConfig.origin,
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}

环境变量

首先,你应该在创建环境变量之前安装以下软件包。

npm i --save @nestjs/config

然后在根文件夹中创建 development.envproduction.env 文件。

让我们将原始配置移动到 development.env 文件中。

ORIGIN=http://localhost:3000

现在打开 auth.config.ts 文件并进行如下修改。

export const jwtConfig = {
  secret: "secretKey",
  expireTime: "1h",
  expireIgnore: false,
};

export const bruteForceLimits = {
  requestLimit: 10,
  ttl: 60,
};

export const corsConfig = {
  methods: "GET, PUT, POST, DELETE",
  allowedHeaders: "Content-Type, Authorization",
};

现在打开 app.module.ts 文件并按如下所示修改它以导入环境变量。

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { bruteForceLimits } from "./config/auth.config";
import { APP_GUARD } from "@nestjs/core";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `${process.env.NODE_ENV || "development"}.env`,
      isGlobal: true,
    }),
    AuthModule,
    UsersModule,
    ThrottlerModule.forRoot({
      ttl: bruteForceLimits.ttl,
      limit: bruteForceLimits.requestLimit,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

最后,打开 app.middleware.ts 文件并插入环境中的源。

import { INestApplication } from "@nestjs/common";
import * as helmet from "helmet";
import { corsConfig } from "./config/auth.config";

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === "production";

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.enableCors({
    origin: process.env.ORIGIN,
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}

请记住在运行生产构建之前将所有内容添加到 production.env 中。

压缩

添加压缩非常重要,这样响应正文会更小,应用程序会更快。要添加压缩,您需要安装以下软件包。

npm i --save compression

然后修改 app.middleware.ts 文件如下。

import { INestApplication } from "@nestjs/common";
import * as helmet from "helmet";
import { corsConfig } from "./config/auth.config";
import * as compression from "compression";

export function middleware(app: INestApplication): INestApplication {
  const isProduction = process.env.NODE_ENV === "production";

  app.use(helmet({ contentSecurityPolicy: isProduction }));
  app.use(compression());

  app.enableCors({
    origin: process.env.ORIGIN || "*",
    methods: corsConfig.methods,
    allowedHeaders: corsConfig.allowedHeaders,
  });

  return app;
}