Azure의 GraphQL: 섹션 7 - 서버측 인증

Azure의 GraphQL 여행에서 우리는 누구나 접근할 수 있는 단점만 만들었다.본고에서 GraphQL 서버에 인증을 추가하는 방법을 알아보겠습니다.
본고에서 우리는 Apollo ServerAzure Static Web Apps를 사용하여 API를 위탁 관리할 것이다. 주로 SWAprovides security 때문이다(당신이 알고 싶다면 이것이 바로 내가 API를 작성해야 하는 이유이다).
만약에Azure에서GraphQL의 초보자라면 Apollo를 사용하여GraphQL 서버를 만들고Azure 함수에 배치하는 방법을 소개해 드리겠습니다. 이것은 본고에서 사용할 과정입니다.

응용 프로그램 만들기


우리가 오늘 사용하고자 하는 응용 프로그램은 기본적인 블로그 응용 프로그램이다. 이 응용 프로그램에서 누군가가 신분 검증을 하고 표시된 새 글을 만들어서 저장할 수 있다. (메모리 저장소만 사용하면 된다.)사람들은 게시물에 댓글을 달 수 있지만, 전제는 그들이 이미 로그인했다는 것이다.
먼저 모델에 대한 유형 세트를 정의합니다.
type Comment {
    id: ID!
    comment: String!
    author: Author!
}

type Post {
    id: ID!
    title: String!
    body: String!
    author: Author!
    comments: [Comment!]!
    comment(id: ID!): Comment
}

type Author {
    id: ID!
    userId: String!
    name: String!
    email: String
}
적절한 입력 유형과 함께 질의와 돌연변이가 추가됩니다.
type Query {
    getPost(id: ID!): Post
    getAllPosts(count: Int! = 5): [Post!]!
    getAuthor(userId: String!): Author
}

input CreatePostInput {
    title: String!
    body: String!
    authorId: ID!
}

input CreateAuthorInput {
    name: String!
    email: String
    userId: String!
}

input CreateCommentInput {
    postId: ID!
    authorId: ID!
    comment: String!
}

type Mutations {
    createPost(input: CreatePostInput!): Post!
    createAuthor(input: CreateAuthorInput!): Author!
    createComment(input: CreateCommentInput!): Post!
}

schema {
    query: Query
    mutation: Mutations
}
이제 우리는 사용할 수 있는 패턴이 생겼다.우리 신분 검증에 대해 이야기합시다.

GraphQL의 인증


GraphQL의 신분 검증은 흥미로운 문제입니다. 이 언어는 그 언어에 아무것도 제공하지 않고 서버에 의존하여 신분 검증을 제공하고, 패턴 정의의 조회와 돌연변이에 어떻게 응용하는지 알 수 있기 때문입니다.
Apollo는 context 함수를 사용하여 some guidance on authentication 전송 요청에 접근할 수 있는 권한을 제공합니다.이 함수를 사용하여 SWA 인증 정보를 패키지 해제하고 context 객체에 추가할 수 있습니다.여기에서 도움을 얻기 위해서, 우리는 @aaronpowell/static-web-apps-api-auth 라이브러리를 사용할 것입니다. 왜냐하면 이것은 우리에게 로그인한 사람이 있는지 알려주고, 제목에서 클라이언트 주체를 풀 수 있기 때문입니다.
요청에서 인증 정보를 추가하는 context 함수를 구현합니다(본 문서에서는 구문 블록과 구문 분석기의 작업 방식과 같은 세부 사항을 건너뛰지만 마지막 전체 예에서 찾을 수 있습니다).
const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ request }: { request: HttpRequest }) => {
        return {
            isAuthenticated: isAuthenticated(request),
            user: getUserInfo(request)
        };
    }
});
여기에서, 우리는 npm 패키지를 사용하여 상하문isAuthenticateduser 속성을 설정합니다. 이것은 패키지 해제SWA authentication information from the header를 통해 작동합니다. (당신은 나의 npm 패키지를 필요로 하지 않습니다. 이것은 단지 유용할 뿐입니다.)

사용자 지정 명령을 사용하여 인증 적용


