Pagina di login di Spring Security con React

1. Panoramica

React è una libreria JavaScript basata su componenti creata da Facebook. Con React, possiamo creare facilmente applicazioni web complesse. In questo articolo, faremo in modo che Spring Security funzioni insieme a una pagina di accesso React.

Approfitteremo delle configurazioni Spring Security esistenti degli esempi precedenti. Quindi ci baseremo su un precedente articolo sulla creazione di un modulo di accesso con Spring Security.

2. Configurare React

Innanzitutto, utilizziamo lo strumento da riga di comando create-react-app per creare un'applicazione eseguendo il comando " create-react-app react" .

Avremo una configurazione come la seguente in 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" } }

Quindi, useremo il plugin frontend-maven per aiutare a costruire il nostro progetto React con 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    

L'ultima versione del plugin può essere trovata qui.

Quando eseguiamo mvn compile , questo plugin scaricherà node e npm , installerà tutte le dipendenze del modulo node e costruirà il progetto react per noi.

Ci sono diverse proprietà di configurazione che dobbiamo spiegare qui. Abbiamo specificato le versioni di node e npm , in modo che il plugin sappia quale versione scaricare.

La nostra pagina di login di React servirà come pagina statica in primavera, quindi usiamo " src / main / webapp / WEB-INF / view / react " come directory di lavoro di npm .

3. Configurazione Spring Security

Prima di immergerci nei componenti React, aggiorniamo la configurazione Spring per servire le risorse statiche della nostra app 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"); } }

Si noti che aggiungiamo la pagina di accesso "index.html" come risorsa statica invece di un JSP servito dinamicamente.

Successivamente, aggiorniamo la configurazione di Spring Security per consentire l'accesso a queste risorse statiche.

Invece di utilizzare "login.jsp" come abbiamo fatto nel precedente articolo di login del modulo, qui usiamo "index.html" come nostra pagina di accesso :

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

Come possiamo vedere dallo snippet sopra quando pubblichiamo i dati del modulo su " / perform_login ", Spring ci reindirizzerà a " /homepage.html " se le credenziali corrispondono correttamente e a " /index.html?error=true " in caso contrario.

4. React Components

Ora sporchiamoci le mani su React. Costruiremo e gestiremo un modulo di accesso utilizzando i componenti.

Nota che useremo la sintassi ES6 (ECMAScript 2015) per creare la nostra applicazione.

4.1. Ingresso

Cominciamo con un componente Input che supporta il fileelementi del modulo di accesso in 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.propTypes = { name: PropTypes.string, placeholder: PropTypes.string, type: PropTypes.string, className: PropTypes.string, value: PropTypes.string, handleError: PropTypes.func } export default Input

Come visto sopra, avvolgiamo il file elemento in un componente controllato da React per essere in grado di gestirne lo stato ed eseguire la convalida sul campo.

React fornisce un modo per convalidare i tipi utilizzando PropTypes . In particolare, utilizziamo Input.propTypes = {…} per convalidare il tipo di proprietà passate dall'utente.

Nota che la convalida di PropType funziona solo per lo sviluppo. La convalida di PropType consiste nel verificare che tutte le ipotesi che stiamo facendo sui nostri componenti siano soddisfatte.

È meglio averlo piuttosto che essere sorpresi da singhiozzi casuali durante la produzione.

4.2. Modulo

Successivamente, creeremo un componente Form generico nel file Form.js che combina più istanze del nostro componente Input su cui possiamo basare il nostro modulo di accesso.

Nel componente Form , prendiamo attributi di HTMLelementi e creare componenti di input da essi.

Quindi i componenti di input e i messaggi di errore di convalida vengono inseriti nel modulo:

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

Ora diamo un'occhiata a come gestiamo gli errori di convalida dei campi e gli errori di accesso:

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

In questo frammento, definiamo la funzione handleError per gestire lo stato di errore del form. Ricorda che l'abbiamo usato anche per la convalida del campo di input . In realtà, handleError () viene passato ai componenti di input come callback nella funzione render () .

Usiamo renderError () per costruire l'elemento del messaggio di errore. Si noti che il costruttore di Form utilizza una proprietà di errore . Questa proprietà indica se l'azione di accesso non riesce.

Quindi arriva il gestore dell'invio del modulo:

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

Racchiudiamo tutti i campi del modulo in FormData e lo inviamo al server utilizzando l' API di recupero .

Non dimentichiamo che il nostro modulo di accesso viene fornito con successUrl e failureUrl , il che significa che indipendentemente dal fatto che la richiesta abbia successo o meno, la risposta richiederebbe un reindirizzamento.

Ecco perché dobbiamo gestire il reindirizzamento nel callback di risposta.

4.3. Rendering di moduli

Ora che abbiamo impostato tutti i componenti di cui abbiamo bisogno, possiamo continuare a inserirli nel DOM. La struttura HTML di base è la seguente (trovala in react / public / index.html ):

Infine, eseguiremo il rendering del modulo nel file con ID " container" in 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( , document.getElementById('container'))

Quindi il nostro modulo ora contiene due campi di input: nome utente e password e un pulsante di invio.

Qui passiamo un attributo di errore aggiuntivo al componente Form perché vogliamo gestire l'errore di accesso dopo il reindirizzamento all'URL di errore: /index.html?error=true .

Ora abbiamo finito di creare un'applicazione di accesso Spring Security usando React. L'ultima cosa che dobbiamo fare è eseguire mvn compile .

Durante il processo, il plugin Maven aiuterà a costruire la nostra applicazione React e raccoglierà il risultato della build in src / main / webapp / WEB-INF / view / react / build .

5. conclusione

In questo articolo, abbiamo spiegato come creare un'app di accesso React e lasciarla interagire con un backend Spring Security. Un'applicazione più complessa implicherebbe la transizione di stato e il routing utilizzando React Router o Redux, ma ciò andrebbe oltre lo scopo di questo articolo.

Come sempre, l'implementazione completa può essere trovata su GitHub. Per eseguirlo localmente, esegui mvn jetty: run nella cartella principale del progetto, quindi possiamo accedere alla pagina di login di React su // localhost: 8080 .