Статьи

Обильное развитие с помощью Spring Boot и React

«Я люблю писать код аутентификации и авторизации». Нет Java-разработчика. Надоело строить одни и те же экраны входа снова и снова? Попробуйте API Okta для размещенной аутентификации, авторизации и многофакторной аутентификации.

За последние пару лет React получил много положительных отзывов, что делает его привлекательным вариантом для разработчиков Java! Как только вы узнаете, как это работает, у вас появится много смысла, и с ним будет весело развиваться. Не только это, но это быстро злой! Если вы следили за мной или читали этот блог некоторое время, вы, возможно, помните мое руководство по Bootiful Development с Spring Boot и Angular . Сегодня я покажу вам, как создать такое же приложение, кроме как с React на этот раз. Прежде чем мы углубимся в это, давайте еще немного поговорим о том, для чего предназначен React, и почему я решил изучить его в этом посте.

Прежде всего, React не является полноценным веб-фреймворком. Это скорее инструментарий для разработки пользовательского интерфейса, а-ля GWT. Если вы хотите сделать HTTP-запрос для извлечения данных с сервера, React не предоставляет никаких утилит для этого. Тем не менее, он имеет огромную экосистему, которая предлагает множество библиотек и компонентов. Что я имею в виду под огромным? Скажем так: по данным npmjs.com , Angular имеет 17 938 пакетов . У React их почти в три раза больше — 42 428!

Angular мой хороший друг и был в течение долгого времени. Я не оставляю своего старого друга, чтобы принять React. Я просто заводил новых друзей. Для человека хорошо иметь много друзей с разным опытом и мнениями!

В этом посте показано, как вы можете создать пользовательский интерфейс и API как отдельные приложения. Вы узнаете, как создавать конечные точки REST с помощью Spring MVC, настраивать Spring Boot для разрешения CORS и создавать приложение React для отображения его данных. Это приложение покажет список пива из API, а затем принесет GIF из GIPHY, который соответствует имени пива. Я также покажу вам, как интегрировать Okta и ее поддержку OpenID Connect (OIDC), чтобы заблокировать ваш API и добавить аутентификацию в ваш пользовательский интерфейс.

Давайте начнем!

Создайте API с помощью Spring Boot

ПРИМЕЧАНИЕ. Приведенные ниже инструкции по созданию API Spring Boot такие же, как и в Bootiful Development с Spring Boot и Angular . Я скопировал их ниже для вашего удобства.

Чтобы начать работу с Spring Boot, перейдите к start.spring.io . В поле «Поиск зависимостей» выберите следующее:

  • H2 : база данных в памяти
  • JPA : стандартный ORM для Java
  • Остальные репозитории : Позволяет вам представить ваши репозитории JPA как конечные точки REST
  • Web : Spring MVC с Джексоном (для JSON), Hibernate Validator и встроенным Tomcat

Если вам больше нравится командная строка, вы можете использовать следующую команду для загрузки файла demo.zip с HTTPie .

http https://start.spring.io/starter.zip \
dependencies==h2,data-jpa,data-rest,web -d

Создайте каталог с именем spring-boot-react-example , внутри которого находится каталог server . Разверните содержимое demo.zip в каталог server .

Откройте проект «сервер» в вашей любимой IDE и запустите DemoApplication или запустите его из командной строки, используя ./mvnw spring-boot:run .

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

package com.example.demo.beer;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
 
@Entity
public class Beer {
 
    @Id
    @GeneratedValue
    private Long id;
    private String name;
 
    public Beer() {}
 