context 대상은 모든 해석 프로그램에서 사용할 수 있기 때문에 우리는 누군가가 신분 검증을 거쳤는지, 사용자 정보 (필요할 경우) 를 검사할 수 있다.기왕 사용할 수 있다면 우리는 어떻게 신분 검증 규칙을 우리의 모델에 응용합니까?패턴 등급의 물건으로 이 문제를 처리하는 것은 의미가 있는 것이지, 해석기에서 내부 검사를 하는 것이 아니다. 왜냐하면 우리의 패턴을 읽는 사람들은 규칙이 무엇인지 잘 알고 있기 때문이다.
GraphQL 지령이 답입니다.명령은 GraphQL 조회와 돌연변이에 사용자 정의 행동을 추가하는 방법입니다.그것들은 패턴에 정의되어 유형, 필드, 파라미터, 조회/변이에 적용될 수 있습니다.
먼저 특정 지역에 적용할 때 사용자에 대한 인증이 필요한 명령을 정의합니다.
directive @isAuthenticated on OBJECT | FIELD_DEFINITION
이 명령은 모든 유형, 필드 또는 매개 변수에 적용되며 컨텍스트의 isAuthenticated 속성이 true인 경우에만 적용됩니다.그렇다면, 우리는 어디에서 그것을 사용해야 합니까?논리적으로 첫 번째는 발생한 모든 돌연변이이기 때문에 모드의 돌연변이 부분을 업데이트하겠습니다.
type Mutations @isAuthenticated {
 createPost(input: CreatePostInput!): Post!
 createAuthor(input: CreateAuthorInput!): Author!
 createComment(input: CreateCommentInput!): Post!
}
현재, 우리는 모드의 @isAuthenticated 대상 유형에 Mutations 을 추가했다.모든 필드 정의에 추가할 수 있지만, 모든 돌연변이가 필요하기 때문에 Mutations 대상 유형에 추가하기만 하면 더욱 쉽다.현재 우리는 검증이 필요한 조회가 없기 때문에 변이를 계속 연구합시다.

사용자 지정 명령 구현


모드에서 명령을 정의하면 GraphQL이 서버에서 할 수 있는 일이라는 것만 알려주지만 실제로는 아무것도 하지 않습니다.우리는 Apollo에서 계승된 SchemaDirectiveVisitor 클래스를 만들어서 그것을 실현할 수 있는 어떤 방식이 필요합니다.
import { SchemaDirectiveVisitor } from "apollo-server-azure-functions";

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {}
이 명령은 객체 유형 또는 필드 정의를 지원할 수 있으므로 다음 두 가지 방법이 필요합니다.
import { SchemaDirectiveVisitor } from "apollo-server-azure-functions";

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
    visitObject(type: GraphQLObjectType) {}

    visitFieldDefinition(
        field: GraphQLField<any, any>,
        details: {
            objectType: GraphQLObjectType;
        }
    ) {}
}
이러한 방법을 실현하기 위해서 우리는 대상 유형의 모든 필드든 단일 필드든 필드를 다시 쓰는 resolve 함수를 필요로 한다.이를 위해 호출될 공통 함수를 만듭니다.
import { SchemaDirectiveVisitor } from "apollo-server-azure-functions";

export class IsAuthenticatedDirective extends SchemaDirectiveVisitor {
    visitObject(type: GraphQLObjectType) {
        this.ensureFieldsWrapped(type);
        type._authRequired = true;
    }

    visitFieldDefinition(
        field: GraphQLField<any, any>,
        details: {
            objectType: GraphQLObjectType;
        }
    ) {
        this.ensureFieldsWrapped(details.objectType);
        field._authRequired = true;
    }

    ensureFieldsWrapped(objectType: GraphQLObjectType) {}
}
우리는 항상 GraphQLObjectType (매개 변수, 필드 디테일에서 패키지 해제) 를 보내면 모든 처리해야 할 일을 규범화할 수 있다는 것을 알 수 있을 것이다.인증이 필요한지 확인하기 위해 필드 정의나 대상 유형에 _authRequired 속성을 추가합니다.
참고: TypeScript를 사용하는 경우 내가 이 코드 라이브러리에 있는 것처럼 다음과 같은 새 필드를 얻기 위해 유형 정의를 확장해야 합니다.
import { GraphQLObjectType, GraphQLField } from "graphql";

declare module "graphql" {
    class GraphQLObjectType {
        _authRequired: boolean;
        _authRequiredWrapped: boolean;
    }

