Spring Boot REST API Full Tutorial
This is a complete tutorial of building a REST api with Spring Boot with. You will learn how to :
- Create REST APIs
- Add spring security with jwt authentication & authorization
- Work with database
- Add database migration with flyway
The source code of this tutorial is published in git : https://github.com/farhantanvirtushar/spring-rest-demo
Creating A Spring Boot REST API
Create a spring boot application from Spring Initializr with Java version 17. Then add following dependency in pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Create a new package named controllers. Then create a new controller named HomeController.java inside our newly created controllers package. Write the following code inside HomeController.java :
package com.example.springrestdemo.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/rest/home")
public class HomeController {
@ResponseBody
@RequestMapping(value = "",method = RequestMethod.GET)
public String hello(){
return "hello world";
}
}
We have created an api endpoint “/rest/home” for GET request. Now run the application through IDE or through command line with the following command :
mvn spring-boot:run
Now open Postman and send a GET request to http://localhost:8080/rest/home
Sending JSON Responses
Currently our api is only returning a string. To return a json, we first need to create a model class. Create a package named model and add a model class HelloRes.java :
package com.example.springrestdemo.model;
public class HelloRes {
private String message;
private String time;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
Update the HomeController like this :
package com.example.springrestdemo.controllers;
import com.example.springrestdemo.model.HelloRes;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.sql.Timestamp;
import java.util.Date;
@Controller
@RequestMapping("/rest/home")
public class HomeController {
@ResponseBody
@RequestMapping(value = "",method = RequestMethod.GET)
public ResponseEntity<HelloRes> hello(){
Date date = new Date();
Timestamp timestamp = new Timestamp(date.getTime());
HelloRes helloRes = new HelloRes();
helloRes.setMessage("Hello World");
helloRes.setTime(timestamp.toString());
return ResponseEntity.ok(helloRes);
}
}
Spring will serialize any java object into a json. In this case, we are creating an object of HelloRes class. Now, run the application and send a GET request to http://localhost:8080/rest/home through postman :
In the HelloRes.java class, we added getter and setter method. We can let spring boot generate all getter, setter and constructor method by using lombok. This will simplify our codes. Add the following dependency to the pom.xml file :
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Now, update the HelloRes class as follows :
package com.example.springrestdemo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HelloRes {
private String message;
private String time;
}
We have used three annotations with the HelloRes.java class.
“@Data” annotation generates all getter & setter methods
“@AllArgsConstructor” generates a constructor with all properties
“@NoArgsConstructor” generates a constructor without any property
By using this annotations, we no longer need to write any getter & setter method. Spring will auto generate this methods.
Securing Routes
Now we want to secure our REST api end points jwt authentication & authorization. Add the following dependencies in pom.xml file :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
We have added two dependencies, one for spring security & other for jwt.
Adding Model Classes
Next we will create some model class for API requests & responses. Create a package named model.request and add a model class LoginReq :
package com.example.springrestdemo.model.request;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginReq {
private String email;
private String password;
}
Create another package named model.response for API response & add two model class LoginRes.java & ErrorRes.java :
package com.example.springrestdemo.model.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRes {
private String email;
private String token;
}
package com.example.springrestdemo.model.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorRes {
HttpStatus httpStatus;
String message;
}
Lastly, add a user model class inside model package named User.java for user login.
package com.example.springrestdemo.model;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String email;
private String password;
private String firstName;
private String lastName;
public User(String email, String password) {
this.email = email;
this.password = password;
}
}
Adding Security Config & JWT Filter
Create a new package named “auth” and add a component class named JwtUtil.java . This class will be used for creating & resolving jwt tokens.
package com.example.springrestdemo.auth;
import com.example.springrestdemo.model.User;
import io.jsonwebtoken.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class JwtUtil {
private final String secret_key = "mysecretkey";
private long accessTokenValidity = 60*60*1000;
private final JwtParser jwtParser;
private final String TOKEN_HEADER = "Authorization";
private final String TOKEN_PREFIX = "Bearer ";
public JwtUtil(){
this.jwtParser = Jwts.parser().setSigningKey(secret_key);
}
public String createToken(User user) {
Claims claims = Jwts.claims().setSubject(user.getEmail());
claims.put("firstName",user.getFirstName());
claims.put("lastName",user.getLastName());
Date tokenCreateTime = new Date();
Date tokenValidity = new Date(tokenCreateTime.getTime() + TimeUnit.MINUTES.toMillis(accessTokenValidity));
return Jwts.builder()
.setClaims(claims)
.setExpiration(tokenValidity)
.signWith(SignatureAlgorithm.HS256, secret_key)
.compact();
}
private Claims parseJwtClaims(String token) {
return jwtParser.parseClaimsJws(token).getBody();
}
public Claims resolveClaims(HttpServletRequest req) {
try {
String token = resolveToken(req);
if (token != null) {
return parseJwtClaims(token);
}
return null;
} catch (ExpiredJwtException ex) {
req.setAttribute("expired", ex.getMessage());
throw ex;
} catch (Exception ex) {
req.setAttribute("invalid", ex.getMessage());
throw ex;
}
}
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(TOKEN_HEADER);
if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) {
return bearerToken.substring(TOKEN_PREFIX.length());
}
return null;
}
public boolean validateClaims(Claims claims) throws AuthenticationException {
try {
return claims.getExpiration().after(new Date());
} catch (Exception e) {
throw e;
}
}
public String getEmail(Claims claims) {
return claims.getSubject();
}
private List<String> getRoles(Claims claims) {
return (List<String>) claims.get("roles");
}
}
Let us see what each method does in this class.
The createToken() method takes an User object , creates a Claims object from user data and builds a jwt token with Jwts.builder() . The Claims object is used as the jwt body.
The resolveClaims() method takes an HttpServletRequest object as parameter and resolve Claims object from Bearer token in the request header. It will through an exception if no such token is present in request header or the token is expired or invalid.
UserDetailsService & UserRepository
Spring security uses an interface called UserDetailsService to load user details and match the user with the user input. We have to implement our own custom user details service. We also need to create a repository to load user details from database. First create a repository named UserRepository.java in “repositories” package :
package com.example.springrestdemo.repositories;
import com.example.springrestdemo.model.User;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
public User findUserByEmail(String email){
User user = new User(email,"123456");
user.setFirstName("FirstName");
user.setLastName("LastName");
return user;
}
}
We created a method inside UserRepository class to find users by email addresses. This method will search a user by email from database and return it. In our example we are returning a static User object with password : “123456”. We are using a plain text password because we are not using any encoding for passwords. UserDetailsService will use this password to verify the user. Let’s create our custom UserDetailsService. Create a new class CustomUserDetailsService inside a new package “services” :
package com.example.springrestdemo.services;
import com.example.springrestdemo.model.User;
import com.example.springrestdemo.repositories.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findUserByEmail(email);
List<String> roles = new ArrayList<>();
roles.add("USER");
UserDetails userDetails =
org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(roles.toArray(new String[0]))
.build();
return userDetails;
}
}
We inject an UserRepository object using dependency injection and override the loadUserByUsername() method, which returns an UserDetails object. In our implementation, loadUserByUsername() get user by the email address from UserRepository , construct an UserDetails object from it and then returns. Spring security will internally call this method with user provided email, and then match the password from UserDetails object with user provided password.
SecurityConfig
Create a class inside auth package named SecurityConfig.java:
package com.example.springrestdemo.auth;
import com.example.springrestdemo.services.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
this.userDetailsService = customUserDetailsService;
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, NoOpPasswordEncoder noOpPasswordEncoder)
throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(noOpPasswordEncoder);
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.requestMatchers("/rest/auth/**").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
@SuppressWarnings("deprecation")
@Bean
public NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
We used two annotations : “@Configuration” and “@EnableWebSecurity”. These annotations tells spring security to use our custom security configuration instead of default one. We created a Bean of SecurityFilterChain which implements our custom filter logic.
http.csrf().disable()
.authorizeRequests()
.requestMatchers("/rest/auth/**").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
This line tells spring security to permit all request without any authentication that starts with “/rest/auth/”. We will use this url prefix for login & registration api
We also created another Bean of NoOpPasswordEncoder. We are using this bean because we don’t want any encoding (e.g. Bcrypt) to our passwords. That means, we are storing plain text into database as passwords. In practice, we don’t store plain text passwords into database.
We created a Bean of AuthenticationManager which will be used to authenticate the user. We passed our CustomUserDetailsService object & password encoding object to AuthenticationManager.
Adding API Route For Login
We need to add an api for user login. Create a controller class named AuthController.java in controllers package.
package com.example.springrestdemo.controllers;
import com.example.springrestdemo.auth.JwtUtil;
import com.example.springrestdemo.model.User;
import com.example.springrestdemo.model.request.LoginReq;
import com.example.springrestdemo.model.response.ErrorRes;
import com.example.springrestdemo.model.response.LoginRes;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/rest/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
@ResponseBody
@RequestMapping(value = "/login",method = RequestMethod.POST)
public ResponseEntity login(@RequestBody LoginReq loginReq) {
try {
Authentication authentication =
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginReq.getEmail(), loginReq.getPassword()));
String email = authentication.getName();
User user = new User(email,"");
String token = jwtUtil.createToken(user);
LoginRes loginRes = new LoginRes(email,token);
return ResponseEntity.ok(loginRes);
}catch (BadCredentialsException e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST,"Invalid username or password");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
}
We created an api “/rest/auth/login” for login requests.
Authentication authentication =
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginReq.getEmail(), loginReq.getPassword()));
String email = authentication.getName();
This line authenticate the user with email & password. The authenticationManager.authenticate() method will internally call loadUserByUsername() method from our CustomUserDetailsService class. Then it will match the password from userDetailsService with the password found from LoginReq. This method will throw exception if the authentication is not successful.
Now build & run the application. Send a POST request from Postman to “/rest/auth/login” api. Add email & password “123456” in the request body.
We have successfully created a jwt token and sent back to user. Now if we want to access any other route, we will get an error:
We got a 403 Forbidden error because this request is not authenticated. We need to add a jwt authorization filter for each request. This filter will block all requests that don’t have jwt token in the request header.
Adding JwtAuthorizationFilter
Create a new class named JwtAuthorizationFilter.java inside auth package.
package com.example.springrestdemo.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final ObjectMapper mapper;
public JwtAuthorizationFilter(JwtUtil jwtUtil, ObjectMapper mapper) {
this.jwtUtil = jwtUtil;
this.mapper = mapper;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, Object> errorDetails = new HashMap<>();
try {
String accessToken = jwtUtil.resolveToken(request);
if (accessToken == null ) {
filterChain.doFilter(request, response);
return;
}
System.out.println("token : "+accessToken);
Claims claims = jwtUtil.resolveClaims(request);
if(claims != null & jwtUtil.validateClaims(claims)){
String email = claims.getSubject();
System.out.println("email : "+email);
Authentication authentication =
new UsernamePasswordAuthenticationToken(email,"",new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}catch (Exception e){
errorDetails.put("message", "Authentication Error");
errorDetails.put("details",e.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(response.getWriter(), errorDetails);
}
filterChain.doFilter(request, response);
}
}
We override the doFilterInternal() method. This method will be called for every request to out application. This method reads Bearer token from request headers and resolves claims. First, it checks if any access token is present in the request header. If the accessToken is null. It will pass the request to next filter chain. Any login request will not have jwt token in their header, therefore they will be passed to next filter chain. If any acessToken is present, then it will validate the token and then authenticate the request in SecurityContext.
Authentication authentication =
new UsernamePasswordAuthenticationToken(email,"",new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
This line will authenticate the request to the SecurityContext. So, any request having a jwt token in their header will be authenticated & permitted by spring security.
Finally we need to add this filter to our SecurityConfig.java class.
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthorizationFilter jwtAuthorizationFilter;
public SecurityConfig(CustomUserDetailsService customUserDetailsService, JwtAuthorizationFilter jwtAuthorizationFilter) {
this.userDetailsService = customUserDetailsService;
this.jwtAuthorizationFilter = jwtAuthorizationFilter;
}
/.../
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.requestMatchers("/rest/auth/**").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilterBefore(jwtAuthorizationFilter,UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/.../
}
Here we have added our jwt filter before the UsernamePasswordAuthenticationFilter. Because we want every request to be authenticated before going through spring security filter.
Now add the Bearer token from postman & you will be able to access any route.
Loading User Details From Database
Our UserRepository doesn’t return user from a database, instead it always returns a static user with a hard coded password. Now we will add a PostgreSQL database in out application and it will store & load user from database by email.
Add the following dependencies to pom.xml file :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
We have added 3 dependencies for : JDBC, JPA & PostgreSQL. JDBC is a low level standard for working on database. JDBC executes raw SQL queries. On the other hand, JPA is an ORM (Object Relational Mapping) which maps java classes to database table. JPA is a high level API for interacting with database. In this article, we will use JPA for sql queries.
First create a postgresql database in local or in cloud & create an “users” table. Run the following sql query to create a table named “users” :
CREATE TABLE IF NOT EXISTS users
(
id serial PRIMARY KEY,
country VARCHAR(255),
date_of_birth TIMESTAMP,
email VARCHAR(255) NOT NULL UNIQUE,
first_name VARCHAR(255) NOT NULL,
gender VARCHAR(255),
last_name VARCHAR(255) NOT NULL,
occupation VARCHAR(255),
password VARCHAR(255) NOT NULL,
profile_photo_url VARCHAR(255),
role VARCHAR(255)
);
Let’s insert an user into our database by running following query :
INSERT INTO users(first_name, last_name, email, password, profile_photo_url, country, occupation, gender, role, date_of_birth)
VALUES ('James','Smith','james19@example.com','james123','','Australia','Doctor','Male','USER',timestamp '1987-01-10');
We have inserted an user named “James Smith” into our database. Let’s insert one more user :
INSERT INTO users(first_name, last_name, email, password, profile_photo_url, country, occupation, gender, role, date_of_birth)
VALUES ('Yousuf','Hussain','yousuf12@example.com','yousuf22','','Egypt','Teacher','Male','USER',timestamp '1984-01-11');
Configuring Database In Spring Boot
In the src > main > resources directory, there is a file named “application.properties” . Write the following properties to this file :
# PostgreSQL connection settings
spring.datasource.url=jdbc:postgresql://localhost:5432/database_name
spring.datasource.username=your_database_username
spring.datasource.password=your_database_password
spring.datasource.driver-class-name=org.postgresql.Driver
# HikariCP settings
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.maximumPoolSize=20
spring.datasource.hikari.idleTimeout=30000
spring.datasource.hikari.maxLifetime=2000000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.poolName=HikariPoolBooks
# JPA settings
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.jdbc.batch_size=15
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.generate_statistics=true
spring.jpa.show-sql=true
spring.oracle.persistence.unit=postgresql
Here, I have created a postgresql database in localhost and used its connection parameters. Use your own database password & connecction parameters.
Using JPA For Database Queries
JPA maps java classes to database tables. We will use our User class for users table in db. Update our User class as following :
package com.example.springrestdemo.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "USERS")
public class User {
@Id
@Column(name = "ID")
private long id;
@Column(name = "EMAIL")
private String email;
@Column(name = "PASSWORD")
private String password;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Column(name = "PROFILE_PHOTO_URL")
private String profilePhotoUrl;
@Column(name = "COUNTRY")
private String country;
@Column(name = "OCCUPATION")
private String occupation;
@Column(name = "DATE_OF_BIRTH")
private Date dateOfBirth;
@Column(name = "GENDER")
private String gender;
@Column(name = "ROLE")
private String role;
public User(String email, String password) {
this.email = email;
this.password = password;
}
public User(String email, String password, String firstName, String lastName) {
this.email = email;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
}
}
We have added some annotations with our class and properties. The “@Entity” annotation tells jpa to use this class as a table. The “@Table” annotations tells jpa which table to map this class with. The “@Column” annotation over properties tells corresponding column names for the class properties. Now let’s update our UserRepository class. Instead of returning a static user, now it will fetch user from database. Update the UserRepository.java as following :
package com.example.springrestdemo.repositories;
import com.example.springrestdemo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
@Query("SELECT u FROM User u WHERE u.email = :email")
User findUserByEmail(@Param("email") String email);
}
We declared UserRepository as an interface which extends JpaRepository interface. We updated our findUserByEmail() which with a “@Query” annotation. We specify sql query in this annotation. We inject the method variable “email” into sql with “:email”.
Now, save & run the code. Try to login with a user credencial that we inserted into database before. For example :
{
"email": "james19@example.com",
"password" : "james123"
}
If you enter wrong password, you will get a bad credencial error. If you enter an email that doesn’t exist in the database, then you will get an error message like this :
UserRepository didn’t find any user with that email. So, it returned a null user. To handle this error properly, let’s update the loadUserByUsername() method in CustomUserDetailsService class :
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findUserByEmail(email);
if(user == null){
throw new UsernameNotFoundException("No user found with email");
}
List<String> roles = Arrays.asList(user.getRole());
UserDetails userDetails =
org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles("USER")
.build();
return userDetails;
}
We checked if user is null and then threw an UsernameNotFoundException if user is null. If we save & run this application again, the application will show a bad credential error message if no user is found with this email.
Adding User Registration Controller
So far we have manually inserted user into user tabel. We will now create api to create new users. First create a UserService in the services package for inserting user into database:
package com.example.springrestdemo.services;
import com.example.springrestdemo.model.User;
import com.example.springrestdemo.repositories.UserRepository;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
public User getUserByEmail(String email){
User user = userRepository.findUserByEmail(email);
return user;
}
public User createUser(User user){
User newUser = userRepository.save(user);
userRepository.flush();
return newUser;
}
}
createUser( ) method will take an User object as parameter & store the user into database with userRepository.save( ) method.
getUserByEmail( ) method will take an email as parameter & find a user from database by email.
Next create a model class RegistrationReq.java inside com.example.springrestdemo.model.request package :
package com.example.springrestdemo.model.request;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegistrationReq {
private String email;
private String password;
private String firstName;
private String lastName;
}
Now, update the AuthController.java as follows:
package com.example.springrestdemo.controllers;
import com.example.springrestdemo.auth.JwtUtil;
import com.example.springrestdemo.model.User;
import com.example.springrestdemo.model.request.LoginReq;
import com.example.springrestdemo.model.request.RegistrationReq;
import com.example.springrestdemo.model.response.ErrorRes;
import com.example.springrestdemo.model.response.LoginRes;
import com.example.springrestdemo.services.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/rest/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserService userService;
private JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager, UserService userService, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userService = userService;
this.jwtUtil = jwtUtil;
}
/../
@ResponseBody
@RequestMapping(value = "/register",method = RequestMethod.POST)
public ResponseEntity register(HttpServletRequest request, HttpServletResponse response, @RequestBody RegistrationReq registrationReq){
try {
User user = new User();
user.setEmail(registrationReq.getEmail());
user.setFirstName(registrationReq.getFirstName());
user.setLastName(registrationReq.getLastName());
user.setPassword(registrationReq.getPassword());
user.setRole("USER");
User newUser = userService.createUser(user);
if(newUser == null){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST,"Error creating new user");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
newUser.setPassword("");
String token = jwtUtil.createToken(newUser);
LoginRes loginRes = new LoginRes(newUser.getEmail(), token);
return ResponseEntity.ok(loginRes);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
}
We have created a new api “/rest/auth/register” for creating a new user. This controller will create a new user into database & return an access token upon successful user registration. Now, send a registration request with postman :
Adding BCrypt Password Encoding
We stored our password as a plain text. But in practice we should not store password as a plain text in database. Because anyone who has access to the database can get user password and login with it. We will store a hash of the password string using BCrypt password encoding. Update the SecurityConfig class :
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/../
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder)
throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
return authenticationManagerBuilder.build();
}
/../
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
We updated our AuthenticationManager to use BCrypt password encoding. Now update the AuthController class :
package com.example.springrestdemo.controllers;
/../
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Controller
@RequestMapping("/rest/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final UserService userService;
private JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager, BCryptPasswordEncoder bCryptPasswordEncoder, UserService userService, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.userService = userService;
this.jwtUtil = jwtUtil;
}
/../
@ResponseBody
@RequestMapping(value = "/register",method = RequestMethod.POST)
public ResponseEntity register(HttpServletRequest request, HttpServletResponse response, @RequestBody RegistrationReq registrationReq){
try {
User user = new User();
user.setEmail(registrationReq.getEmail());
user.setFirstName(registrationReq.getFirstName());
user.setLastName(registrationReq.getLastName());
user.setPassword(registrationReq.getPassword());
user.setRole("USER");
String password = registrationReq.getPassword();
String encodedPassword = bCryptPasswordEncoder.encode(password);
System.out.println("encoded password : "+ encodedPassword);
user.setPassword(encodedPassword);
User newUser = userService.createUser(user);
if(newUser == null){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST,"Error creating new user");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
newUser.setPassword("");
String token = jwtUtil.createToken(newUser);
LoginRes loginRes = new LoginRes(newUser.getEmail(), token);
return ResponseEntity.ok(loginRes);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
}
We updated our “/register” method to use BCryptPasswordEncoder.
String password = registrationReq.getPassword();
String encodedPassword = bCryptPasswordEncoder.encode(password);
System.out.println("encoded password : "+ encodedPassword);
user.setPassword(encodedPassword);
User newUser = userService.createUser(user);
This block of codes create a hash of the password and then set it in the user object. Now if you register any new user , a hash will be set into the user table password column. Let us create a new user and check our database. Run the following query after creating a new user :
select * from users;
Adding FlyWay Migration
We have directly run SQL query to create database table. In practice we should use a database migration tool. A migration tool is like git for database. We can keep track of database change history and revert any change. In this tutorial we will use flyway migration.
Add following dependency to pom.xml :
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>9.20.0</version>
</dependency>
Add a flyway config file flyway.conf in the project root directory (where pom.xml file is located). Write the following lines in flyway.conf file :
flyway.user=your_database_username
flyway.password=your_database_password
flyway.schemas=database_name
flyway.url=jdbc:postgresql://localhost:5432/database_name
flyway.locations=filesystem:db/migration
Create the “db/migration” directory inside src > main > resources. We will add migration scripts into db/migration directory.
Now, add a migration script inside db/migration directory, the name of the script will be : V1_1__create_users_table.sql
The directory structure will look like this :
Flyway migration scripts have a naming convention : V<version_name>_<script_no>__<description>.sql
Where,
<version_name> = Database version number. Example : 1
<script_no> = Migration script number. Example : 1
<description> = Script description. Example : create_users_table
Add the users table creation sql query in the script :
CREATE TABLE IF NOT EXISTS users
(
id serial PRIMARY KEY,
country VARCHAR(255),
date_of_birth TIMESTAMP,
email VARCHAR(255) NOT NULL UNIQUE,
first_name VARCHAR(255) NOT NULL,
gender VARCHAR(255),
last_name VARCHAR(255) NOT NULL,
occupation VARCHAR(255),
password VARCHAR(255) NOT NULL,
profile_photo_url VARCHAR(255),
role VARCHAR(255)
);
Now, delete the existing users table by the following query :
drop table users;
Now, run flyway migration executing the following command :
mvn clean flyway:migrate
This will create the users table and a flyway history table in the database :
The flyway_schema_history table is managed by flyway to keep track of database changes history.
Adding More APIs
Now, we will add some apis to let users do CRUD operations. We will create a StoryController to create, read, update and delete stories.
First, add a new migration script in db/migration for creating a table for stories. Name the migration script as V1_2__create_stories_table.sql :
CREATE TABLE IF NOT EXISTS stories
(
id serial PRIMARY KEY,
story TEXT,
created_at TIMESTAMP,
user_id INT,
CONSTRAINT fk_stories
FOREIGN KEY (user_id) REFERENCES users (id)
);
Eeach story has a foreign key : user_id. This table will store all stories created by users. Run the migration script with maven flyway command.
Now, add a model class for stories table inside model package :
package com.example.springrestdemo.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.sql.Timestamp;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "STORIES")
public class Story {
@Id
@Column(name = "ID")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@Column(name = "STORY")
private String story;
@Column(name = "CREATED_AT")
private Timestamp createdAt;
@Column(name = "USER_ID")
private Long userId;
}
We also need to add repository & service class for Story class. Inside repositories package add StoryRepository.java :
package com.example.springrestdemo.repositories;
import com.example.springrestdemo.model.Story;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StoryRepository extends JpaRepository<Story,Long> {
@Query("SELECT s FROM Story s WHERE s.userId = :userId")
List<Story> findAllByUser(@Param("userId") Long userId);
}
Inside services package add StoryServices.java :
package com.example.springrestdemo.services;
import com.example.springrestdemo.model.Story;
import com.example.springrestdemo.repositories.StoryRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StoryService {
private final StoryRepository storyRepository;
public StoryService(StoryRepository storyRepository) {
this.storyRepository = storyRepository;
}
public Story createStory(Story story){
Story newStory = storyRepository.save(story);
storyRepository.flush();
return newStory;
}
public Story getStoryById(Long id){
Optional<Story> optionalStory = storyRepository.findById(id);
return optionalStory.orElse(null);
}
public List<Story> getAllStoriesByUser(Long userId){
List<Story> storyList = storyRepository.findAllByUser(userId);
return storyList;
}
public List<Story> getAllStories(){
List<Story> storyList = storyRepository.findAll();
return storyList;
}
}
Finally add a StoryController inside controllers package :
package com.example.springrestdemo.controllers;
import com.example.springrestdemo.model.Story;
import com.example.springrestdemo.model.User;
import com.example.springrestdemo.model.response.ErrorRes;
import com.example.springrestdemo.model.response.LoginRes;
import com.example.springrestdemo.services.StoryService;
import com.example.springrestdemo.services.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
@Controller
@RequestMapping("/rest/story")
public class StoryController {
private final StoryService storyService;
private final UserService userService;
public StoryController(StoryService storyService, UserService userService) {
this.storyService = storyService;
this.userService = userService;
}
@ResponseBody
@RequestMapping(value = "/get-all",method = RequestMethod.GET)
public ResponseEntity getAllStories(HttpServletRequest request, HttpServletResponse response){
try {
List<Story> storyList = storyService.getAllStories();
return ResponseEntity.ok(storyList);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
@ResponseBody
@RequestMapping(value = "/create-story",method = RequestMethod.POST)
public ResponseEntity createStory(HttpServletRequest request, HttpServletResponse response, @RequestBody Story story){
try {
String userEmail = request.getUserPrincipal().getName();
User user = userService.getUserByEmail(userEmail);
if(user == null){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, "Invalid user");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
Long userId = user.getId();
story.setUserId(userId);
Date date = new Date();
Timestamp currentTimeStamp = new Timestamp(date.getTime());
story.setCreatedAt(currentTimeStamp);
Story newStory = storyService.createStory(story);
return ResponseEntity.ok(newStory);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
}
We created two apis. One for getting all stories and other for creating new story. The getAllStories() method returns all stories from database. The createStory() method first get logged in user email from spring security and then get the user id from database. Finaly it sets the user id to the userId properties of Story object.
Now, run the application and send a post request to “/rest/story/create-story” api with the following request body :
Add a few more stories and then send a get request to “/rest/story/get-all” api
[
{
"id": 1,
"story": "Hello world !!!",
"createdAt": null,
"userId": 1
},
{
"id": 2,
"story": "Hello world 1",
"createdAt": "2023-07-12T08:41:40.878+00:00",
"userId": 1
},
{
"id": 3,
"story": "Hello world 3",
"createdAt": "2023-07-12T08:45:00.656+00:00",
"userId": 1
}
]
Let us add two more apis : one for getting a story by id & other for getting all stories written by a user.
/../
@Controller
@RequestMapping("/rest/story")
public class StoryController {
/../
@ResponseBody
@RequestMapping(value = "/get-by-id/{id}",method = RequestMethod.GET)
public ResponseEntity getStoryById(HttpServletRequest request, HttpServletResponse response, @PathVariable("id") Long id){
try {
Story story = storyService.getStoryById(id);
if(story == null){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, "No story is found");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
return ResponseEntity.ok(story);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
@ResponseBody
@RequestMapping(value = "/get-by-user-id/{userId}",method = RequestMethod.GET)
public ResponseEntity getAllStoriesByUserId(HttpServletRequest request, HttpServletResponse response, @PathVariable("userId") Long userId){
try {
List<Story> storyList = storyService.getAllStoriesByUser(userId);
return ResponseEntity.ok(storyList);
} catch (Exception e){
ErrorRes errorResponse = new ErrorRes(HttpStatus.BAD_REQUEST, e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}
}
Now send a get request to http://localhost:8080/rest/story/get-by-id/2 :
{
"id": 2,
"story": "Hello world 1",
"createdAt": "2023-07-12T08:41:40.878+00:00",
"userId": 1
}
Send another get request to http://localhost:8080/rest/story/get-by-user-id/1
[
{
"id": 1,
"story": "Hello world !!!",
"createdAt": null,
"userId": 1
},
{
"id": 2,
"story": "Hello world 1",
"createdAt": "2023-07-12T08:41:40.878+00:00",
"userId": 1
},
{
"id": 3,
"story": "Hello world 3",
"createdAt": "2023-07-12T08:45:00.656+00:00",
"userId": 1
}
]