«1. Обзор

React — это основанная на компонентах библиотека JavaScript, созданная Facebook. С React мы можем легко создавать сложные веб-приложения. В этой статье мы собираемся заставить Spring Security работать вместе со страницей входа React.

Мы воспользуемся преимуществами существующих конфигураций Spring Security из предыдущих примеров. Итак, мы будем опираться на предыдущую статью о создании входа в форму с помощью Spring Security.

2. Настройте React

Во-первых, давайте воспользуемся инструментом командной строки create-react-app для создания приложения, выполнив команду «create-react-app react».

У нас будет следующая конфигурация в react/package.json:

{
    "name": "react",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
        "react": "^16.4.1",
        "react-dom": "^16.4.1",
        "react-scripts": "1.1.4"
    },
    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
    }
}

Затем мы будем использовать frontend-maven-plugin для создания нашего проекта React с помощью Maven:

<plugin>
    <groupId>com.github.eirslett</groupId>
    <artifactId>frontend-maven-plugin</artifactId>
    <version>1.6</version>
    <configuration>
        <nodeVersion>v8.11.3</nodeVersion>
        <npmVersion>6.1.0</npmVersion>
        <workingDirectory>src/main/webapp/WEB-INF/view/react</workingDirectory>
    </configuration>
    <executions>
        <execution>
            <id>install node and npm</id>
            <goals>
                <goal>install-node-and-npm</goal>
            </goals>
        </execution>
        <execution>
            <id>npm install</id>
            <goals>
                <goal>npm</goal>
            </goals>
        </execution>
        <execution>
            <id>npm run build</id>
            <goals>
                <goal>npm</goal>
            </goals>
            <configuration>
                <arguments>run build</arguments>
            </configuration>
        </execution>
    </executions>
</plugin>

~ ~~ Последнюю версию плагина можно найти здесь.

Когда мы запустим mvn compile, этот плагин загрузит node и npm, установит все зависимости модуля node и создаст для нас реактивный проект.

Здесь нужно объяснить несколько свойств конфигурации. Мы указали версии node и npm, чтобы плагин знал, какую версию скачивать.

Наша страница входа в React будет статической в ​​Spring, поэтому мы используем «src/main/webapp/WEB-INF/view/react» в качестве рабочего каталога npm.

3. Конфигурация безопасности Spring

Прежде чем мы углубимся в компоненты React, мы обновим конфигурацию Spring, чтобы она обслуживала статические ресурсы нашего приложения React:

@EnableWebMvc
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(
      ResourceHandlerRegistry registry) {
 
        registry.addResourceHandler("/static/**")
          .addResourceLocations("/WEB-INF/view/react/build/static/");
        registry.addResourceHandler("/*.js")
          .addResourceLocations("/WEB-INF/view/react/build/");
        registry.addResourceHandler("/*.json")
          .addResourceLocations("/WEB-INF/view/react/build/");
        registry.addResourceHandler("/*.ico")
          .addResourceLocations("/WEB-INF/view/react/build/");
        registry.addResourceHandler("/index.html")
          .addResourceLocations("/WEB-INF/view/react/build/index.html");
    }
}

Обратите внимание, что мы добавляем страницу входа «index» .html» в качестве статического ресурса вместо динамически обслуживаемого JSP.

Далее мы обновляем конфигурацию Spring Security, чтобы разрешить доступ к этим статическим ресурсам.

Вместо использования «login.jsp», как мы делали в предыдущей статье о входе в форму, здесь мы используем «index.html» в качестве нашей страницы входа:

@Configuration
@EnableWebSecurity
@Profile("!https")
public class SecSecurityConfig 
  extends WebSecurityConfigurerAdapter {

    //...

    @Override
    protected void configure(final HttpSecurity http) 
      throws Exception {
        http.csrf().disable().authorizeRequests()
          //...
          .antMatchers(
            HttpMethod.GET,
            "/index*", "/static/**", "/*.js", "/*.json", "/*.ico")
            .permitAll()
          .anyRequest().authenticated()
          .and()
          .formLogin().loginPage("/index.html")
          .loginProcessingUrl("/perform_login")
          .defaultSuccessUrl("/homepage.html",true)
          .failureUrl("/index.html?error=true")
          //...
    }
}

Как видно из фрагмента выше когда мы отправляем данные формы в «/perform_login», Spring перенаправляет нас на «/homepage.html», если учетные данные совпадают, и на «/index.html?error=true» в противном случае.

4. Компоненты React

Теперь давайте запачкаем руки React. Мы создадим форму входа и будем управлять ею с помощью компонентов.

Обратите внимание, что мы будем использовать синтаксис ES6 (ECMAScript 2015) для создания нашего приложения.

4.1. Input

Давайте начнем с компонента Input, который поддерживает элементы \u003cinput /\u003e формы входа в react/src/Input.js:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Input extends Component {
    constructor(props){
        super(props)
        this.state = {
            value: props.value? props.value : '',
            className: props.className? props.className : '',
            error: false
        }
    }

    //...

    render () {
        const {handleError, ...opts} = this.props
        this.handleError = handleError
        return (
          <input {...opts} value={this.state.value}
            onChange={this.inputChange} className={this.state.className} /> 
        )
    }
}

Input.propTypes = {
  name: PropTypes.string,
  placeholder: PropTypes.string,
  type: PropTypes.string,
  className: PropTypes.string,
  value: PropTypes.string,
  handleError: PropTypes.func
}

export default Input

Как показано выше, мы оборачиваем элемент \u003cinput /\u003e в компонент, контролируемый React, чтобы иметь возможность управлять своим состоянием и выполнять проверку полей.

