In this Spring Boot tutorial, you will learn how to implement User Authentication(User Login) functionality for your RESTful Web Service application.
There is also a step-by-step video demonstration on how to do User Authentication available here.
The user authentication functionality we are going to implement in this tutorial will work the following way:
- A user sends HTTP Post Request with username and password to a /login web service endpoint,
- The Authentication Filter will trigger and will check if such user exits in our database,
- If authentication is successful, a new GWT token is generated and returned back as an HTTP Header in HTTP Response.
Create RESTful Web Service
I assume you already have a RESTful Web Service, but just in case you do not have one, here is a quick tutorial that shows how to build a very simple Web Service project with Spring Boot( Includes a video tutorial ).
POM.XML Dependencies
The source code in this tutorial will need the following dependencies for it to work.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.appsdeveloperblog.app.ws</groupId> <artifactId>mobile-app-ws</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>mobile-app-ws</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.1</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>org.modelmapper</groupId> <artifactId>modelmapper</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
JSON Payload
When a user sends HTTP POST Request to a /login URL they will need to include the following JSON Payload in the body of HTTP Post Request.
{ "email":"test2@test.com", "password":"12345678" }
Where:
- email – an email address, will be used as a username,
- password – a user password used when creating a user profile.
Here is an example of HTTP Post request to perform user login with CURL command:
CURL command
curl -X POST \ http://localhost:8080/users-ws/login \ -H 'Content-Type: application/json' \ -H 'cache-control: no-cache' \ -d '{ "email":"test2@test.com", "password":"12345678" }'
where
users-ws – is a name of my Spring Boot application. In your case, it will be different.
@EnableWebSecurity
To authenticate user with their username and password, we will user Spring Security. In the POM.xml file above, we have already added Spring Security dependency. Now we need to create a new Configuration class that will contain Spring Security configuration details.
package com.appsdeveloperblog.photoapp.api.users.security; import com.appsdeveloperblog.photoapp.api.users.service.UsersService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration @EnableWebSecurity public class WebSecurity{ private final UsersService usersService; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired public WebSecurity(UsersService usersService, BCryptPasswordEncoder bCryptPasswordEncoder) { this.usersService = usersService; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { // Configure AuthenticationManagerBuilder AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); // Get AuthenticationManager AuthenticationManager authenticationManager = authenticationManagerBuilder.build(); http.cors().and().csrf().disable(); http.authorizeHttpRequests() .requestMatchers(HttpMethod.POST, "/users") .permitAll() .anyRequest().authenticated() .and() .addFilter(getAuthenticationFilter(authenticationManager)) .authenticationManager(authenticationManager); http.headers().frameOptions().disable(); return http.build(); } protected AuthenticationFilter getAuthenticationFilter(AuthenticationManager authenticationManager) throws Exception { return new AuthenticationFilter(authenticationManager); } }
The above WebSecurity class depends on a couple of classes which we will need to create:
- UserService – a service class which will be used to load user details from a database, and
- AuthenticationFilter – a filter class that we will need to create. This custom filter class will be used to authenticate a user and to generate the JWT token.
Let’s create the custom AuthenticationFilter class first.
AuthenticationFilter class
package com.appsdeveloperblog.photoapp.api.users.security; import com.appsdeveloperblog.photoapp.api.users.service.UsersService; import com.appsdeveloperblog.photoapp.api.users.shared.UserDto; import com.appsdeveloperblog.photoapp.api.users.ui.model.LoginRequestModel; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final UsersService usersService; private final String TOKEN_SECRET="h4of9eh48vmg02nfu30v27yen295hfj65"; public AuthenticationFilter(AuthenticationManager authenticationManager, UsersService usersService) { this.usersService = usersService; super.setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { LoginRequestModel creds = new ObjectMapper() .readValue(req.getInputStream(), LoginRequestModel.class); return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( creds.getEmail(), creds.getPassword(), new ArrayList<>()) ); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { // Get User Details from Database String userName = ((User) auth.getPrincipal()).getUsername(); UserDto userDto = usersService.getUserByEmail(userName); // Generate GWT String token = Jwts.builder() .setSubject(userDto.getUserId()) .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong("3600000"))) .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET ) .compact(); res.addHeader("Token", token); res.addHeader("UserID", userDto.getUserId()); } }
The above AuthenticationFilter class will:
- Read user email and user password from HTTPServletRequest object. For JSON payload to be available to us in Java code, Spring Framework needs to be provided a Java class into which the JSON payload will be converted. The code of this Java class is below and it is called LoginRequestModel.
- Spring Framework will try to authenticate a user and if authentication is successful, a successfulAuthentication() method will be called by Spring Framework.
- Inside of successfulAuthentication() method, we will use our custom UsersService class to load user record by email address from a database. We need user details so that we can get the public userId value which we will then add to a Response HTTP Header and to a JWT token.
- We will then use JWTs builder to generate a new JWT token containing the value of public userId as a Subject.
- We will then set expiration time and sign the JWT token with a TOKEN_SECRET value which is just a unique alpha-numeric string of characters defined in the same class.
Once the successfulAuthentication() method completes successfully, the public UserId value and the JWT token will be added to a Response HTTP Header and a Response will be sent to a user. Try it.
For the above Authentication class to work, we will need to create a couple of more Java Classes: LoginRequestModel and UsersService class.
LoginRequestModel Class
This class is being used to convert the JSON payload containing a user email address and password into a Java object which is being used in Authentication Filter.
package com.appsdeveloperblog.app.ws.ui.model.request; public class LoginRequestModel { private String email; private String password; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Users Service Interface
The AuthenticationFilter class makes use of UsersService to fetch user details from a database. Below are the UsersService interface and a UsersService class that implements UsersService interface.
package com.appsdeveloperblog.photoapp.api.users.service; import com.appsdeveloperblog.photoapp.api.users.shared.UserDto; import org.springframework.security.core.userdetails.UserDetailsService; public interface UsersService extends UserDetailsService { public UserDto getUserByEmail(String email); }
Please note that the UsersService interface extends org.springframework.security.core.userdetails.UserDetailsService.
Users Service Class
The below UsersService class implements UsersService interface which is mentioned above.
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.appsdeveloperblog.photoapp.api.users.service; import com.appsdeveloperblog.photoapp.api.users.data.UserEntity; import com.appsdeveloperblog.photoapp.api.users.data.UsersRepository; import com.appsdeveloperblog.photoapp.api.users.shared.UserDto; import java.util.ArrayList; import java.util.UUID; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import com.appsdeveloperblog.photoapp.api.users.shared.UsersServiceException; import org.modelmapper.convention.MatchingStrategies; @Service public class UsersServiceImpl implements UsersService { BCryptPasswordEncoder bCryptPasswordEncoder; UsersRepository usersRepository; @Autowired public UsersServiceImpl(BCryptPasswordEncoder bCryptPasswordEncoder, UsersRepository usersRepository) { this.bCryptPasswordEncoder = bCryptPasswordEncoder; this.usersRepository = usersRepository; } @Override public UserDto getUserByEmail(String email) { UserEntity userEntity = usersRepository.findByEmail(email); if (userEntity == null) { throw new UsernameNotFoundException(email); } return new ModelMapper().map(userEntity, UserDto.class); } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { UserEntity userEntity = usersRepository.findByEmail(email); if (userEntity == null) { throw new UsernameNotFoundException(email); } return new User(userEntity.getEmail(), userEntity.getEncryptedPassword(), true, // Email verification status true, true, true, new ArrayList<>()); } }
UserEntity Class
To store User Details into a database and to fetch User Details from a database I use Spring Data JPA. This means we will need to create a few more classes: UserEntity class and a UsersRepository Interface.
package com.appsdeveloperblog.photoapp.api.users.data; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "users") public class UserEntity implements Serializable { private static final long serialVersionUID = 5313493413859894403L; @Id @GeneratedValue private long id; @Column(nullable = false, unique = true) private String userId; @Column(nullable = false, length = 50) private String firstName; @Column(nullable = false, length = 50) private String lastName; @Column(nullable = false, length = 120, unique = true) private String email; @Column(nullable = false) private String encryptedPassword; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getEncryptedPassword() { return encryptedPassword; } public void setEncryptedPassword(String encryptedPassword) { this.encryptedPassword = encryptedPassword; } }
UsersEntity Interface
Below is a very simple UsersRepository that extends Spring Data JPA CrudRepository Interface. The two methods in this interface are Query Methods. These two methods will fetch UserEntity from a database by Email or by UserId fields.
package com.appsdeveloperblog.photoapp.api.users.data; import org.springframework.data.repository.CrudRepository; public interface UsersRepository extends CrudRepository<UserEntity, Long>{ UserEntity findByEmail(String email); UserEntity findByUserId(String userId); }
UserDto Class
Another class that you will see is being used when converting UserEntity to UserDto is a User Data Transfer Object class. It is a very simple Java bean class which is just a data transfer object.
package com.appsdeveloperblog.photoapp.api.users.shared; public class UserDto { private String userId; private String firstName; private String lastName; private String email; private String password; private String encryptedPassword; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEncryptedPassword() { return encryptedPassword; } public void setEncryptedPassword(String encryptedPassword) { this.encryptedPassword = encryptedPassword; } }
I hope this tutorial was of some help to you. There is a step-by-step video demonstration available for this tutorial here which actually covers a lot more than User Authentication. Check it out.
Happy learning!
Hello Sergey Kargopolov and thanks for the explanation. I have a short question, if we want to send some data in payload (body) not just in header or token how is it done?
Hi Eduard!
Have a look at this tutorial(there is also a video demonstration). It explains how to send and accept HTTP POST request that contains JSON payload in HTTP Request body.
https://www.appsdeveloperblog.com/postmapping-requestbody-spring-mvc/
I hope it will be of some aid to you.
Thanks for the replay, I was referring to the path /login (implemented by Spring Security). In your tutorial and this post you used the header where you saved 2 values (key = value), but if we want to return something in the payload(body) how do we proceed? Or if we want to customize the error message from the login method implemented by Spring Security.
Thanks again!