by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
API应该是安全的,以防止潜在的攻击。然而,令人惊讶的是,简单的人为错误往往会导致API端点不受保护。最近的一个例子是Optus数据泄露,它是由“访问客户数据不需要身份验证的API”引起的。这突出了对API进行彻底测试和严格安全措施以防止此类事件发生的重要性。
未受保护的API是OWASP的主要漏洞之一。其定义如下:
现代应用程序通常涉及富客户端应用程序和API,例如浏览器中的JavaScript和移动应用程序,
它们连接到某种API。这些API通常不受保护,并且包含许多漏洞。
为了避免未受保护的API,第一道防线是通过单元测试。在本文中,我们将讨论如何使用单元测试来确保NestJS控制器和端点得到保护。
守卫Guards
在NestJS中,我们使用身份验证/授权保护来保护控制器或端点。使用Guards,我们可以确保只有授权用户才能访问API。以下是身份验证保护的示例。
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: any) {
// 对用户请求进行身份验证以确保其经过身份验证
// 即验证令牌
}
}
AuthGuard
类包含canActivate
方法,该方法将ExecutionContext
对象作为参数。该方法执行验证以确定是否应允许该请求。
要将AuthGuard
应用于控制器,我们可以使用@UseGuards
装饰器
@UseGuards(AuthGuard)
@Controller()
export class AppController {}
如果@UseGuards
装饰器被意外删除,API将变得不受保护,并容易受到未经授权的访问。为了确保Guard正确应用于控制器,我们可以使用单元测试来验证其存在。
为了实现这一点,我们需要使用反射元数据库和NestJS未记录的DiscoveryService API。让我们先来看反射元数据库。
Reflect-MetaData
反射元数据库提供对反射API的支持,反射API是ECMAScript规范的一部分。我们可以在运行时使用它来获取元数据。值得注意的是,NestJS还在后台使用反射元数据来处理元数据。
下面的示例演示了如何从AppController中检索Guards元数据。
const guards = Reflect.getMetadata('__guards__', AppController);
getMetaData方法采用两个参数:
-
metadataKey:用于存储和检索元数据的密钥。在这种情况下,密钥是__guards__,NestJs使用它来引用guards。
-
target:在其上定义元数据的目标对象。
使用getMetadata
方法,我们可以编写一个简单的单元测试来验证AppController是否受到保护。
it('should AuthGard be applied to the AppController', async () => {
const guards = Reflect.getMetadata('__guards__', AppController);
const guard = new guards[0]();
expect(guard).toBeInstanceOf(AuthGuard);
});
如果应用程序中有多个控制器,是否可以同时编写一个涵盖所有控制器的单元测试?答案是肯定的,但我们需要使用NestJS发现服务。
发现服务
NestJS发现服务是一个未记录的公共API。需要注意的是,“未记录”的功能在未来可能会发生更改或损坏。虽然这是一个方便使用的功能,但通常最好避免依赖应用程序中未记录的功能。在我个人看来,只要你意识到风险,在单元测试中使用它是可以接受的。
如下所示,使用this.discoveryService.getControllers()
,我们可以获得InstanceWrapper={metatype,name,instance,…}
类型的集合。
const controllers = await discoveryService.getControllers({});
要从InstanceWrapper
中提取防护元数据,我们可以使用getEnhancersMetadata
方法。在下面的测试中,我们遍历每个控制器,并验证它们是否受到AuthGuard
的保护。
it("should have AuthGard applied for all controllers", async () => {
const controllers = await discoveryService.getControllers({});
controllers.map((c) => {
const guard = c
.getEnhancersMetadata()
?.filter(
({ instance }: InstanceWrapper) => instance instanceof AuthGuard
);
expect(guard[0].name).toEqual("AuthGuard");
});
});
为了实现精细级别的访问控制,我们可以定义RoleGuard并将其应用于各个端点。我们在下面的函数中使用SetMetadata
来分配具有特定键的元数据。SetMetadata
是一个开箱即用的NestJS装饰器函数。
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
要将角色
装饰器应用于端点,我们需要将角色传递给装饰器
@Get()
@Roles('Admin')
getAll() {
return [];
}
保护端点
为了检测端点是否与角色装饰器相关联,我们使用如下所示的getMetadata
。
const decorators = Reflect.getMetadata(
'roles',
DevController.prototype.getAll,
);
在单元测试中,我们可以验证端点是否受到具有角色的角色装饰器的保护
it("should getAll be accessible by Admin role only", () => {
const decorators = Reflect.getMetadata(
"roles",
DevController.prototype.getAll
);
expect(decorators).toContain("Admin");
});
Reflect
API可用于获取端点的其他元数据。
it("should getHello has correct path and http method", () => {
const path = Reflect.getMetadata("path", appController.getHello);
expect(path).toBe("/");
const method = Reflect.getMetadata("method", appController.getHello);
expect(method).toBe(RequestMethod.GET);
});
在上面对getMetaData
的使用中,使用的键是“path
”和“method
”,它们分别对应于getHello
方法的path和HTTP方法。
奖励内容:使用发现服务动态获取服务列表
发现服务可以很好地解决某些问题。例如,在我最近的一个NestJS项目中,有一个MapperResolver
类:
@Injectable()
export class MapperResolver {
private mapperList: IMapper[];
constructor(
private serviceAMapper: ServiceAMapper,
private serviceBMapper: ServiceBMapper,
private serviceCMapper: ServiceCMapper,
private serviceDMapper: ServiceDMapper,
) {
this.mapperList = [
serviceAMapper,
serviceBMapper,
serviceCMapper,
serviceDMapper
];
}
public Resolve(serviceType: string): IMapper {
const mapper = this.mapperList.find(c => c.serviceType === serviceType);
if (mapper) {
return mapper;
}
throw new Error(`No Mapper found`);
}
}
在现实世界的项目中,有10多个Mapper类被注入到MapperResolver
类构造函数中,随着新功能的添加,这个数字还在继续增长。这已成为一个维护问题。
我们可以使用发现服务来解决这个问题。由于此主题不在本文的范围内,我将只对解决方案进行简要描述。
-
创建一个接受参数的decorator
@ServiceRegister
-
将装饰器添加到每个Mapper类,即
@ServiceRegister(“Mapper”)
-
使用
discoveryService.getProviders()
检索所有提供程序,并使用元数据筛选出映射器服务。
最终的结果是,我们能够删除MapperResolver类中所有注入的Mapper服务。但是,请再次注意使用DiscoveryService,因为它是一个未经文档记录的API。
总结
定期测试和监控应用程序的安全性以确保其安全可靠是很重要的。通过将安全检查嵌入到单元测试中,并定期运行这些测试,我们可以发现任何问题并在它们在生产中引起问题之前进行修复。
祝大家快乐的编程!