    class GraphQLField<TSource, TContext, TArgs = { [key: string]: any }> {
        _authRequired: boolean;
    }
}
구현해야 할 때ensureFieldsWrapped:
  ensureFieldsWrapped(objectType: GraphQLObjectType) {
    if (objectType._authRequiredWrapped) {
      return;
    }
    objectType._authRequiredWrapped = true;

    const fields = objectType.getFields();

    for (const fieldName of Object.keys(fields)) {
      const field = fields[fieldName];
      const { resolve = defaultFieldResolver } = field;
      field.resolve = isAuthenticatedResolver(field, objectType, resolve);
    }
  }
우리는 우선 이 지령이 이미 이 대상에 적용되었는지 검사할 것이다. 왜냐하면 이 지령은 여러 번 적용될 수 있기 때문에 우리는 이미 포장된 내용을 포장할 필요가 없다.
다음에 우리는 대상 유형에서 모든 필드를 가져와 순환시키고 그것들의 resolve 함수를 가져올 것이다. (정의된 경우, 그렇지 않으면 기본적인GraphQL 필드 해상도를 사용할 것이다.) 그리고 isAuthenticatedResolver 함수로 이 함수를 포장할 것이다.
const isAuthenticatedResolver = (
    field: GraphQLField<any, any>,
    objectType: GraphQLObjectType,
    resolve: typeof defaultFieldResolver
): typeof defaultFieldResolver => (...args) => {
    const authRequired = field._authRequired || objectType._authRequired;

    if (!authRequired) {
        return resolve.apply(this, args);
    }

    const context = args[2];

    if (!context.isAuthenticated) {
        throw new AuthenticationError(
            "Operation requires an authenticated user"
        );
    }
    return resolve.apply(this, args);
};
이 기능은 일부 응용 프로그램과 비슷하지만 JavaScript에서 매개변수를 받아들인 다음 런타임에 사용할 새 함수를 반환하는 함수를 만들고 있습니다.필드 정의, 대상 유형, 원시 resolve 함수를 전송할 것입니다. 실행할 때 필요하기 때문에 패키지 범위 내에서 포착할 것입니다.
파서에 대해서는 필드나 대상 유형에 인증이 필요한지 확인하고 필요하지 않으면 원시 파서의 결과를 되돌려줍니다.
있다면, 우리는 context (이것은 Apollo 해석 프로그램의 세 번째 매개 변수) 를 가져와서 사용자가 신분 검증을 거쳤는지 확인하고, 없으면 Apollo가 제공한 AuthenticationError 를 던질 것입니다. 만약 그들이 신분 검증을 거쳤다면, 우리는 원시 해석 프로그램의 결과를 되돌려받을 것입니다.

명령 사용


우리는 이미 이 명령을 우리의 모드에 추가했고, 이 명령을 처리하기 위해 실현을 만들었으며, 나머지는 Apollo에게 그것을 사용하라고 알리는 것이다.
이를 위해 ApolloServer 파일의 index.ts를 업데이트합니다.
const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ request }: { request: HttpRequest }) => {
        return {
            isAuthenticated: isAuthenticated(request),
            user: getUserInfo(request)
        };
    },
    schemaDirectives: {
        isAuthenticated: IsAuthenticatedDirective
    }
});
schemaDirectives 속성은 우리가 아폴로에게 우리의 지령을 사용하라고 알려준 곳이다.이것은 키/값이 맞습니다. 그 중에서 키는 지령명이고 값은 실현됩니다.

결론


우리 망했어!이것은 정적 웹 응용 프로그램을 사용하는 인증 모델의 사용자 정의 명령을 사용하여GraphQL 서버에 인증을 추가하는 방법을 보여 주는 매우 간단한 예이다.
사용자 정의 지령을 사용하면 패턴을 표시하고, 패턴 단계에서 어떤 필드와 유형이 인증을 필요로 하는지 지시한 다음, 지령이 우리에게 복잡한 작업을 처리하도록 할 수 있다.
React UI on my GitHub 와 배치된 프로그램 is here 을 포함한 완전한 예시 프로그램을 찾을 수 있지만, 메모리에 저장되어 있기 때문에 데이터가 고도로 순식간에 저장된다는 것을 기억하십시오.

알 룬 바벨 / 그림ql-auth