    public Beer(String name) {
        this.name = name;
    }
 
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public String toString() {
        return "Beer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Добавьте класс BeerRepository который использует Spring Data для выполнения CRUD на этом объекте.

package com.example.demo.beer;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
interface BeerRepository extends JpaRepository<Beer, Long> {
}

Добавьте BeerCommandLineRunner который использует этот репозиторий и создает набор данных по умолчанию.

package com.example.demo.beer;
 
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
 
import java.util.stream.Stream;
 
@Component
public class BeerCommandLineRunner implements CommandLineRunner {
 
    private final BeerRepository repository;
 
    public BeerCommandLineRunner(BeerRepository repository) {
        this.repository = repository;
    }
 
    @Override
    public void run(String... strings) throws Exception {
        // Top beers from https://www.beeradvocate.com/lists/top/
        Stream.of("Kentucky Brunch Brand Stout", "Good Morning", "Very Hazy", "King Julius",
                "Budweiser", "Coors Light", "PBR").forEach(name ->
                repository.save(new Beer(name))
        );
        repository.findAll().forEach(System.out::println);
    }
}

Перестройте свой проект, и вы увидите список пива, напечатанный в вашем терминале.

Добавьте аннотацию @RepositoryRestResource в BeerRepository чтобы представить все ее операции CRUD в качестве конечных точек REST.

import org.springframework.data.rest.core.annotation.RepositoryRestResource;
 
@RepositoryRestResource
interface BeerRepository extends JpaRepository<Beer, Long> {
}

Добавьте класс BeerController чтобы создать конечную точку, которая отфильтровывает не очень BeerController пиво.

package com.example.demo.beer;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
 
@RestController
public class BeerController {
    private BeerRepository repository;
 
    public BeerController(BeerRepository repository) {
        this.repository = repository;
    }
 
    @GetMapping("/good-beers")
    public Collection<Beer> goodBeers() {
        return repository.findAll().stream()
                .filter(this::isGreat)
                .collect(Collectors.toList());
    }
 
    private boolean isGreat(Beer beer) {
        return !beer.getName().equals("Budweiser") &&
                !beer.getName().equals("Coors Light") &&
                !beer.getName().equals("PBR");
    }
}

Перестройте свое приложение и перейдите по http://localhost:8080/good-beers . Вы должны увидеть список хорошего пива в вашем браузере.

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

http localhost:8080/good-beers

Создать проект с приложением Create React

Создание API кажется легкой частью в наши дни, во многом благодаря Spring Boot. В этом разделе я надеюсь показать вам, что создание пользовательского интерфейса с помощью React также довольно просто. Если вы выполните следующие шаги, вы создадите новое приложение React, получите названия пива и изображения из API и создадите компоненты для отображения данных.

Чтобы создать проект React, убедитесь, что у вас установлены Node.js , Create React App и Yarn .

npm install -g [email protected]

В окне терминала перейдите в корень каталога spring-boot-react-example и выполните следующую команду. Эта команда создаст новое приложение React с поддержкой TypeScript.

create-react-app client --scripts-version=react-scripts-ts

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

До сих пор вы создали API Good-Beers и приложение React, но не создали UI для отображения списка сортов пива из вашего API. Для этого откройте client/src/App.tsx и добавьте метод componentDidMount() .

componentDidMount() {
  this.setState({isLoading: true});
 
    .then(response => response.json())
    .then(data => this.setState({beers: data, isLoading: false}));
}

Жизненный цикл компонента React будет вызывать метод componentDidMount() . Приведенный выше код использует fetch , современную замену XMLHttpRequest . Это поддерживается в большинстве браузеров в соответствии с caniuse.com .

Вы можете видеть, что он устанавливает состояние beers с данными ответа. Чтобы инициализировать состояние для этого компонента, вам необходимо переопределить конструктор.

constructor(props: any) {
  super(props);
 
  this.state = {
    beers: [],
    isLoading: false
  };
}

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

class App extends React.Component<{}, any> {
  constructor(props: any) {
    super(props);
 
    this.state = {
      beers: [],
      isLoading: false
    };
  }
  // componentDidMount() and render()
}

Измените метод render() чтобы иметь следующий JSX. JSX — это XML-подобный синтаксис Facebook, который отображает HTML через JavaScript.

render() {
  const {beers, isLoading} = this.state;
 
  if (isLoading) {
    return <p>Loading...</p>;
  }
 
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to React</h2>
      </div>
      <div>
        <h2>Beer List</h2>
        {beers.map((beer: any) =>
          <div key={beer.id}>
            {beer.name}
          </div>
        )}
      </div>
    </div>
  );
}

Если вы посмотрите на http://localhost:3000 в вашем браузере, вы увидите сообщение «Загрузка…». Если вы загляните в консоль вашего браузера, вы, скорее всего, увидите проблему с CORS.

Failed to load http://localhost:8080/good-beers: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access.

Чтобы решить эту проблему, вам нужно настроить Spring Boot, чтобы разрешить междоменный доступ с http://localhost:3000 .

Настройте CORS для Spring Boot

В проекте сервера откройте server/src/main/java/com/example/demo/beer/BeerController.java и добавьте аннотацию @CrossOrigin для включения совместного использования ресурсов между источниками (CORS) с клиента ( http://localhost:3000 ).

import org.springframework.web.bind.annotation.CrossOrigin;
...
    @GetMapping("/good-beers")
    @CrossOrigin(origins = "http://localhost:3000")
    public Collection goodBeers() {

После внесения этих изменений перезапустите сервер, обновите браузер, и вы сможете увидеть список пивных из вашего Spring Boot API.

Создать компонент BeerList

Чтобы упростить поддержку этого приложения, переместите App.tsx и рендеринг списка пива из App.tsx в его собственный компонент BeerList . Создайте src/BeerList.tsx и заполните его кодом из App.tsx .

import * as React from 'react';
 
class BeerList extends React.Component<{}, any> {
  constructor(props: any) {
    super(props);
 
    this.state = {
      beers: [],
      isLoading: false
    };
  }
 
  componentDidMount() {
    this.setState({isLoading: true});
 
      .then(response => response.json())
      .then(data => this.setState({beers: data, isLoading: false}));
  }
 
  render() {
    const {beers, isLoading} = this.state;
 
    if (isLoading) {
      return <p>Loading...</p>;
    }
 
    return (
      <div>
        <h2>Beer List</h2>
        {beers.map((beer: any) =>
          <div key={beer.id}>
            {beer.name}
          </div>
        )}
      </div>
    );
  }
}
 
export default BeerList;

Затем измените client/src/App.tsx чтобы он содержал только оболочку и ссылку на <BeerList/> .

import * as React from 'react';
import './App.css';
import BeerList from './BeerList';
 
const logo = require('./logo.svg');
 
class App extends React.Component<{}, any> {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo"/>
          <h2>Welcome to React</h2>
        </div>
        <BeerList/>
      </div>
    );
  }
}
 
export default App;

Создать компонент GiphyImage

Чтобы он выглядел немного лучше, добавьте компонент GIPHY для получения изображений на основе имени пива. Создайте client/src/GiphyImage.tsx и поместите в него следующий код.

import * as React from 'react';
 
interface GiphyImageProps {
  name: string;
}
 
class GiphyImage extends React.Component<GiphyImageProps, any> {
  constructor(props: GiphyImageProps) {
    super(props);
 
    this.state = {
      giphyUrl: '',
      isLoading: false
    };
  }
 
