Безопасность проекта так же важна, как и его реализация. Иногда мы хотим, чтобы доступ к нашим данным имели только зарегистрированные ранее люди, а не все. Мы обеспечиваем это с аутентификацией.

И иногда мы не хотим, чтобы все имели доступ ко всем данным и API. Мы обеспечиваем это концепцией авторизации.

В этой статье я расскажу о том, как мы можем обеспечить авторизацию и аутентификацию на стороне сервера с помощью jsonwebtoken (jwt) и >библиотеки graphql-shield. Я создам сервер в Node.js, используя GraphQL. Затем я выполню процесс входа с помощью токена доступа, который мы сгенерировали с помощью jwt. После этого я решу, какому вошедшему в систему пользователю разрешен доступ к какому API, используя функции graphql-shield.

После сборки сервера я приведу несколько примеров того, как отправлять запросы к этим GraphQL API со стороны клиента.

Я буду использовать Visual Studio Code в качестве IDE и Angular11 в качестве клиента, и я предполагаю, что вы знакомы с GraphQL, Angular и Node.j. Если вы не знакомы с этими концепциями, вы можете найти некоторую информацию в этой статье о том, как создать сервер GraphQL в Node.js.

Веб-токен JSON (jwt)

Я не буду говорить о структуре jwt. Но кратко; jwt – это стандарт, позволяющий передавать информацию между сторонами в зашифрованном виде в виде объекта JSON. Если вы шифруете данные с помощью jwt, вы подписываете их специальной строкой. Опять же, эта подпись требуется для того, чтобы расшифровать данные, куда они были переданы.

graphql-щит

graphql-shield — это библиотека, которая помогает вам создать уровень разрешений для доступа к вашим API, созданным с помощью GraphQL. С помощью этой библиотеки вы можете определять правила аутентификации и авторизации по своему усмотрению и применять их к нужному API.

Давайте создадим наш проект сейчас. Сначала я начну с серверной:

Я создаю файл package.json в файле, где я создам проект:

npm-init -y

затем я устанавливаю два пакета, которые обеспечивают удобство синтаксиса:

npm install babel-cli babel-preset-es2015

Поскольку я буду устанавливать GraphQL-Server с graphql-yoga, я устанавливаю соответствующий пакет:

npm install graphql-yoga

Теперь я устанавливаю необходимые пакеты для аутентификации и авторизации:

npm install jsonwebtoken graphql-shield

мы установили необходимые пакеты. Теперь давайте добавим данные, которые мы будем использовать. Поскольку я не буду использовать базу данных, я добавляю массив с именем Users, который содержит некоторую информацию о пользователе, в файл с именем data.js и экспортирую его:

const Users = [
 {
   id: 1,
   username: “john”,
   city: “Melbourne”,
   password: “123456”, 
   role: “admin”,
 },
 {
   id: 2,
   username: “samet”,
   city: “Istanbul”,
   password: “123”,
   role: “user”,
 },
 {
   id: 3,
   username: “maria”,
   city: “Zagreb”,
   password: “456”,
   role: “user”,
 },
];
module.exports = Users;

Для облегчения чтения кода я создаю два отдельных файла для двух основных концепций GraphQL; typeDefs и преобразователи:

Я создаю файл TypeDefs.js и добавляю следующие коды:

const typeDefs = `
  type Query {
    users: [User!]!
    login(username:String!, password:String!): String
  }  
  type Mutation{
    addUser(id:ID!, username:String!, city:String! ): [User]
  }  
  
  type User{
    id: ID!
    username: String!
    password: ID!
    role: String!
    city: String!
  }
`;
module.exports = typeDefs;

Здесь функция "users" — это функция, которая получает всех пользователей. Функция логин выполнит аутентификацию. "addUser" — это функция, которая добавляет нового пользователя в массив.

Я создаю файл resolvers.js и добавляю следующие коды:

import Users from “./data”;
import jwt from “jsonwebtoken”;
const resolvers = {
  Query: {
    users: async (parent, args) => {
      return Users;
    },
    login: async (_, { username, password }) => {
      let user = Users.find((u) => u.username === username && u.password === password
    );
    if(user){
     const token = jwt.sign(
      {username: user.username, password: user.password, role:   user.role }, “MY_TOKEN_SECRET”);
      return token;    } else return "unknown user"
  },
},  
  Mutation: {
    addUser: async (_, { id, username, city }) => {
      const newUser = {
        id: id,
        username: username,
        city: city,
      };
      Users.push(newUser);
     return Users;
    },
  },
};
module.exports = resolvers;

Аутентификация

Здесь мы видим, что jsonwebtoken (jwt) используется в функции логин. Это проверяет, находится ли информация о пользователе, отправленная клиентом, в массиве на сервере (обычно в базе данных) по информации об имени пользователя и пароле. Это аутентификация. Если пользователь существует в базе данных, с помощью метода jwt .sign() создается токен и отправляется клиенту. Этот токен хранится в localStorage клиента, и когда запрашивается доступ к API, информация об этом токене также отправляется вместе с запросом.

При создании токена мы можем предоставить необходимую информацию в виде требований. Здесь имя пользователя, пароль и роль информация, которую я дал. Также должно быть указано ключевое строковое значение, которым мы подписываем токен. В примере я использовал строку «MY_TOKEN_SECRET». Этот ключ также будет использоваться для декодирования токена.

Теперь давайте создадим файл index.js и добавим следующие коды.

import { GraphQLServer } from “graphql-yoga”;
import jwt from “jsonwebtoken”;
import { rule, shield, and, or, not } from “graphql-shield”;
import typeDefs from "./TypeDefs";
import resolvers from "./resolvers";
function getClaims(req) {
  let token;
  try {
    token = jwt.verify(req.request.headers.authorization, “MY_TOKEN_SECRET”);
  } catch (e) {
    return null;
  }
  console.log(token);
  return token;
}
// Rules
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
  return ctx.claims !== null;
});
const canAddUser = rule()(async (parent, args, ctx, info) => {
  return ctx.claims.role === “admin”;
});
// Permissions
const permissions = shield({
  Query: {
    users: and(isAuthenticated),
  },
  Mutation: {
    addUser: and(isAuthenticated, canAddUser),
  },
});
const server = new GraphQLServer({
  typeDefs,
  resolvers,
  middlewares: [permissions],
  context: (req) => ({
    claims: getClaims(req),
  }),
});
server.start({ port: 4000 }, () =>
  console.log(“Server is running on http://localhost:4000")
);

В конструкторе GraphQLServer мы видим, что в дополнение к typeDefs и передаются аргументы промежуточного ПО и контекста. преобразователи. Во-первых, я хочу поговорить о концепции контекста.

Аргумент контекста в GraphQL

контекст в преобразователе — это объект, совместно используемый всеми преобразователями определенного выполнения. Это полезно для хранения таких данных, как информация об аутентификации , текущий пользователь, подключение к базе данных, источники данных и другие данные, необходимые для выполнения бизнес-логики.

объект контекста — это объект, который передается каждому преобразователю на каждом уровне, поэтому мы можем получить к нему доступ в любом месте кода нашей схемы. Объект req, который он получает, содержит информацию о запросе. Этот req также включает информацию токена, отправленную клиентом. С помощью функции .getClaims(), которую мы определили в контексте, мы декодируем токен, используя.verify()метод объект jwt и получить требования от токена.

function getClaims(req) {
  let token;
  try {
    token = jwt.verify(req.request.headers.authorization, “MY_TOKEN_SECRET”);
  } catch (e) {
    return null;
  }
  console.log(token);
  return token;
}

Каждый раз, когда мы отправляем запрос от клиента на сервер, запускается метод getClaims() и возвращает декодированный токен. Как видно выше, вы можете увидеть декодированный токен в консоли.

Авторизация

Согласно нашему сценарию, в информации претензии, приходящей с запросом, есть поле role. В соответствии с этой информацией о роли мы определяем правила с помощью библиотеки graphql-shield. Например; Мы хотим, чтобы пользователь вошел в систему для доступа к функции "users". Не имеет значения, является ли поле «роль» «пользователь» или «администратор». Для этого я пишу следующий код.

const isAuthenticated = rule()(async (parent, args, ctx, info) => {
  return ctx.claims !== null;
});

В соответствии с правилом, которое мы определили с помощью функции .rule() в библиотеке graphql-shield, если утверждения не null, то есть, если учетные данные пользователя верны, пользователь имеет право доступа к функции "users". Другими словами, достаточно войти в программу с учетными данными пользователя, чтобы получить доступ к функции "users".

Однако по сценарию вам нужно быть администратором, чтобы добавить нового пользователя в программу. Мы указали правило для этого с помощью следующего кода:

const canAddUser = rule()(async (parent, args, ctx, info) => {
  return ctx.claims.role === “admin”;
});

Затем мы определили щит, чтобы указать, к каким функциям будут применяться эти правила.

// Permissions
const permissions = shield({
  Query: {
    users: and(isAuthenticated),
  },
  Mutation: {
    addUser: and(isAuthenticated, canAddUser),
  },
});

Как видите, здесь важно, являются ли типы функций Query или Mutation, как определено в typeDefs.

И мы передаем эти правила (разрешения) на сервер с помощью аргумента middlewares;

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  middlewares: [permissions],
  context: (req) => ({
    claims: getClaims(req),
  }),
});