Azure 정적 웹 사이트 템플릿


이 저장소에는 React+TypeScript를 사용하여 프로젝트를 만드는 데 사용되는 템플릿이 있습니다.
템플릿에는 TypeScript를 사용하는 사이트Azure Static Web App와 빈 폴더Create React App가 있고 TypeScript도 사용됩니다.
시작하려면 Use this template 버튼을 클릭하여 이 템플릿에서 저장소를 만들고 보기Azure Functions를 클릭합니다.

어플리케이션 실행


저장소 루트 디렉터리와 api 폴더의 터미널 실행npm start에서 이 두 서버를 시작하면 웹 응용 프로그램은 api, API는 http://localhost:3000를 엽니다.또는 http://localhost:7071 의 VS Code launch 를 사용하여 이 두 프로그램을 실행하고 디버거를 추가할 수 있습니다.
GitHub docs on using templates

GitHub 보기 보상 - 데이터를 현재 사용자로 제한


만약 우리가 Run full stack 유형을 보았다면, 우리는 일부 사용 가능한 필드를 현재 사용자로 제한하고 싶을 것입니다. 예를 들어 그들의 전자메일이나 ID입니다. 이 문제를 처리하기 위해 Author 명령을 만듭니다.
directive @isSelf on OBJECT | FIELD_DEFINITION

type Author {
    id: ID! @isSelf
    userId: String! @isSelf
    name: String!
    email: String @isSelf
}
이것만 있으면 isSelf 필드는 누구에게나 사용할 수 있지만, 그들의 개인 자료에 관한 모든 다른 내용은 그들에게만 한정된다.이제 이 명령을 수행할 수 있습니다.
import { UserInfo } from "@aaronpowell/static-web-apps-api-auth";
import {
    AuthenticationError,
    SchemaDirectiveVisitor
} from "apollo-server-azure-functions";
import { GraphQLObjectType, defaultFieldResolver, GraphQLField } from "graphql";
import { Author } from "../generated";
import "./typeExtensions";

const isSelfResolver = (
    field: GraphQLField<any, any>,
    objectType: GraphQLObjectType,
    resolve: typeof defaultFieldResolver
): typeof defaultFieldResolver => (...args) => {
    const selfRequired = field._isSelfRequired || objectType._isSelfRequired;

    if (!selfRequired) {
        return resolve.apply(this, args);
    }

    const context = args[2];

    if (!context.isAuthenticated || !context.user) {
        throw new AuthenticationError(
            "Operation requires an authenticated user"
        );
    }

    const author = args[0] as Author;
    const user: UserInfo = context.user;

    if (author.userId !== user.userId) {
        throw new AuthenticationError(
            "Cannot access data across user boundaries"
        );
    }

    return resolve.apply(this, args);
};

export class IsSelfDirective extends SchemaDirectiveVisitor {
    visitObject(type: GraphQLObjectType) {
        this.ensureFieldsWrapped(type);
        type._isSelfRequired = true;
    }

    visitFieldDefinition(
        field: GraphQLField<any, any>,
        details: {
            objectType: GraphQLObjectType;
        }
    ) {
        this.ensureFieldsWrapped(details.objectType);
        field._isSelfRequired = true;
    }

    ensureFieldsWrapped(objectType: GraphQLObjectType) {
        if (objectType._isSelfRequiredWrapped) {
            return;
        }

        objectType._isSelfRequiredWrapped = true;

        const fields = objectType.getFields();

        for (const fieldName of Object.keys(fields)) {
            const field = fields[fieldName];
            const { resolve = defaultFieldResolver } = field;
            field.resolve = isSelfResolver(field, objectType, resolve);
        }
    }
}
이 지령은 확실히 그 사용 방식에 대해 가설을 했다. 왜냐하면 Author.name 함수의 첫 번째 매개 변수는 resolve 유형이기 때문이다. 이것은 조회나 돌연변이를 통해 작가를 해석하려 한다는 것을 의미하지만, 다른 측면에서 그의 작업 원리는 Author 지령과 매우 비슷해서 누군가가 로그인할 수 있도록 확보한다.이것은 현재 사용자가 요청한 작성자인지 확인합니다. 그렇지 않으면 오류가 발생할 수 있습니다.

좋은 웹페이지 즐겨찾기