Page de connexion Spring Security avec React

Page de connexion Spring Security avec React

1. Vue d'ensemble

React est une bibliothèque JavaScript basée sur des composants créée par Facebook. Avec React, nous pouvons créer facilement des applications Web complexes. Dans cet article, nous allons faire fonctionner Spring Security avec une page de connexion React.

Nous tirerons parti des configurations Spring Security existantes des exemples précédents. Nous allons donc nous appuyer sur un article précédent sur la création d'unForm Login with Spring Security.

2. Configurer Réagir

Commençons paruse the command-line tool create-react-app to create an application en exécutant la commande «create-react-app react”.

Nous aurons une configuration comme la suivante dansreact/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"
    }
}

Ensuite, nous allonsuse the frontend-maven-plugin to help build our React project with Maven:


    com.github.eirslett
    frontend-maven-plugin
    1.6
    
        v8.11.3
        6.1.0
        src/main/webapp/WEB-INF/view/react
    
    
        
            install node and npm
            
                install-node-and-npm
            
        
        
            npm install
            
                npm
            
        
        
            npm run build
            
                npm
            
            
                run build
            
        
    

La dernière version du plugin peut être trouvéehere.

Lorsque nous exécutonsmvn compile, ce plugin téléchargeranode etnpm, installera toutes les dépendances du module de nœud et construira le projet react pour nous.

Il y a plusieurs propriétés de configuration que nous devons expliquer ici. Nous avons spécifié les versions denode etnpm, afin que le plugin sache quelle version télécharger.

Notre page de connexion React servira de page statique dans Spring, nous utilisons donc «src/main/webapp_ / WEB-INF / view / react_» comme répertoire de travail denpm.

3. Configuration de sécurité de printemps

Avant de plonger dans les composants React, nous mettons à jour la configuration de Spring afin de servir les ressources statiques de notre application 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");
    }
}

Note that we add the login page “index.html” as a static resource au lieu d'une JSP servie dynamiquement.

Ensuite, nous mettons à jour la configuration de Spring Security pour autoriser l'accès à ces ressources statiques.

Au lieu d'utiliser“login.jsp” comme nous l'avons fait dans l'articlethe previous form login, nous utilisons ici“index.html” comme pageLogin:

@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")
          //...
    }
}

Comme nous pouvons le voir dans l'extrait ci-dessus lorsque nous publions des données de formulaire dans «/perform_login», Spring nous redirigera vers «/homepage.html» si les informations d'identification correspondent avec succès et vers «/index.html?error=true» sinon.

4. Composants de réaction

Maintenant, mettons la main à la pâte sur React. Nous allons créer et gérer une connexion par formulaire à l'aide de composants.

Notez que nous utiliserons la syntaxe ES6 (ECMAScript 2015) pour créer notre application.

4.1. Contribution

Commençons par un composantInput qui sauvegarde les éléments<input /> du formulaire de connexion dansreact/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.propTypes = {
  name: PropTypes.string,
  placeholder: PropTypes.string,
  type: PropTypes.string,
  className: PropTypes.string,
  value: PropTypes.string,
  handleError: PropTypes.func
}

export default Input

Comme vu ci-dessus, nous enveloppons l'élément<input /> dans un composant contrôlé par React pour pouvoir gérer son état et effectuer une validation de champ.

React fournit un moyen de valider les types en utilisantPropTypes. Plus précisément, nous utilisonsInput.propTypes = \{…} pour valider le type de propriétés transmises par l'utilisateur.

Notez que la validation dePropType fonctionne uniquement pour le développement. La validation dePropType consiste à vérifier que toutes les hypothèses que nous formulons sur nos composants sont satisfaites.

Il vaut mieux l’avoir plutôt que de se faire surprendre par un hoquet aléatoire dans la production.

4.2. Form

Ensuite, nous allons créer un composant Form générique dans le fichierForm.js qui combine plusieurs instances de notre composantInput sur lequel nous pouvons baser notre formulaire de connexion.

Dans le composantForm, nous prenons les attributs des éléments HTML<input/> et créons des composantsInput à partir d'eux.

Ensuite, les composantsInput et les messages d'erreur de validation sont insérés dans lesForm:

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) => (
            
          )
        )
        const errors = this.renderError()
        return (
            
{this.form=fm}} > {inputs} {errors}
) } } Form.propTypes = { name: PropTypes.string, action: PropTypes.string, method: PropTypes.string, inputs: PropTypes.array, error: PropTypes.string } export default Form

Voyons maintenant comment nous gérons les erreurs de validation de champ et les erreurs de connexion:

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 
{errmsg}
} } //... }

Dans cet extrait, nous définissons la fonctionhandleError pour gérer l'état d'erreur du formulaire. Rappelons que nous l'avons également utilisé pour la validation du champInput. Actually, handleError() is passed to the Input Components as a callback in the render() function.

Nous utilisonsrenderError() pour construire l'élément de message d'erreur. Notez que le constructeurForm’s consomme une propriétéerror. Cette propriété indique si l'action de connexion échoue.

Vient ensuite le gestionnaire de soumission de formulaire:

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))
        }
    }
}

Nous enveloppons tous les champs du formulaire dansFormData et l'envoyons au serveur en utilisant lesfetch API.

N'oublions pas que notre formulaire de connexion est livré avec unsuccessUrl et unfailureUrl, ce qui signifie que peu importe si la demande aboutit ou non, la réponse nécessitera une redirection.

C’est pourquoi nous devons gérer la redirection dans le rappel de réponse.

4.3. Rendu de formulaire

Maintenant que nous avons configuré tous les composants dont nous avons besoin, nous pouvons continuer à les placer dans le DOM. La structure HTML de base est la suivante (trouvez-la sousreact/public/index.html):



  
    
  
  

    

Enfin, nous allons rendre le formulaire dans les<div/> avec l'ID "container” dansreact/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(
  
, document.getElementById('container'))

Notre formulaire contient donc maintenant deux champs de saisie:username etpassword, et un bouton d'envoi.

Ici, nous passons un attributerror supplémentaire au composantForm car nous voulons gérer l'erreur de connexion après la redirection vers l'URL d'échec:/index.html?error=true.

form login error

Nous avons maintenant terminé de créer une application de connexion Spring Security à l'aide de React. La dernière chose que nous devons faire est d'exécutermvn compile.

Pendant le processus, le plugin Maven aidera à construire notre application React et à rassembler le résultat de la construction ensrc/main/webapp/WEB-INF/view/react/build.

5. Conclusion

Dans cet article, nous avons expliqué comment créer une application de connexion React et la laisser interagir avec un backend Spring Security. Une application plus complexe impliquerait une transition d’état et un routage à l’aide deReact Router ouRedux, mais cela n'entre pas dans le cadre de cet article.

Comme toujours, l'implémentation complète peut être trouvéeover on Github. Pour l'exécuter localement, exécutezmvn jetty:run dans le dossier racine du projet, puis nous pouvons accéder à la page de connexion React àhttp://localhost:8080.