  componentDidMount() {
    const giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q=';
 
    fetch(giphyApi + this.props.name)
      .then(response => response.json())
      .then(response => {
        if (response.data.length > 0) {
          this.setState({giphyUrl: response.data[0].images.original.url});
        } else {
          // dancing cat for no images found
          this.setState({giphyUrl: '//media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'});
        }
        this.setState({isLoading: false});
      });
  }
 
  render() {
    const {giphyUrl, isLoading} = this.state;
 
    if (isLoading) {
      return <p>Loading image...</p>;
    }
 
    return (
      <img src={giphyUrl} alt={this.props.name} width="200"/>
    );
  }
}
 
export default GiphyImage;

Измените метод render() в BeerList.tsx чтобы использовать этот компонент.

import GiphyImage from './GiphyImage';
...
render() {
  const {beers, isLoading} = this.state;
 
  if (isLoading) {
    return <p>Loading...</p>;
  }
 
  return (
    <div>
      <h2>Beer List</h2>
      {beers.map((beer: any) =>
        <div key={beer.id}>
          {beer.name}<br/>
          <GiphyImage name={beer.name}/>
        </div>
      )}
    </div>
  );
}

Результат должен выглядеть примерно как следующий список названий пива с изображениями.

Вы только что создали приложение React, которое взаимодействует с Spring Boot API с использованием междоменных запросов. Поздравляем!

Добавить поддержку PWA

Create React App имеет встроенную поддержку прогрессивных веб-приложений (PWA). Чтобы узнать, как он интегрирован, откройте client/README.md и client/README.md поиск «Создание прогрессивного веб-приложения».

Чтобы увидеть, как это работает, запустите yarn build в client каталоге. После завершения этой команды вы увидите сообщение, подобное следующему.

The build folder is ready to be deployed.
You may serve it with a static server:
 
