DevGang
Авторизоваться

Создавайте и защищайте API GraphQL с помощью Laravel 

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

В этом руководстве вы узнаете, как настроить GraphQL API с помощью Laravel, бесплатного фреймворка для веб-приложений PHP с открытым исходным кодом. Затем вы защитите API, чтобы он был доступен только авторизованным пользователям, выполняющим вход с помощью Okta.

Построение проекта

Рекомендуемый способ создания нового проекта Laravel - использовать Laravel Sail, интерфейс командной строки, разработанный для среды Laravel Docker. Сначала создайте каталог, в котором будут храниться оба набора кода для этого проекта - один набор для серверной части, а другой - для внешнего интерфейса. Убедитесь, что у вас установлены последние версии Docker и Docker Compose. Если вам нужна помощь, ознакомьтесь с руководством по Docker, а также с документацией по Docker Compose.

Чтобы запустить проект Laravel для этого руководства, вы можете получить сценарий из laravel.build. Не рекомендуется передавать скрипты из Интернета в Bash, поэтому сначала проверьте скрипт. Если вы перейдете к этому демонстрационному коду, вы увидите сгенерированный скрипт. Вы можете изменить «graphql-demo» на любое название проекта. Если вам это нравится, вставьте его в свой терминал или направьте напрямую в Bash, используя curl -s "https://laravel.build/graphql-demo?with=mysql" | bash.

Это создаст новый каталог с именем, которое вы использовали в качестве параметра пути (в данном случае graphql-demo), и настроит для вас Laravel. Как только скрипт загрузит Docker Image for Sail, он предложит вам запустить cd graphql-demo && ./vendor/bin/sail up. Поскольку вы будете постоянно вызывать Sail, сделайте для него псевдоним. Вы можете использовать alias sail=./vendor/bin/sail.

После выполнения команды up вы обнаружите, что Laravel работает по адресу http://localhost.

Модели Laravel

Затем настройте свои модели Laravel и миграции. Чтобы работать с более интересными данными, создайте несколько взаимосвязанных моделей. Это похоже на создание системы отслеживания проблем. В системе будут следующие модели:

  1. Пользователь
  2. Проблема
  3. Комментарий

Если вы хотите скопировать файлы из общедоступного репозитория GitHub, модели можно найти здесь, а миграции - здесь.

Если вы предпочитаете создавать эти модели самостоятельно, выполните команду с помощью инструмента Laravel Artisan CLI. Поскольку вы используете Sail, выполняйте с помощью sail php artisan <command>.

Затем запустите следующее:

sail php artisan make:model -m Issue
sail php artisan make:model -m Comment

Модель пользователя и миграции уже существуют по умолчанию.

Миграции Laravel

Вышеупомянутые команды также создали необходимые миграции благодаря флагу -m. Обновите их, выполнив действия, описанные здесь.

Пользователь:

<?php
// database/migrations/2014_10_12_000000_create_users_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('users', function (Blueprint $table) {
           $table->id();
           $table->string('name');
           $table->string('email')->unique();
           $table->timestamp('email_verified_at')->nullable();
           $table->string('password');
           $table->rememberToken();
           $table->timestamps();
       });
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('users');
   }
}

Проблема:

<?php
// database/migrations/2021_11_14_031132_create_issues_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateIssuesTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('issues', function (Blueprint $table) {
           $table->id('id');
           $table->unsignedBigInteger('author_id');
           $table->unsignedBigInteger('assignee_id');
           $table->string('title');
           $table->text('description');
           $table->timestamps();
       });
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('issues');
   }
}

Комментарий:

<?php
// database/migrations/2021_11_14_030557_create_comments_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentsTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('comments', function (Blueprint $table) {
           $table->id('id');
           $table->unsignedBigInteger('issue_id');
           $table->string('content');
           $table->unsignedBigInteger('author_id');
           $table->timestamps();
       });
   }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
   public function down()
   {
       Schema::dropIfExists('comments');
   }
}

Отношения

Затем определите некоторые отношения на моделях, чтобы связать все вместе. Ваши модели должны выглядеть как здесь.

Пользователь:

<?php
// app/Models/User.php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
   use HasApiTokens, HasFactory, Notifiable;

   /**
    * The attributes that are mass assignable.
    *
    * @var string[]
    */
   protected $fillable = [
       'name',
       'email',
       'password',
   ];

   /**
    * The attributes that should be hidden for serialization.
    *
    * @var array
    */
   protected $hidden = [
       'password',
       'remember_token',
   ];

   /**
    * The attributes that should be cast.
    *
    * @var array
    */
   protected $casts = [
       'email_verified_at' => 'datetime',
   ];

   public function issues(): HasMany {
       return $this->hasMany(Issue::class, 'author_id', 'id');
   }

   public function comments(): HasMany {
       return $this->hasMany(Comment::class);
   }
}

Проблема:

<?php
// app/Models/Issue.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Issue extends Model
{
   use HasFactory;

   public function author(): BelongsTo {
           return $this->belongsTo(User::class, 'author_id');
   }

   public function assignee(): BelongsTo {
           return $this->belongsTo(User::class, 'assignee_id');
   }

   public function comments(): HasMany {
           return $this->hasMany(Comment::class);
   }
}

Комментарий:

<?php
// app/Models/Comment.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
   use HasFactory;

   public function issue(): BelongsTo {
           return $this->belongsTo(Issue::class);
   }

   public function author(): BelongsTo {
           return $this->belongsTo(User::class, 'author_id');
   }
}

Factories and seeders

Добавьте factories и seeders, чтобы у вас были данные для работы. Это позволяет вам сгенерировать столько экземпляров ваших моделей, сколько вам нужно, а также предварительно заполненные отношения, которые вы только что определили. Выполните следующее:

sail php artisan make:seeder SimpleSeeder
sail php artisan make:factory IssueFactory
sail php artisan make:factory CommentFactory

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

database/factories/IssueFactory.php:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class IssueFactory extends Factory
{
   /**
    * Define the model's default state.
    *
    * @return array
    */
   public function definition()
   {
       return [
           'title' => $this->faker->sentence,
           'description' => $this->faker->sentence,
       ];
   }
}
database/factories/CommentFactory.php:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CommentFactory extends Factory
{
   /**
    * Define the model's default state.
    *
    * @return array
    */
   public function definition()
   {
       return [
           'content' => $this->faker->sentence
       ];
   }
}
database/seeders/SimpleSeeder.php:

<?php

namespace Database\Seeders;

use App\Models\Comment;
use App\Models\Issue;
use App\Models\User;
use Illuminate\Database\Seeder;

class SimpleSeeder extends Seeder
{
   /**
    * Run the database seeds.
    *
    * @return void
    */
   public function run()
   {
       User::factory()
           ->has(
               Issue::factory()
                   ->for(User::factory(), 'assignee')
                   ->has(
                       Comment::factory()
                           ->for(User::factory(), 'author')
                           ->count(3)
                   )
                   ->count(5)
           )
           ->count(10)
           ->create();
   }
}

После обновления этих классов обновите DatabaseSeeder.php, чтобы вызвать новый SimpleSeeder:

<?php
// database/seeders/DatabaseSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
   /**
    * Seed the application's database.
    *
    * @return void
    */
   public function run()
   {
       $this->call(SimpleSeeder::class);
   }
}

Чтобы убедиться, что все работает должным образом, запустите только что созданные миграции и сидеры. Это заполнит вашу базу данных записями, которые могут использоваться GraphQL API. Выполните следующую команду:

sail php artisan migrate:fresh --seed

Затем вы установите сервер GraphQL.

Установка Lighthouse

Есть несколько разных пакетов для GraphQL с Laravel. В этом руководстве основное внимание будет уделено Lighthouse, поскольку он имеет минимальный шаблон и позволяет быстро приступить к работе с GraphQL. Как и в случае с командами php artisan выше, добавьте префикс sail, если вы хотите установить зависимости компоновщика с помощью Sail

Установите зависимости GraphQL:

sail composer require nuwave/lighthouse mll-lab/laravel-graphql-playground

Lighthouse создаст для вас сервер, а Playground позволяет протестировать вашу схему без внешнего инструмента.

После установки обеих зависимостей выполните следующие команды, чтобы опубликовать схему и файл конфигурации из Lighthouse:

sail php artisan vendor:publish --tag=lighthouse-schema
sail php artisan vendor:publish --tag=lighthouse-config

Добавьте маршрут GraphQL API в файл конфигурации CORS. Перейдите к массиву config/cors.php и обновите paths его 'graphql', например:

'paths' => ['api/*', 'sanctum/csrf-cookie', 'graphql'],

Наконец, обновите схему Lighthouse на graphql/schema.graphql. Измените содержимое на следующее:

type Query {
   users: [User!]! @all
   user(id: Int! @eq): User @find

   issues: [Issue!]! @all
   issue(id: Int! @eq): Issue @find
}

type User {
   id: ID!
   name: String!
   issues: [Issue!]! @hasMany
}

type Issue {
   id: ID!
   title: String!
   description: String!
   author: User! @belongsTo
   assignee: User! @belongsTo
   comments: [Comment!]! @hasMany
}

type Comment {
   id: ID!
   content: String!
   issue: Issue! @belongsTo
   author: User! @belongsTo
}

Этот файл описывает, какие типы доступны через GraphQL и какие атрибуты они предоставляют, а также отношения, которым они могут следовать.

Демо без авторизации

Перейдите на сайт playground по адресу http://localhost/graphql-playground. Интерфейс позволит вам опробовать GraphQL с только что созданным сервером. Введите следующий запрос слева и нажмите кнопку Play:

query GetIssues {
  issues {
    id
    title
    description
    author {
      name
    }
    assignee {
      name
    }
    comments {
      id
      content
      author {
        name
      }
    }
  }
}

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

Затем вы добавите аутентификацию в свой GraphQL API и разрешите пользователям входить в систему через Okta.

Добавление аутентификации

Чтобы добавить аутентификацию в свой API, вам необходимо выполнить некоторую конфигурацию. Перейдите на портал разработчиков Okta и зарегистрируйте учетную запись разработчика, чтобы вы могли создать приложение для своего интерфейса. Для этого перейдите в « Приложения»> «Приложение» на боковой панели и выберите « Создать интеграцию приложений». Выберите OIDC в качестве метода входа и установите тип приложения как «Одностраничное приложение».

На следующей странице дайте интеграции вашего приложения имя, например «Laravel GraphQL Demo», оставьте тип предоставления как «Код авторизации» и измените URI перенаправления при входе и выходе из системы на порт 3000 вместо 8080. Для « Контролируемый доступ » выберите разрешить доступ всем в моей организации, поскольку уровни доступа не важны для этого руководства.

Вам будет предоставлен ваш идентификатор клиента - обязательно запишите его. Вы сможете увидеть свой домен Okta, что вам также следует отметить. Прежде чем покинуть сайт Okta, перейдите в раздел Безопасность> API, чтобы увидеть URI вашего эмитента. Обратите внимание на это и перейдите на вкладку « Надежное происхождение ». Щелкните Добавить источник, установите URL-адрес источника как http://localhost:3000, установите флажки CORS и Перенаправить и нажмите Сохранить. Это позволит избежать проблем с интерфейсом.

Установка пакетов

Вернитесь к своему терминалу и запустите sail composer require okta/jwt-verifier firebase/php-jwt. Это установит пакеты, необходимые для проверки токенов доступа Okta. Затем запустите sail php artisan make:middleware VerifyJwt, чтобы создать новый класс для вашего промежуточного программного обеспечения. Откройте его и установите его содержимое следующим образом:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Okta\JwtVerifier\Adaptors\FirebasePhpJwt;
use Okta\JwtVerifier\JwtVerifierBuilder;

class VerifyJwt
{
   /**
    * Handle an incoming request.
    *
    * @param  \Illuminate\Http\Request  $request
    * @param  \Closure  $next
    * @return mixed
    */
   public function handle(Request $request, Closure $next)
   {
       $jwtVerifier = (new JwtVerifierBuilder())
           ->setAdaptor(new FirebasePhpJwt())
           ->setAudience('api://default')
           ->setClientId(env('OKTA_CLIENT_ID'))
           ->setIssuer(env('OKTA_ISSUER_URI'))
           ->build();

       try {
           $jwtVerifier->verify($request->bearerToken());
           return $next($request);
       } catch (\Exception $exception) {
           Log::error($exception);
       }

       return response('Unauthorized', 401);

   }
}

