繁体   English   中英

为 nestjs 创建 isAuthor 守卫的最佳方法是什么?

[英]What would be the best way to create an isAuthor guard for nestjs?

我一直在上 udemy 的在线课程,并在玩guard middleware

我还创建了按照教程建议的admin.guard auth.guard ,但我在想如果我想添加一个isAuthor.guard ,不仅管理员可以对post或其他内容进行更改,而且原作者也可以进行更改编辑...

创建它的更好方法是什么? 应该是守门员吧? 还是中间件会更好?

PS 我尝试通过这篇文章通过守卫访问服务在 Nest.JS 中将服务注入守卫,但对我没有用。

编辑:此外,是否有可能拥有或守卫? 例如isAdmin / isAuthor所以它可以灵活使用而不是有一个isAdminOrAuthor

在此先感谢您的任何建议/建议。

我建议基本 RBAC 实现(来自 NestJS 文档): https ://docs.nestjs.com/security/authorization

您创建一个枚举:

role.enum.ts

export enum Role {
  User = 'user',
  Admin = 'admin',
}

您创建一个简单的装饰器来将元数据添加到任何控制器路由:

roles.decorator.ts

import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

像这样:

import { Roles } from '../decorators/roles.decorator'
import { Role } from '../enums/role.enum';

@Post()
@Roles(Role.Admin, Role.User)
create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

然后你创建一个 Guard 来处理请求:(Guards 的厉害之处在于它们可以访问你使用简单的装饰器添加到控制器路由的任何元数据)(中间件没有这个功能)。

roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    //Here you check where the user has the required role
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

在此示例中,假设 request.user 包含用户实例和允许的角色(在角色属性下),您可以在自定义身份验证保护中实现该关联( https://docs.nestjs.com/security/authentication

为确保此示例有效,您的 User 类必须如下所示:

class User {
  // ...other properties
  roles: Role[];
}

最后在 app.module.ts 或任何其他全局模块中全局(或在控制器级别)启用 Roles Guard:

providers: [
  {
    provide: APP_GUARD,
    useClass: RolesGuard,
  },
],

PS。 我想尝试回答您关于为什么将服务注入到警卫中不起作用的问题,但我需要查看代码,如果您分享它,我也许可以回答。

我不知道这是否是最好的方法,但这个方法似乎很实用(它适用于更大的 scope 而不仅仅是 isAdmin/isAuthor 的情况)。 注意:如果仅需要 isAdmin isAuthor 情况,请将适当的逻辑从 PostRelationResolver 移至 RolesGuard,并跳过整个通用方法。

这里提供了一种通用方法,因为它允许涵盖范围更广、性质相同的案例(有用户和任何特定实体——需要应用基于关系的限制)。

所以,要覆盖它。

假设阅读帖子(仅作为示例)以管理员可以看到所有帖子而作者只能看到他们自己的帖子的方式受到限制。

它可以这样实现:

  @Get('read-post/:postId')
  @UseGuards(RolesGuard)
  @SetMetadata('general-roles', [GeneralRole.ADMIN])
  @SetMetadata('relation-roles', [RelationRole.POST_AUTHOR])
  readPostAsAuthor(
    @Param('postId') postId: number,
  ) {
    return this.postRepository.findPostById(postId);
  }

对于帖子列表,如下所示:

  @Get('read-all-posts')
  async readAllPosts(
    @Req() request
  ) {
    const posts = await this.postRepository.findAll();
    return this.rbacService.filterList(
      request, 
      posts, 
      [GeneralRole.ADMIN], 
      [RelationRole.POST_AUTHOR]
    );
  }

列表过滤器注意事项:应确保实现甚至不响应不允许的帖子,并且此过滤器应仅用作备份(因为请求不包含足够的信息来限制调用)。

为此,需要 RolesGuard 实现:

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { GeneralRole } from "../role/general-role";
import { RelationRole } from "../role/relation-role";
import { RbacService } from "../rbac.service";

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private rbacService: RbacService,
  ) {
  }
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const contextHandler = context.getHandler();
    const request = context.switchToHttp().getRequest();
    
    const requestedGeneralRoles = this.reflector.get<GeneralRole[]>('general-roles', contextHandler);
    const requestedRelationRoles = this.reflector.get<RelationRole[]>('relation-roles', contextHandler);

    return this.rbacService.authorize(request, requestedGeneralRoles, requestedRelationRoles);
  }
}

实际授权的逻辑包含在 rbacService 中,如下所示:

import { Injectable } from "@nestjs/common";
import { GeneralRole } from "./role/general-role";
import { RelationRole } from "./role/relation-role";
import { UserRepository } from "./repository/user.repository";
import { CoreRelationResolver } from "./relation-resolver/core.relation-resolver";

@Injectable()
export class RbacService {

  constructor(
    private userRepository: UserRepository,
    private coreRelationResolver: CoreRelationResolver,
  ) {
  }

  // NOTE: This method should be implemented however token to user mapping is done - based on business requirement.
  async getUserByToken(token: string) {
    return await this.userRepository.findByToken(token);
  }

  async authorize(request: any, requestedGeneralRoles: GeneralRole[], requestedRelationRoles: RelationRole[]) {
    const user = await this.getUserByToken(request.headers['token']);

    if (!user) {
      return false;
    }

    if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
      // If user is of general role, it is simply allowed - regardless of relationRoles.
      return true;
    }
    
    // Relation roles handling (user is not ADMIN - for example - but is author of post)
    if (requestedRelationRoles) {
      const relationRoles = await this.coreRelationResolver.getRelationRoles(user, requestedRelationRoles, request);
      return this.isAllowed(requestedRelationRoles, relationRoles);
    }