  yarn global add serve
  serve -s build

Запустите предложенные команды, и вы сможете открыть браузер для просмотра http://localhost:5000 . Ваш браузер, скорее всего, покажет ошибку CORS на своей консоли, поэтому снова BeerController.java и настройте его разрешенные источники, чтобы разрешить порт 5000.

@CrossOrigin(origins = {"http://localhost:3000", "http://localhost:5000"})

Перезагрузите сервер, и http://localhost:5000 должен загрузить названия пива и изображения.

Я провел аудит Lighthouse в Chrome и обнаружил, что на данный момент это приложение набирает только 73/100 баллов.

Вы заметите на скриншоте выше, что «Манифест не имеет иконок по крайней мере 512 пикселей». Это звучит достаточно легко исправить. Вы можете скачать 512-пиксельный бесплатный значок пива с этой страницы .

ПРИМЕЧАНИЕ. Этот значок сделан Freepik с www.flaticon.com . Лицензировано CC 3.0 BY .

Скопируйте загруженный beer.png в client/public . Измените файл client/public/manifest.json чтобы иметь имя, специфичное для этого приложения, и добавить 512-пиксельный значок.

{
  "short_name": "Beer",
  "name": "Good Beer",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "beer.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

После внесения этих изменений я смог набрать 82 очка Lighthouse для PWA. Самая заметная жалоба из этого отчета заключалась в том, что я не использовал HTTPS. Чтобы увидеть, как приложение получит оценку при использовании HTTPS, я развернул его в Pivotal Cloud Foundry и Heroku . Я был накачан, чтобы обнаружить, что он набрал ? на обеих платформах

Чтобы прочитать сценарии, которые я использовал для развертывания всего, см. cloudfoundry.sh и heroku.sh в репозитории GitHub этой статьи. Я благодарен @starbuxman и @codefinger за помощь в их создании!

Добавить аутентификацию с Okta

Вы можете подумать: «Это довольно круто, легко понять, почему люди влюбляются в React». Есть еще один инструмент, в который вы можете влюбиться после того, как попробуете его: аутентификация с Okta! Почему окта? Потому что вы можете получить 7000 активных ежемесячных пользователей бесплатно ! Это стоит попробовать, особенно если вы видите, как легко добавить аутентификацию в Spring Boot и взаимодействовать с Okta.

Okta Spring Boot Starter

Чтобы заблокировать сервер, вы можете использовать Okta’s Spring Boot Starter . Чтобы интегрировать этот стартер, добавьте следующую зависимость в server/pom.xml :

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>0.2.0</version>
</dependency>

Вам также необходимо добавить раздел <dependencyManagement> чтобы обновить поддержку OAuth в Spring Security.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
    </dependencies>
</dependencyManagement>

ПРИМЕЧАНИЕ. Существует проблема со стартером Spring Boot от Okta, когда он не работает с DevTools Spring Boot.

Теперь вам нужно настроить сервер на использование Okta для аутентификации. Для этого вам нужно будет создать приложение OIDC в ​​Okta.

Создать приложение OIDC в ​​Okta

Войдите в свою учетную запись Okta Developer (или зарегистрируйтесь, если у вас нет учетной записи) и выберите Приложения > Добавить приложение . Нажмите « Одностраничное приложение» , нажмите « Далее» и присвойте приложению имя, которое вы запомните. Измените все экземпляры localhost:8080 на localhost:3000 и нажмите Готово .

Скопируйте идентификатор клиента в файл server/src/main/resources/application.properties . Пока вы там, добавьте свойство okta.oauth2.issuer , соответствующее вашему домену Okta. Например:

okta.oauth2.issuer=https://{yourOktaDomain}.com/oauth2/default
okta.oauth2.clientId={clientId}

ПРИМЕЧАНИЕ . Значение { yourOktaDomain } должно быть примерно таким: dev-123456.oktapreview.com . Убедитесь, что вы не включили -admin в значение!

Обновите server/src/main/java/com/okta/developer/demo/DemoApplication.java чтобы включить его в качестве сервера ресурсов.

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
 
@EnableResourceServer
@SpringBootApplication

После внесения этих изменений вы сможете перезагрузить сервер и увидеть, что доступ запрещен при попытке перейти по адресу http: // localhost: 8080 .

Реактивная поддержка Okta

React SDK от Okta позволяет интегрировать OIDC в ​​приложение React. Вы можете узнать больше о Okta React SDK на сайте npmjs.com . Для установки выполните следующие команды:

yarn add @okta/okta-react react-router-dom
yarn add -D @types/react-router-dom

React SDK от Okta зависит от реакции-роутера , поэтому причина для установки react-router-dom . Настройка маршрутизации в client/src/App.tsx является обычной практикой, поэтому замените ее код на TypeScript ниже, который устанавливает аутентификацию с Okta.

import * as React from 'react';
import './App.css';
import Home from './Home';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Security, ImplicitCallback } from '@okta/okta-react';
 
const config = {
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  redirectUri: window.location.origin + '/implicit/callback',
  clientId: '{clientId}'
};
 
export interface Auth {
  login(): {};
  logout(): {};
  isAuthenticated(): boolean;
  getAccessToken(): string;
}
 
class App extends React.Component {
 