Как только он будет присоединен к конфигурации промежуточного программного обеспечения Lighthouse, он позволит вам защитить свой GraphQL API от запросов, у которых нет действительных токенов. Идентификатор клиента и эмитент берутся из переменных среды, которые необходимо установить в вашем файле .env. Откройте этот файл и добавьте следующее:

OKTA_CLIENT_ID=<the client ID you noted earlier>
OKTA_ISSUER_URI=< the issuer URI you noted earlier>

Откройте config/lighthouse.php и обновите массив 'middleware', чтобы добавить промежуточное ПО в Lighthouse:

...
'middleware' => [
   // Verify BearerToken from Okta
   \App\Http\Middleware\VerifyJwt::class,

   \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,

   // Logs in a user if they are authenticated. In contrast to Laravel's 'auth'
   // middleware, this delegates auth and permission checks to the field level.
   \Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class,

   // Logs every incoming GraphQL query.
   // \Nuwave\Lighthouse\Support\Http\Middleware\LogGraphQLQueries::class,
],
...

Вместо этого вы можете добавить проверку JWT к API route Guard, но описанный выше метод подходит для этого руководства. У graphql-playground не должно быть доступа к вашему API, потому что у него нет токена. Чтобы получить токен, вы настроите простое интерфейсное приложение для входа в Okta, а затем используете этот токен для вызова вашего API.

Если вы просто хотите убедиться, что ваш API работает, вы можете клонировать интерфейс из общедоступного репозитория GitHub. Вам нужно будет вставить свой идентификатор клиента и URL-адрес эмитента в файл App.js, но он должен работать по умолчанию. Чтобы создать интерфейс, читайте дальше.

Создание интерфейса

Поскольку это сделано для целей тестирования, вы создадите простой интерфейс с React, официальной библиотекой Okta React и Apollo Client.

Если у вас нет Node.js и npm, вы можете начать работу с nvm - диспетчером версий узлов.

Из родительского каталога (который содержит каталог вашего проекта Laravel) запустите npx create-react-app graphql-demo-frontend.

Это создаст минимальное приложение React для вашего внешнего интерфейса. Чтобы установить зависимости, запустите npm install @apollo/client graphql @okta/okta-react @okta/okta-auth-js react-router-dom@^5.1.6.

У приложения React будут файлы в каталоге src/, но большинство из них не нужны. Удалите их все и вместо этого создайте следующие файлы.

index.js

Файл index.js монтирует ваше приложение React на его корневой узел DOM, но он также обрабатывает создание клиента Apollo и извлекает Okta JWT из локального хранилища.

Примечание. Использование локального хранилища подходит для этого руководства, но не применяйте этот подход в производственной среде, поскольку изменения в базовых библиотеках и способах их хранения токена могут привести к его поломке.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { ApolloClient, InMemoryCache, ApolloProvider, from, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const oktaTokenStorage = JSON.parse(localStorage.getItem('okta-token-storage'));
const accessToken = oktaTokenStorage?.accessToken?.accessToken;

const httpLink = createHttpLink({
  uri: 'http://localhost/graphql',
});

// inject the access token into the Apollo Client
const authLink = setContext((_, { headers }) => {
  const token = accessToken;
  return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : "",
        }
  }
});

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: from([authLink, httpLink])
});