    return false;
  }
  
  isAllowed(requestedRelationRoles: RelationRole[], containingRelationRoles: RelationRole[]) {
    const matches = containingRelationRoles.filter(sr => {
      return !!requestedRelationRoles.find(rr => rr === sr);
    });

    return !!matches.length;
  }

  async filterList(
    request: any, 
    entities: any[], 
    requestedGeneralRoles: GeneralRole[], 
    requestedRelationRoles: RelationRole[]
  ): Promise<any[]> {
    const user = await this.getUserByToken(request.headers['token']);

    if (!user) {
      return [];
    }

    if (requestedGeneralRoles && requestedGeneralRoles.indexOf(user.role) !== -1) {
      return entities;
    }

    const result = [];
    const relationResolver = await this.coreRelationResolver.findRelationResolver(requestedRelationRoles);
    
    for (const entity of entities) {
      const singleEntityRelations = await relationResolver.getRelations(user, entity);
      if (this.isAllowed(requestedRelationRoles, singleEntityRelations)) {
        result.push(entity);
      } else {
        console.warn("WARNING: Check next entity and query that responds with it. It shouldn't be here!");
        console.warn(entity);
      }
    }
    
    return result;
  }
}

在继续逻辑的 rest 之前,请允许我在这里提供一个小的描述。

授权逻辑在 RbacService 中停止。

CoreRelationResolver 服务是关于识别使用应用程序(发出请求)的用户与给定操作的实体(object)(在其上执行操作)之间的关系。

用户和特定实体之间的可能关系用 RelationalRoles 描述。 使用 RelationalRoles 限制定义为:“只有给定 Post 的 AUTHOR 和 COLLABORATOR 才能看到它”。

此处提供了 CoreRelationResolver 实现:

import { Injectable } from "@nestjs/common";
import { RelationRole } from "../role/relation-role";
import { IRelationResolver } from "./i-relation-resolver";
import { PostRelationResolver } from "./post.relation-resolver";
import { UserEntity } from "../entity/user.entity";
import { ClientAppRelationResolver } from "./client-app.relation-resolver";

@Injectable()
export class CoreRelationResolver {

  private relationResolvers: IRelationResolver<UserEntity, unknown>[];

  constructor(
    private postRelationAuthorization: PostRelationResolver,
    private clientAppRelationResolver: ClientAppRelationResolver,
  ) {
    this.relationResolvers = [
      this.postRelationAuthorization,
      this.clientAppRelationResolver,
    ];
  }

  async getRelationRoles(user: UserEntity, requiredRelations: RelationRole[], request: any): Promise<RelationRole[]> {
    let relationRoles = [];

    const relationResolver = await this.findRelationResolver(requiredRelations);
    
    if (relationResolver) {
      const relatedObject = await relationResolver.getRelatedObject(request);

      if (relatedObject) {
        relationRoles = await relationResolver.getRelations(user, relatedObject);
      }
    }

    return relationRoles;
  }

  async findRelationResolver(requiredRelations: RelationRole[]): Promise<IRelationResolver<UserEntity, unknown>> {
    let result = null;
    
    for (const relationResolver of this.relationResolvers) {
      const supportedRelations = await relationResolver.getSupportedRelations();

      const matches = supportedRelations.filter(sr => {
        return !!requiredRelations.find(rr => rr === sr);
      });

      if (matches.length) {
        result = relationResolver;
        break;
      }
    }
    
    return result;
  }
}

它的设计方式是在其构造函数中应注册并正确实现任何 RelationResolver(IRelationResolver 接口)。

IRelationResolver 接口:

import { RelationRole } from "../role/relation-role";

/**
 * T - Type of user
 * U - Type of relatedObject
 */
export interface IRelationResolver<T, U> {
  /**
   * Return RelationRoles that this resolver is responsible to handle.
   */
  getSupportedRelations(): Promise<RelationRole[]>;

  /**
   * Retrieve related object from the request data.
   */
  getRelatedObject(request: any): Promise<U>;

  /**
   * Calculate and provide relation between user and related object.
   */
  getRelations(user: T, relatedObject: U): Promise<RelationRole[]>;
}

最后,检索相关的 object 并识别用户与给定的 object 之间的关系,在此处实现:

import { IRelationResolver } from "./i-relation-resolver";
import { Injectable } from "@nestjs/common";
import { RelationRole } from "../role/relation-role";
import { UserEntity } from "../entity/user.entity";
import { PostEntity } from "../entity/post.entity";
import { PostRepository } from "../repository/post.repository";

@Injectable()
export class PostRelationResolver implements IRelationResolver<UserEntity, PostEntity> {
  constructor(
    private postRepository: PostRepository
  ) {
  }
  
  async getSupportedRelations(): Promise<RelationRole[]> {
    return [RelationRole.POST_AUTHOR];
  }

  async getRelatedObject(request: any): Promise<PostEntity> {
    const postId: string = request.params.postId;
  
    return await this.postRepository.findPostById(parseInt(postId));
  }

  async getRelations(user: UserEntity, relatedObject: PostEntity): Promise<RelationRole[]> {
    const relations = [];
    
    if (relatedObject.authorId === user.id) {
      relations.push(RelationRole.POST_AUTHOR);
    }
    
    return relations;
  }
}

显然,自由就是在这里实现任何需要的东西,不管关系是如何定义的。

对于接下来的所有 RBAC 案例(针对不同的实体类型),应该创建 RelationResolver,实现它,并将其注册到 CoreRelationResolver 的构造函数中。

总而言之,考虑到可用性范围,这种方法应该足够灵活,可以应用于许多 RBAC 场景(请从概念上考虑它——没有添加稳健性功能)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM