If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
React is a component-based JavaScript library built by Facebook. With React, we can build complex web applications with ease. In this article, weβre going to make Spring Security work together with a React Login page.
Weβll take advantage of the existing Spring Security configurations of previous examples. So, weβll build on top of a previous article about creating a Form Login with Spring Security.
2. Set up React
First, letβs use the command-line tool create-react-app to create an application by executing the command βcreate-react-app reactβ.
Weβll have a configuration like the following 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"
}
}
Then, weβll use the frontend-maven-plugin to help build our React project with 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>
The latest version of the plugin can be found here.
When we run mvn compile, this plugin will download node and npm, install all node module dependencies and build the react project for us.
There are several configuration properties we need to explain here. We specified the versions of node and npm, so that the plugin will know which version to download.
Our React login page will serve as a static page in Spring, so we use βsrc/main/webapp/WEB-INF/view/reactβ as npmβs working directory.
3. Spring Security Configuration
Before we dive into the React components, we update the Spring configuration to serve the static resources of our React app:
@EnableWebMvc
@Configuration
public class MvcConfig extends WebMvcConfigurer {
@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 instead of a dynamically served JSP.
Next, we update the Spring Security configuration to allow access to these static resources.
Instead of using βlogin.jspβ as we did in the previous form login article, here we use βindex.htmlβ as our Login page:
@Configuration
@EnableWebSecurity
@Profile("!https")
public class SecSecurityConfig {
//...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeHttpRequests(request -> request.requestMatchers("/admin/**")
.hasRole("ADMIN")
.requestMatchers("/anonymous*")
.anonymous()
.requestMatchers(HttpMethod.GET, "/index*", "/static/**", "/*.js", "/*.json", "/*.ico", "/rest")
.permitAll()
.anyRequest()
.authenticated())
.formLogin(form -> form.loginPage("/index.html")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/homepage.html", true)
.failureUrl("/index.html?error=true"))
.logout(logout -> logout.logoutUrl("/perform_logout")
.deleteCookies("JSESSIONID"))
.build();
}
}
As we can see from the snippet above, when we post form data to β/perform_loginβ, Spring will redirect us to β/homepage.htmlβ if the credentials match successfully and to β/index.html?error=trueβ otherwise.
4. React Components
Now, letβs get our hands dirty on React. Weβll build and manage a form login using components.
Note that weβll use ES6 (ECMAScript 2015) syntax to build our application.
4.1. Input
Letβs start with an Input component that backs the <input /> elements of the login form 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 {...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
As seen above, we wrap the <input /> element into a React controlled component to be able to manage its state and perform field validation.
React provides a way to validate the types using PropTypes. Specifically, we use Input.propTypes = {β¦} to validate the type of properties passed in by the user.
Note that PropType validation works for development only. PropType validation is to check that all the assumptions that weβre making about our components are being met.
Itβs better to have it rather than getting surprised by random hiccups in production.
4.2. Form
Next, weβll build a generic Form component in the file Form.js that combines multiple instances of our Input component on which we can base our login form.
In the Form component, we take attributes of HTML <input/> elements and create Input components from them.
Then the Input components and validation error messages are inserted into the Form:
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
Now letβs take a look at how we manage field validation errors and login error:
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>
}
}
//...
}
In this snippet, we define the handleError function to manage the error state of the form. Recall that we also used it for Input field validation. Actually, handleError() is passed to the Input Components as a callback in the render() function.
We use renderError() to construct the error message element. Note that Formβs constructor consumes an error property. This property indicates if the login action fails.
Then comes the form submission handler:
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))
}
}
}
We wrap all form fields into FormData and send it to the server using the fetch API.
Letβs not forget our login form comes with a successUrl and failureUrl, meaning that no matter if the request is successful or not, the response would require a redirection.
Thatβs why we need to handle redirection in the response callback.
4.3. Form Rendering
Now that weβve set up all the components we need, we can continue to put them in the DOM. The basic HTML structure is as follows (find it under react/public/index.html):
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<div id="root">
<div id="container"></div>
</div>
</body>
</html>
Finally, weβll render the Form into the <div/> with 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(
<Form {...props} error={params.get('error')} />,
document.getElementById('container'))
So our form now contains two input fields: username and password, and a submit button.
Here we pass an additional error attribute to the Form component because we want to handle login error after redirection to the failure URL: /index.html?error=true.
π form login errorNow weβve finished building a Spring Security login application using React. The last thing we need to do is to run mvn compile.
During the process, the Maven plugin will help build our React application and gather the build result in src/main/webapp/WEB-INF/view/react/build.
5. Conclusion
In this article, weβve covered how to build a React login app and let it interact with a Spring Security backend. A more complex application would involve state transition and routing using React Router or Redux, but thatβd be beyond the scope of this article.
