NESTJS: BUILDING THE BASE
原文来自: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
使用以下命令生成带有服务的 Auth
和 User
模块。
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/login
的 POST
请求。
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.ts
的 src/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);
}
}
现在再次发送之前的 POS
T 请求,你应该会获得 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)
装饰器添加到请求中。
由于 JwtAuthGuard
和 PermissionsGuard
都在此处的每个请求中使用,因此将它们移至类的顶部。
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:dev
和 start: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.env
和 production.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;
}