ReactDOM.render(
  <React.StrictMode>
        <ApolloProvider client={client}>
          <App />
        </ApolloProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

App.js

В этом файле находится основная часть приложения и настраивается клиент Okta. Используйте здесь URI вашего эмитента и идентификатор клиента:

import React from 'react';
import { SecureRoute, Security, LoginCallback } from '@okta/okta-react';
import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js';
import { BrowserRouter as Router, Route, useHistory } from 'react-router-dom';
import Home from './Home';
import IssueTracker from './IssueTracker';

const oktaAuth = new OktaAuth({
  issuer: <your issuer URI>, // issuer URL
  clientId: <your client ID>, // client id for SPA app
  redirectUri: window.location.origin + '/login/callback'
});

const App = () => {
  const history = useHistory();
  const restoreOriginalUri = async (_oktaAuth, originalUri) => {
      history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
  };

  return (
    <Security oktaAuth={oktaAuth} restoreOriginalUri={restoreOriginalUri}>
      <Route path='/' exact={true} component={Home} />
      <SecureRoute path='/issue-tracker' component={IssueTracker} />
      <Route path='/login/callback' component={LoginCallback} />
    </Security>
  );
};

const AppWithRouterAccess = () => (
  <Router>
        <App />
  </Router>
);

export default AppWithRouterAccess;

Home.js

Этот компонент действует как панель мониторинга, предлагая пользователю войти в систему через Okta, если они не прошли проверку подлинности, и предоставляя им ссылку на отслеживание проблем, если они:

import { useOktaAuth } from "@okta/okta-react";

const Home = () => {
    const { oktaAuth, authState } = useOktaAuth();

    const login = async () => oktaAuth.signInWithRedirect();
    const logout = async () => oktaAuth.signOut('/');

    if (!authState) {
        return <div>Loading...</div>;
    }

    if (!authState.isAuthenticated) {
        return (
            <div>
                <p>Not Logged in yet</p>
                <button onClick={login}>Login</button>
            </div>
        );
    }

    return (
        <div>
            <p>Logged in!</p>
            <p>
                <a href="/issue-tracker">go to Issue Tracker</a>
            </p>
            <button onClick={logout}>Logout</button>
        </div>
    );
};

export default Home;

IssueTracker.js

Этот компонент использует запрос, который вы пробовали ранее на платформе graphql-play, для извлечения данных в желаемой форме. Поскольку токен доступа вводится в Apollo Client, интерфейс может запрашивать серверную часть, несмотря на установленное вами промежуточное программное обеспечение. Когда данные возвращаются, компонент отображает их в виде списка.

import * as React from 'react';

import { useQuery, gql } from "@apollo/client";

const ISSUES = gql`
query GetIssues {
  issues {
    id,
    title,
    description,
    author {
      name
    },
    assignee {
      name
    },
    comments {
      id,
      content,
      author {
        name
      }
    }
  }
}
`

export default function IssueTracker() {

    const { loading, error, data } = useQuery(ISSUES);
    
    console.log({ loading, error, data })
    
    if (loading) {
        return <div>loading...</div>
    }
    
    if (error) {
        return <div>
            <code>error.message</code>
        </div>
    }
    
    return <ul>
        {data.issues.map((issue) => {
            return <li>
                <div>
                    <p>title: {issue.title}</p>
                    <p>description: {issue.description}</p>
                    <p>author: {issue.author.name}</p>
                    <p>assignee: {issue.assignee.name}</p>
                </div>
            </li>
        })}
    </ul>
};

Запуск веб-интерфейса

Установив эти компоненты на свои места, запустите npm run start и интерфейс будет запущен в localhost:3000. Перейдите туда, и когда вы нажмете « Войти», вы увидите экран входа в Okta. Войдите в систему, используя учетные данные своей учетной записи разработчика Okta, и вы будете перенаправлены в компонент Home. Вы должны увидеть ссылку, чтобы перейти к «системе отслеживания проблем». Щелчок по этой ссылке покажет вам страницу, заполненную данными из вашего GraphQL API.

Погрузитесь глубже с Okta

Теперь у вас должен быть Laravel GraphQL API, защищенный Okta, который, как вы видели, легко интегрируется с вашими проектами и может обеспечить безопасную расширяемую аутентификацию для ваших приложений с минимальной конфигурацией.

Однако Laravel, React и GraphQL - это еще не все, что Okta может вам предложить. Ознакомьтесь с огромным списком поддерживаемых интеграций, и вы обязательно найдете то, что соответствует вашим потребностям.

Чтобы просмотреть весь код в этом руководстве, проверьте GitHub здесь для бэкэнда и здесь для внешнего интерфейса.

Источник:

#JavaScript #PHP #Laravel
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В подарок 100$ на счет при регистрации

Получить