  render() {
    return (
      <Router>
        <Security
          issuer={config.issuer}
          client_id={config.clientId}
          redirect_uri={config.redirectUri}
        >
          <Route path="/" exact={true} component={Home}/>
          <Route path="/implicit/callback" component={ImplicitCallback}/>
        </Security>
      </Router>
    );
  }
}
 
export default App;

Создайте client/src/Home.tsx чтобы он содержал оболочку приложения, App.tsx ранее содержала App.tsx . Этот класс отображает оболочку приложения, а также кнопки входа / выхода из системы и <BeerList/> если вы аутентифицированы.

import * as React from 'react';
import './App.css';
import BeerList from './BeerList';
import { withAuth } from '@okta/okta-react';
import { Auth } from './App';
 
const logo = require('./logo.svg');
 
interface HomeProps {
  auth: Auth;
}
 
interface HomeState {
  authenticated: boolean;
}
 
export default withAuth(class Home extends React.Component<HomeProps, HomeState> {
  constructor(props: HomeProps) {
    super(props);
    this.state = {authenticated: false};
    this.checkAuthentication = this.checkAuthentication.bind(this);
    this.checkAuthentication();
  }
 
  async checkAuthentication() {
    const isAuthenticated = await this.props.auth.isAuthenticated();
    const {authenticated} = this.state;
    if (isAuthenticated !== authenticated) {
      this.setState({authenticated: isAuthenticated});
    }
  }
 
  componentDidUpdate() {
    this.checkAuthentication();
  }
 
  render() {
    const {authenticated} = this.state;
    let body = null;
    if (authenticated) {
      body = (
        <div className="Buttons">
          <button onClick={this.props.auth.logout}>Logout</button>
          <BeerList auth={this.props.auth}/>
        </div>
      );
    } else {
      body = (
        <div className="Buttons">
          <button onClick={this.props.auth.login}>Login</button>
        </div>
      );
    }
 
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo"/>
          <h2>Welcome to React</h2>
        </div>
        {body}
      </div>
    );
  }
});

Если вы посмотрите на свое приложение React в браузере, вы, вероятно, увидите ошибку, подобную следующей:

./src/Home.tsx
(4,26): error TS7016: Could not find a declaration file for module '@okta/okta-react'.
'/Users/mraible/spring-boot-react-example/client/node_modules/@okta/okta-react/dist/index.js'
implicitly has an 'any' type.
 Try `npm install @types/@okta/okta-react` if it exists or add a new declaration (.d.ts) file
 containing `declare module '@okta/okta-react';`

Создайте client/src/okta.d.ts со следующей декларацией для решения этой проблемы.

declare module '@okta/okta-react';

Перезапустите клиент, и вы увидите, что над компонентом BeerList нужно проделать определенную BeerList .

./src/Home.tsx
(44,21): error TS2339: Property 'auth' does not exist on type 'IntrinsicAttributes &
IntrinsicClassAttributes<BeerList> & Readonly<{ children?: ReactNode; }> & ...'.

В client/src/BeerList.tsx добавьте свойство auth в реквизиты, создав интерфейс BeerListProps который передается в сигнатуру класса.

import { Auth } from './App';
 
interface BeerListProps {
  auth: Auth;
}
 
interface BeerListState {
  beers: Array<{}>;
  isLoading: boolean;
}
 
class BeerList extends React.Component<BeerListProps, BeerListState> {
  ...
}

Добавьте следующие правила CSS в client/src/App.css чтобы сделать кнопки входа / выхода из системы немного более заметными.