React предоставляет способ проверки типов с помощью PropTypes. В частности, мы используем Input.propTypes = {…} для проверки типа свойств, переданных пользователем.

Обратите внимание, что проверка PropType работает только для разработки. Проверка PropType — это проверка того, что все предположения, которые мы делаем о наших компонентах, выполняются.

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

4.2. Form

Далее мы создадим общий компонент формы в файле Form.js, который объединяет несколько экземпляров нашего компонента ввода, на основе которого мы можем создать нашу форму входа.

В компоненте Form мы берем атрибуты элементов HTML \u003cinput/\u003e и создаем из них компоненты ввода.

Затем компоненты ввода и сообщения об ошибках проверки вставляются в форму:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Input from './Input'

class Form extends Component {

    //...

    render() {
        const inputs = this.props.inputs.map(
          ({name, placeholder, type, value, className}, index) => (
            <Input key={index} name={name} placeholder={placeholder} type={type} value={value}
              className={type==='submit'? className : ''} handleError={this.handleError} />
          )
        )
        const errors = this.renderError()
        return (
            <form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} >
              {inputs}
              {errors}
            </form>
        )
    }
}

Form.propTypes = {
  name: PropTypes.string,
  action: PropTypes.string,
  method: PropTypes.string,
  inputs: PropTypes.array,
  error: PropTypes.string
}

export default Form

Теперь давайте посмотрим, как мы справляемся с ошибками проверки полей и ошибками входа в систему:

class Form extends Component {

    constructor(props) {
        super(props)
        if(props.error) {
            this.state = {
              failure: 'wrong username or password!',
              errcount: 0
            }
        } else {
            this.state = { errcount: 0 }
        }
    }

    handleError = (field, errmsg) => {
        if(!field) return

        if(errmsg) {
            this.setState((prevState) => ({
                failure: '',
                errcount: prevState.errcount + 1, 
                errmsgs: {...prevState.errmsgs, [field]: errmsg}
            }))
        } else {
            this.setState((prevState) => ({
                failure: '',
                errcount: prevState.errcount===1? 0 : prevState.errcount-1,
                errmsgs: {...prevState.errmsgs, [field]: ''}
            }))
        }
    }

    renderError = () => {
        if(this.state.errcount || this.state.failure) {
            const errmsg = this.state.failure 
              || Object.values(this.state.errmsgs).find(v=>v)
            return <div className="error">{errmsg}</div>
        }
    }

    //...

}

В этом фрагменте мы определите функцию handleError для управления состоянием ошибки формы. Напомним, что мы также использовали его для проверки поля ввода. На самом деле, handleError() передается компонентам ввода как обратный вызов в функции render().

Мы используем renderError() для создания элемента сообщения об ошибке. Обратите внимание, что конструктор формы использует свойство ошибки. Это свойство указывает, происходит ли сбой при входе в систему.

Затем идет обработчик отправки формы:

class Form extends Component {

    //...

    handleSubmit = (event) => {
        event.preventDefault()
        if(!this.state.errcount) {
            const data = new FormData(this.form)
            fetch(this.form.action, {
              method: this.form.method,
              body: new URLSearchParams(data)
            })
            .then(v => {
                if(v.redirected) window.location = v.url
            })
            .catch(e => console.warn(e))
        }
    }
}

Мы оборачиваем все поля формы в FormData и отправляем их на сервер с помощью fetch API.

«Давайте не будем забывать, что наша форма входа поставляется с SuccessUrl и failureUrl, что означает, что независимо от того, успешен запрос или нет, ответ потребует перенаправления.

Вот почему нам нужно обрабатывать перенаправление в обратном вызове ответа.

4.3. Рендеринг формы

Теперь, когда мы настроили все необходимые нам компоненты, мы можем продолжить размещать их в DOM. Базовая структура HTML выглядит следующим образом (ее можно найти в разделе react/public/index.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
  </head>
  <body>

    <div id="root">
      <div id="container"></div>
    </div>

  </body>
</html>

Наконец, мы отрендерим форму в \u003cdiv/\u003e с идентификатором «container» в react/src. /index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import Form from './Form'

const inputs = [{
  name: "username",
  placeholder: "username",
  type: "text"
},{
  name: "password",
  placeholder: "password",
  type: "password"
},{
  type: "submit",
  value: "Submit",
  className: "btn" 
}]

const props = {
  name: 'loginForm',
  method: 'POST',
  action: '/perform_login',
  inputs: inputs
}

const params = new URLSearchParams(window.location.search)

ReactDOM.render(
  <Form {...props} error={params.get('error')} />,
  document.getElementById('container'))

Теперь наша форма содержит два поля ввода: имя пользователя и пароль, а также кнопку отправки.

Здесь мы передаем дополнительный атрибут ошибки компоненту формы, потому что мы хотим обработать ошибку входа после перенаправления на URL-адрес сбоя: /index.html?error=true.

form login error

Теперь мы закончили создание приложения для входа в Spring Security с использованием React. Последнее, что нам нужно сделать, это запустить mvn compile.

В процессе плагин Maven поможет собрать наше приложение React и собрать результат сборки в src/main/webapp/WEB-INF/view/react/build.

5. Заключение

В этой статье мы рассмотрели, как создать приложение для входа в React и позволить ему взаимодействовать с серверной частью Spring Security. Более сложное приложение будет включать переход состояний и маршрутизацию с использованием React Router или Redux, но это выходит за рамки этой статьи.

Как всегда, полную реализацию можно найти на GitHub. Чтобы запустить его локально, выполните mvn jetty:run в корневой папке проекта, после чего мы сможем получить доступ к странице входа в систему React по адресу http://localhost:8080.