如何使用NestJS GraphQL正确授权某些字段?

问题描述 投票:0回答:1

我有一个 NestJS GraphQL API,它有一个

User
对象,我可以这样查询

@ObjectType()
export class User {
  @Field(() => ID)
  id: string

  @Field()
  name: string

  @Field()
  password: string
}

如果提出请求的人获得授权,我希望他们能够检索整个对象,如果没有,则应排除

password
字段。理想情况下,我想在这个对象上使用某种属性。

我有自己的授权系统,用于细粒度的权限,但是为此目的并不重要。我尝试将

password
字段添加为单独的 fieldResolver,并带有基本防护来检查用户是否已登录:

 @UseGuards(UserGuard)
 @ResolveField(() => String)
 async password(@Parent() user: User): Promise<string> {
   return 'this is the password'
 }

如果他们登录,它会按预期工作,我得到以下响应:

{
  "id": "1",
  "name": "John Doe",
  "password": "this is the password"
}

但是,如果他们没有登录,我会收到

Unauthorized
错误并且数据为空:

{
  errors: [
    {
      "message": "Unauthorized",
      // more error stuff
    }
  ],
  data: null
}

我期望的是

password
字段未经授权,但其他字段的数据仍然返回,例如

{
  errors: [
    {
      "message": "Unauthorized",
      "path": ["password"]
    }
  ],
  data: {
    "id": "1",
    "name": "John Doe"
  }
}

我知道这是 .NET GraphQL API 中的行为,所以想知道如何在 NestJS 中实现它?或者这不是我们想要的行为?

graphql nestjs apollo nestjs-graphql
1个回答
0
投票

我创建了这个公共指令和字段装饰器

公共指令.ts

import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { ForbiddenException } from '@nestjs/common';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';

export function publicDirectiveTransformer(schema: GraphQLSchema, directiveName: string) {
  const typeDirectiveArgumentMaps: Record<string, any> = {};
  return mapSchema(schema, {
    [MapperKind.OBJECT_TYPE]: (objectType) => {
      const publicDirective = getDirective(schema, objectType, directiveName)?.[0];
      if (publicDirective) {
        typeDirectiveArgumentMaps[objectType.name] = publicDirective;
      }
      return undefined;
    },
    [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
      const publicDirective =
        getDirective(schema, fieldConfig, directiveName)?.[0] ??
        typeDirectiveArgumentMaps[typeName];
      const { resolve = defaultFieldResolver } = fieldConfig;
      fieldConfig.resolve = (source, args, context, info) => {
        if (context.user) return resolve(source, args, context, info);
        const { path } = info;
        // return to auth.guard for root level field query, mutation, subscription
        if (!path.prev) return resolve(source, args, context, info);
        // check fields for PublicProtected requests without token
        if (!publicDirective)
          throw new ForbiddenException({ message: `Can't access ${path.key}`, ctx: context });
        return resolve(source, args, context, info);
      };
      return fieldConfig;
    },
  });
}

public.decorator.ts

import { applyDecorators, SetMetadata } from '@nestjs/common';
import {
  Field,
  FieldOptions,
  ReturnTypeFunc,
  ResolveField,
  Directive,
  ObjectType,
  InterfaceType,
  InterfaceTypeOptions,
  ObjectTypeOptions,
} from '@nestjs/graphql';

export const PublicField = (returnTypeFunction?: ReturnTypeFunc, options?: FieldOptions) =>
  applyDecorators(Directive('@public'), Field(returnTypeFunction, options));

export const PublicResolveField = (
  name?: string,
  returnTypeFunction?: ReturnTypeFunc,
  options?: FieldOptions,
) => applyDecorators(Directive('@public'), ResolveField(name, returnTypeFunction, options));

export const PublicObjectType = (options?: ObjectTypeOptions) =>
  applyDecorators(Directive('@public'), ObjectType(options ?? {}));

export const PublicInterfaceType = (options?: InterfaceTypeOptions) =>
  applyDecorators(Directive('@public'), InterfaceType(options));

包含这个app.module.ts

GraphQLModule.forRoot<ApolloDriverConfig>({
  ...Config options
  transformSchema: (schema) => publicDirectiveTransformer(schema, 'public'),
  buildSchemaOptions: {
    directives: [
      new GraphQLDirective({
        name: 'public',
        locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT],
      }),
    ],
  },
});

现在你可以使用like

@ObjectType()
export class User {
  @PublicField(() => ID)
  id: string

  @PublicField()
  name: string

  @Field()
  password: string
}

或者您也可以使用

@PublicObjectType
@PublicInterfaceType

将整个班级设置为公共

注意: 如果授权完成,则需要在 GraphQL 上下文中设置

user
键,其检查用于
publicDirectiveTransformer

© www.soinside.com 2019 - 2024. All rights reserved.