.Buttons {
  margin-top: 10px;
}
 
.Buttons button {
  font-size: 1em;
}

Ваш браузер shoa, проверьте следующее повторно.

Когда вы нажимаете кнопку, чтобы войти, введите адрес электронной почты и пароль, которые вы использовали для создания учетной записи Okta Developer. Когда он перенаправляет вас обратно в ваше приложение, вы, скорее всего, увидите «Загрузка…» и ошибку CORS в консоли вашего браузера.

Эта ошибка возникает из-за того, что @CrossOrigin не @CrossOrigin Spring Security. Чтобы решить эту проблему, добавьте simpleCorsFilter компонент simpleCorsFilter в тело DemoApplication.java .

package com.example.demo;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
 
import java.util.Arrays;
import java.util.Collections;
 
@EnableResourceServer
@SpringBootApplication
public class DemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
 
    @Bean
    public FilterRegistrationBean simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:5000"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

Чтобы все это работало на клиенте, измените метод componentDidMount() в client/src/BeerList.tsx чтобы установить заголовок авторизации.

async componentDidMount() {
  this.setState({isLoading: true});
 
  try {
    const response = await fetch('http://localhost:8080/good-beers', {
      headers: {
        Authorization: 'Bearer ' + await this.props.auth.getAccessToken()
    }
  });
    const data = await response.json();
    this.setState({beers: data, isLoading: false});
  } catch (err) {
    this.setState({error: err});
  }
}

Вам также необходимо добавить error в интерфейсе BeerListState .

interface BeerListState {
  beers: Array<{}>;
  isLoading: boolean;
  error: string;
}

Измените конструктор, чтобы он инициализировал error пустой строкой.

this.state = {
  beers: [],
  isLoading: false,
  error: ''
};

Затем измените метод render() чтобы при возникновении ошибки отображалась ошибка.

render() {
  const {beers, isLoading, error} = this.state;
 
  if (isLoading) {
    return <p>Loading ...</p>;
  }
 
  if (error.length > 0) {
    return <p>Error: {error}</p>;
  }
 
  return (...)
}

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

Если это работает, поздравляю!

Очистить эти предупреждения TypeScript

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

./src/BeerList.tsx
[16, 22]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.
[52, 27]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.
./src/GiphyImage.tsx
[7, 59]: Type declaration of 'any' loses type-safety. Consider replacing it with a more precise
type, the empty type ('{}'), or suppress this occurrence.

Чтобы исправить первую проблему, измените client/src/BeerList.tsx чтобы его конструктор client/src/BeerList.tsx следующим образом:

constructor(props: BeerListProps) {
  ...
}

Для второго вопроса создайте интерфейс Beer в client/src/BeerList.tsx . Поместите это рядом с другими интерфейсами наверху.

interface Beer {
  id: number;
  name: string;
}

Затем измените { beers.map((beer: any) => на { beers.map((beer: Beer) => .

Третья проблема может быть решена путем создания нового интерфейса GiphyImageState в client/src/GiphyImage.tsx для определения свойств состояния.

interface GiphyImageState {
  giphyUrl: string;
  isLoading: boolean;
}
 
class GiphyImage extends React.Component<GiphyImageProps, GiphyImageState> {
  ...
}

После внесения этих изменений вы должны избавиться от предупреждений TypeScript.

Узнайте больше о Spring Boot и React

Чтобы узнать больше о React, Spring Boot или Okta, ознакомьтесь со следующими ресурсами:

Вы можете найти исходный код, связанный с этой статьей, на GitHub . Основной пример (без аутентификации) находится в master ветви, а интеграция Okta — в ветви okta . Чтобы проверить ветку Okta на вашем локальном компьютере, выполните следующие команды.

git clone [email protected]:oktadeveloper/spring-boot-react-example.git
git checkout okta

Если вы обнаружите какие-либо проблемы, пожалуйста, добавьте комментарий ниже, и я сделаю все возможное, чтобы помочь. Если вам понравился этот урок, я бы хотел, чтобы вы следили за мной в Twitter . Чтобы получать уведомления о других статьях, подобных этой, следите за @oktadev .

«Я люблю писать код аутентификации и авторизации». Нет Java-разработчика. Надоело строить одни и те же экраны входа снова и снова? Попробуйте API Okta для размещенной аутентификации, авторизации и многофакторной аутентификации.