Теперь наш сервер готов к тестированию с настройкой аутентификации и авторизации. Наконец, я добавляю приведенный ниже код в узел scripts в файле package.json:

“start”: “nodemon index.js — exec babel-node — presets es2015”,

И я пишу следующую команду на терминале, где существует файл проекта.

npm start

Теперь вы можете тестировать коды в интерфейсе Playground. (Если "nodemon" не установлен на вашем компьютере, вы можете установить его с помощью команды "npm i nodemon")

Однако вы можете протестировать функцию Вход только на экране PlaygroundUI. Потому что мы не установили для него правило авторизации. Чтобы протестировать две другие функции, вы можете загрузить проект, который отправляет запрос на этот сервер из Angular. Или в консоли вы можете получить токен, сгенерированный вами с помощью функции "login", и назначить его переменной token в .getClaims() с помощью копировать/вставить.

в случае несанкционированных транзакций появится следующий экран:

Когда вы загружаете и запускаете соответствующий клиентский проект, он будет работать на порту 4200. Ваш серверный проект также должен работать на порту 4000 (он уже работает на порту 4000 по умолчанию).

Экран входа приветствует вас:

Давайте сделаем наш пример запроса со следующей информацией о пользователе:

{
   id: 2,
   username: “samet”,
   city: “Istanbul”,
   password: “123”,
   role: “user”,
}

Когда вы вводите имя пользователя и пароль, кнопка списка пользователей будет работать. Потому что эта кнопка запускает функцию «пользователи» на сервере, и вам нужно только войти в систему, чтобы получить доступ к этой функции.

Однако этот пользователь не сможет добавить нового пользователя, поскольку значение роли не равно "admin". Если вы попытаетесь добавить нового пользователя в разделе ДОБАВИТЬ НОВОГО ПОЛЬЗОВАТЕЛЯ слева, вы получите следующее предупреждение:

В сообщении об ошибке на консоли написано: "Не авторизовано!". Это означает несанкционированную операцию.

Вы можете сделать еще один запрос с информацией о пользователе John, у которого есть роль "admin".

Подведение итогов

В Node.js мы видели, как структуры аутентификации и авторизации могут быть созданы на сервере, созданном с помощью GraphQL. . Затем мы наблюдали за результатами, отправляя запросы из заранее подготовленного клиентского проекта на этот сервер.

Вы можете получить доступ к серверному проекту с помощью этой ссылки.