Skip to main content

BezKoder

  • Courses
  • Full Stack
  • Spring
  • Node
  • Vue
  • Angular
  • React
  • Firebase
  • Django
  • Dart
  • JsonFormatter
  • Categories

Spring Boot Security and JWT tutorial with example

Last modified: March 15, 2023 bezkoder Security, Spring

In this tutorial, we’re gonna build a Spring Boot, Spring Security that supports JWT working with H2 embedded Database. You’ll know:

  • Flow for User Login, Registration, Authorization with JWT and HttpOnly Cookie
  • Spring Boot Rest Api Architecture with Spring Security and JWT
  • How to configure Spring Security to work with JWT
  • How to define Data Models and association for Authentication and Authorization
  • Way to use Spring Data JPA to interact with H2 Database

– Related Posts:

  • Using Token in HTTP Authorization Headers
  • Spring Boot JPA + H2 example: CRUD Rest APIs
  • Spring Boot File upload example
  • @RestControllerAdvice example in Spring Boot
  • Spring Boot @ControllerAdvice & @ExceptionHandler example
  • @DataJpaTest example for Spring Data Repository Unit Test
  • Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)

Other Databases:
– Spring Boot, Spring Security example with JWT and MySQL
– Spring Boot, Spring Security example with JWT and MongoDB

Contents

  • Overview
  • Flow
  • Architecture
  • Technology
  • Project Structure
  • Setup Project
  • Configuration
  • Create the models
  • Implement Repositories
  • Configure Spring Security
  • Implement UserDetails & UserDetailsService
  • Filter the Requests
  • Create JWT Utility class
  • Handle Exception
  • Define payloads
  • Create Rest Controllers
  • Run & Check
  • Conclusion
  • Source Code
  • Further Reading

Overview of Spring Boot Security JWT example

We will build a Spring Boot + Spring Security application with JWT in that:

  • User can signup new account (registration), or login with username & password.
  • By User’s role (admin, moderator, user), we authorize the User to access resources.

These are APIs that we need to provide:

Methods Urls Actions
POST /api/auth/signup signup new account
POST /api/auth/signin login an account
POST /api/auth/signout logout the account
GET /api/test/all retrieve public content
GET /api/test/user access User’s content
GET /api/test/mod access Moderator’s content
GET /api/test/admin access Admin’s content

The database we will use is H2 by configuring project dependency & datasource.

Flow of Spring Boot Security JWT example

The diagram shows flow of how we implement User Registration, User Login/Logout and Authorization process.

spring-boot-security-jwt-example-flow

A legal JWT will be stored in HttpOnly Cookie if Client accesses protected resources.

You may need to implement Refresh Token:

spring-security-refresh-token-jwt-spring-boot-flow

More details at: Spring Boot Refresh Token with JWT example

Spring Boot Architecture with Spring Security

You can have an overview of our Spring Boot Security JWT example with the diagram below:

spring-boot-security-jwt

Now I will explain it briefly.

Spring Security

– WebSecurityConfig is the crux of our security implementation. It configures cors, csrf, session management, rules for protected resources. We can also extend and customize the default configuration that contains the elements below.
(WebSecurityConfigurerAdapter is deprecated from Spring 2.7.0, you can check the source code for update. More details at:
WebSecurityConfigurerAdapter Deprecated in Spring Boot)

– UserDetailsService interface has a method to load User by username and returns a UserDetails object that Spring Security can use for authentication and validation.

– UserDetails contains necessary information (such as: username, password, authorities) to build an Authentication object.

– UsernamePasswordAuthenticationToken gets {username, password} from login Request, AuthenticationManager will use it to authenticate a login account.

– AuthenticationManager has a DaoAuthenticationProvider (with help of UserDetailsService & PasswordEncoder) to validate UsernamePasswordAuthenticationToken object. If successful, AuthenticationManager returns a fully populated Authentication object (including granted authorities).

– OncePerRequestFilter makes a single execution for each request to our API. It provides a doFilterInternal() method that we will implement parsing & validating JWT, loading User details (using UserDetailsService), checking Authorizaion (using UsernamePasswordAuthenticationToken).

– AuthenticationEntryPoint will catch authentication error.

Repository contains UserRepository & RoleRepository to work with Database, will be imported into Controller.

Controller receives and handles request after it was filtered by OncePerRequestFilter.

– AuthController handles signup/login requests

– TestController has accessing protected resource methods with role based validations.

Understand the architecture deeply and grasp the overview more easier:
Spring Boot Architecture for JWT with Spring Security

Technology

  • Java 8
  • Spring Boot 2 (with Spring Security, Spring Web, Spring Data JPA)
  • jjwt 0.9.1
  • H2 – embedded database
  • Maven 3.6.1

Project Structure

This is folders & files structure for our Spring Boot Security JWT example:

spring-boot-security-jwt-project-structure

security: we configure Spring Security & implement Security Objects here.

  • WebSecurityConfig
  • (WebSecurityConfigurerAdapter is deprecated from Spring 2.7.0, you can check the source code for update. More details at:
    WebSecurityConfigurerAdapter Deprecated in Spring Boot)

  • UserDetailsServiceImpl implements UserDetailsService
  • UserDetailsImpl implements UserDetails
  • AuthEntryPointJwt implements AuthenticationEntryPoint
  • AuthTokenFilter extends OncePerRequestFilter
  • JwtUtils provides methods for generating, parsing, validating JWT

controllers handle signup/login requests & authorized requests.

  • AuthController: @PostMapping(‘/signup’), @PostMapping(‘/signin’), @PostMapping(‘/signout’)
  • TestController: @GetMapping(‘/api/test/all’), @GetMapping(‘/api/test/[role]’)

repository has interfaces that extend Spring Data JPA JpaRepository to interact with Database.

  • UserRepository extends JpaRepository<User, Long>
  • RoleRepository extends JpaRepository<Role, Long>

models defines two main models for Authentication (User) & Authorization (Role). They have many-to-many relationship.

  • User: id, username, email, password, roles
  • Role: id, name

payload defines classes for Request and Response objects

We also have application.properties for configuring Spring Datasource, Spring Data JPA and App properties (such as JWT Secret string or Token expiration time).

Setup new Spring Boot Security with JWT project

Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.

Then open pom.xml and add these dependencies:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

Configure Spring Datasource, JPA, App properties

Under src/main/resources folder, open application.properties, add some new lines.

spring.h2.console.enabled=true
# default path: h2-console
spring.h2.console.path=/h2-ui
 
spring.datasource.url=jdbc:h2:file:./testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
 
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update

# App Properties
bezkoder.app.jwtCookieName= bezkoder
bezkoder.app.jwtSecret= bezKoderSecretKey
bezkoder.app.jwtExpirationMs= 86400000
  • spring.datasource.url: jdbc:h2:mem:[database-name] for In-memory database and jdbc:h2:file:[path/database-name] for disk-based database.
  • spring.datasource.username & spring.datasource.password properties are the same as your database installation.
  • Spring Boot uses Hibernate for JPA implementation, we configure H2Dialect for H2 Database
  • spring.jpa.hibernate.ddl-auto is used for database initialization. We set the value to update value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should be validate.
  • spring.h2.console.enabled=true tells the Spring to start H2 Database administration tool and you can access this tool on the browser: http://localhost:8080/h2-console.
  • spring.h2.console.path=/h2-ui is for H2 console’s url, so the default url http://localhost:8080/h2-console will change to http://localhost:8080/h2-ui.

Create the models

We’re gonna have 3 tables in database: users, roles and user_roles for many-to-many relationship.

Let’s define these models.
In models package, create 3 files:

ERole enum in ERole.java.
In this example, we have 3 roles corresponding to 3 enum.

package com.bezkoder.spring.security.login.models;

public enum ERole {
  ROLE_USER,
  ROLE_MODERATOR,
  ROLE_ADMIN
}

Role model in Role.java

package com.bezkoder.spring.security.login.models;

import javax.persistence.*;

@Entity
@Table(name = "roles")
public class Role {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @Enumerated(EnumType.STRING)
  @Column(length = 20)
  private ERole name;

  public Role() {

  }

  public Role(ERole name) {
    this.name = name;
  }

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public ERole getName() {
    return name;
  }

  public void setName(ERole name) {
    this.name = name;
  }
}

User model in User.java.
It has 5 fields: id, username, email, password, roles.

package com.bezkoder.spring.security.login.models;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
@Table(name = "users",
       uniqueConstraints = {
           @UniqueConstraint(columnNames = "username"),
           @UniqueConstraint(columnNames = "email")
       })
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @NotBlank
  @Size(max = 20)
  private String username;

  @NotBlank
  @Size(max = 50)
  @Email
  private String email;

  @NotBlank
  @Size(max = 120)
  private String password;

  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "user_roles", 
             joinColumns = @JoinColumn(name = "user_id"),
             inverseJoinColumns = @JoinColumn(name = "role_id"))
  private Set<Role> roles = new HashSet<>();

  public User() {
  }

  public User(String username, String email, String password) {
    this.username = username;
    this.email = email;
    this.password = password;
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  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 Set<Role> getRoles() {
    return roles;
  }

  public void setRoles(Set<Role> roles) {
    this.roles = roles;
  }
}

Implement Repositories

Now, each model above needs a repository for persisting and accessing data. In repository package, let’s create 2 repositories.

UserRepository

There are 3 necessary methods that JpaRepository supports.

package com.bezkoder.spring.security.login.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.security.login.models.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findByUsername(String username);

  Boolean existsByUsername(String username);

  Boolean existsByEmail(String email);
}

RoleRepository

This repository also extends JpaRepository and provides a finder method.

package com.bezkoder.spring.security.login.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.bezkoder.spring.security.login.models.ERole;
import com.bezkoder.spring.security.login.models.Role;

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
  Optional<Role> findByName(ERole name);
}

Configure Spring Security

Without WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter is deprecated from Spring 2.7.0. More details at:
WebSecurityConfigurerAdapter Deprecated in Spring Boot.

In security package, create WebSecurityConfig class.

WebSecurityConfig.java

package com.bezkoder.spring.security.login.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.bezkoder.spring.security.login.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.security.login.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.security.login.security.services.UserDetailsServiceImpl;

@Configuration
@EnableGlobalMethodSecurity(
    // securedEnabled = true,
    // jsr250Enabled = true,
    prePostEnabled = true)
public class WebSecurityConfig {
  
  @Value("${spring.h2.console.path}")
  private String h2ConsolePath;
  
  @Autowired
  UserDetailsServiceImpl userDetailsService;

  @Autowired
  private AuthEntryPointJwt unauthorizedHandler;

  @Bean
  public AuthTokenFilter authenticationJwtTokenFilter() {
    return new AuthTokenFilter();
  }
 
  @Bean
  public DaoAuthenticationProvider authenticationProvider() {
      DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
       
      authProvider.setUserDetailsService(userDetailsService);
      authProvider.setPasswordEncoder(passwordEncoder());
   
      return authProvider;
  }

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests().antMatchers("/api/auth/**").permitAll()
        .antMatchers("/api/test/**").permitAll()
        .antMatchers(h2ConsolePath + "/**").permitAll()
        .anyRequest().authenticated();
    
 // fix H2 database console: Refused to display ' in a frame because it set 'X-Frame-Options' to 'deny'
    http.headers().frameOptions().sameOrigin();
    
    http.authenticationProvider(authenticationProvider());

    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
  }
}

With WebSecurityConfigurerAdapter

In security package, create WebSecurityConfig class that extends WebSecurityConfigurerAdapter.

WebSecurityConfig.java

package com.bezkoder.spring.security.login.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.bezkoder.spring.security.login.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.security.login.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.security.login.security.services.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
    // securedEnabled = true,
    // jsr250Enabled = true,
    prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  
  @Value("${spring.h2.console.path}")
  private String h2ConsolePath;
  
  @Autowired
  UserDetailsServiceImpl userDetailsService;

  @Autowired
  private AuthEntryPointJwt unauthorizedHandler;

  @Bean
  public AuthTokenFilter authenticationJwtTokenFilter() {
    return new AuthTokenFilter();
  }

  @Override
  public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
      .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
      .authorizeRequests().antMatchers("/api/auth/**").permitAll()
      .antMatchers("/api/test/**").permitAll()
      .antMatchers(h2ConsolePath + "/**").permitAll()
      .anyRequest().authenticated();
    
    // fix H2 database console: Refused to display ' in a frame because it set 'X-Frame-Options' to 'deny'
    http.headers().frameOptions().sameOrigin();

    http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
  }
}

Let me explain the code above.

– @EnableWebSecurity allows Spring to find and automatically apply the class to the global Web Security.

– @EnableGlobalMethodSecurity provides AOP security on methods. It enables @PreAuthorize, @PostAuthorize, it also supports JSR-250. You can find more parameters in configuration in Method Security Expressions.

– We override the configure(HttpSecurity http) method from WebSecurityConfigurerAdapter interface. It tells Spring Security how we configure CORS and CSRF, when we want to require all users to be authenticated or not, which filter (AuthTokenFilter) and when we want it to work (filter before UsernamePasswordAuthenticationFilter), which Exception Handler is chosen (AuthEntryPointJwt).

– Spring Security will load User details to perform authentication & authorization. So it has UserDetailsService interface that we need to implement.

– The implementation of UserDetailsService will be used for configuring DaoAuthenticationProvider by AuthenticationManagerBuilder.userDetailsService() method.

– We also need a PasswordEncoder for the DaoAuthenticationProvider. If we don’t specify, it will use plain text.

Implement UserDetails & UserDetailsService

If the authentication process is successful, we can get User’s information such as username, password, authorities from an Authentication object.

Authentication authentication = 
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

If we want to get more data (id, email…), we can create an implementation of this UserDetails interface.

security/services/UserDetailsImpl.java

package com.bezkoder.spring.security.login.security.services;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.bezkoder.spring.security.login.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class UserDetailsImpl implements UserDetails {
  private static final long serialVersionUID = 1L;

  private Long id;

  private String username;

  private String email;

  @JsonIgnore
  private String password;

  private Collection<? extends GrantedAuthority> authorities;

  public UserDetailsImpl(Long id, String username, String email, String password,
      Collection<? extends GrantedAuthority> authorities) {
    this.id = id;
    this.username = username;
    this.email = email;
    this.password = password;
    this.authorities = authorities;
  }

  public static UserDetailsImpl build(User user) {
    List<GrantedAuthority> authorities = user.getRoles().stream()
        .map(role -> new SimpleGrantedAuthority(role.getName().name()))
        .collect(Collectors.toList());

    return new UserDetailsImpl(
        user.getId(), 
        user.getUsername(), 
        user.getEmail(),
        user.getPassword(), 
        authorities);
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
  }

  public Long getId() {
    return id;
  }

  public String getEmail() {
    return email;
  }

  @Override
  public String getPassword() {
    return password;
  }

  @Override
  public String getUsername() {
    return username;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;
    UserDetailsImpl user = (UserDetailsImpl) o;
    return Objects.equals(id, user.id);
  }
}

Look at the code above, you can notice that we convert Set<Role> into List<GrantedAuthority>. It is important to work with Spring Security and Authentication object later.

As I have said before, we need UserDetailsService for getting UserDetails object. You can look at UserDetailsService interface that has only one method:

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

So we implement it and override loadUserByUsername() method.

security/services/UserDetailsServiceImpl.java

package com.bezkoder.spring.security.login.security.services;

import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;

import com.bezkoder.spring.security.login.models.User;
import com.bezkoder.spring.security.login.repository.UserRepository;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  UserRepository userRepository;

  @Override
  @Transactional
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

    return UserDetailsImpl.build(user);
  }
}

In the code above, we get full custom User object using UserRepository, then we build a UserDetails object using static build() method.

Filter the Requests

Let’s define a filter that executes once per request. So we create AuthTokenFilter class that extends OncePerRequestFilter and override doFilterInternal() method.

security/jwt/AuthTokenFilter.java

package com.bezkoder.spring.security.login.security.jwt;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import com.bezkoder.spring.security.login.security.services.UserDetailsServiceImpl;

public class AuthTokenFilter extends OncePerRequestFilter {
  @Autowired
  private JwtUtils jwtUtils;

  @Autowired
  private UserDetailsServiceImpl userDetailsService;

  private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    try {
      String jwt = parseJwt(request);
      if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
        String username = jwtUtils.getUserNameFromJwtToken(jwt);

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        UsernamePasswordAuthenticationToken authentication = 
            new UsernamePasswordAuthenticationToken(userDetails,
                                                    null,
                                                    userDetails.getAuthorities());
        
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    } catch (Exception e) {
      logger.error("Cannot set user authentication: {}", e);
    }

    filterChain.doFilter(request, response);
  }

  private String parseJwt(HttpServletRequest request) {
    String jwt = jwtUtils.getJwtFromCookies(request);
    return jwt;
  }
}

What we do inside doFilterInternal():
– get JWT from the HTTP Cookies
– if the request has JWT, validate it, parse username from it
– from username, get UserDetails to create an Authentication object
– set the current UserDetails in SecurityContext using setAuthentication(authentication) method.

After this, everytime you want to get UserDetails, just use SecurityContext like this:

UserDetails userDetails =
	(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

Create JWT Utility class

This class has 3 main funtions:

  • getJwtFromCookies: get JWT from Cookies by Cookie name
  • generateJwtCookie: generate a Cookie containing JWT from username, date, expiration, secret
  • getCleanJwtCookie: return Cookie with null value (used for clean Cookie)
  • getUserNameFromJwtToken: get username from JWT
  • validateJwtToken: validate a JWT with a secret

security/jwt/JwtUtils.java

package com.bezkoder.spring.security.login.security.jwt;

import java.util.Date;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;

import com.bezkoder.spring.security.login.security.services.UserDetailsImpl;
import io.jsonwebtoken.*;

@Component
public class JwtUtils {
  private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

  @Value("${bezkoder.app.jwtSecret}")
  private String jwtSecret;

  @Value("${bezkoder.app.jwtExpirationMs}")
  private int jwtExpirationMs;

  @Value("${bezkoder.app.jwtCookieName}")
  private String jwtCookie;

  public String getJwtFromCookies(HttpServletRequest request) {
    Cookie cookie = WebUtils.getCookie(request, jwtCookie);
    if (cookie != null) {
      return cookie.getValue();
    } else {
      return null;
    }
  }

  public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
    String jwt = generateTokenFromUsername(userPrincipal.getUsername());
    ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).build();
    return cookie;
  }

  public ResponseCookie getCleanJwtCookie() {
    ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build();
    return cookie;
  }

  public String getUserNameFromJwtToken(String token) {
    return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
  }

  public boolean validateJwtToken(String authToken) {
    try {
      Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
      return true;
    } catch (SignatureException e) {
      logger.error("Invalid JWT signature: {}", e.getMessage());
    } catch (MalformedJwtException e) {
      logger.error("Invalid JWT token: {}", e.getMessage());
    } catch (ExpiredJwtException e) {
      logger.error("JWT token is expired: {}", e.getMessage());
    } catch (UnsupportedJwtException e) {
      logger.error("JWT token is unsupported: {}", e.getMessage());
    } catch (IllegalArgumentException e) {
      logger.error("JWT claims string is empty: {}", e.getMessage());
    }

    return false;
  }
  
  public String generateTokenFromUsername(String username) {   
    return Jwts.builder()
        .setSubject(username)
        .setIssuedAt(new Date())
        .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
        .signWith(SignatureAlgorithm.HS512, jwtSecret)
        .compact();
  }
}

Remember that we’ve added bezkoder.app.jwtSecret, bezkoder.app.jwtExpirationMs and bezkoder.app.jwtCookieName properties in application.properties file.

Handle Authentication Exception

Now we create AuthEntryPointJwt class that implements AuthenticationEntryPoint interface. Then we override the commence() method. This method will be triggerd anytime unauthenticated User requests a secured HTTP resource and an AuthenticationException is thrown.

security/jwt/AuthEntryPointJwt.java

package com.bezkoder.spring.security.login.security.jwt;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

  private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
      throws IOException, ServletException {
    logger.error("Unauthorized error: {}", authException.getMessage());

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    final Map<String, Object> body = new HashMap<>();
    body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
    body.put("error", "Unauthorized");
    body.put("message", authException.getMessage());
    body.put("path", request.getServletPath());

    final ObjectMapper mapper = new ObjectMapper();
    mapper.writeValue(response.getOutputStream(), body);
  }
}

HttpServletResponse.SC_UNAUTHORIZED is the 401 Status code. It indicates that the request requires HTTP authentication.

If you want to customize the response data, just use an ObjectMapper like following code:

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
    throws IOException, ServletException {
  logger.error("Unauthorized error: {}", authException.getMessage());

  response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

  final Map<String, Object> body = new HashMap<>();
  body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
  body.put("error", "Unauthorized");
  body.put("message", authException.getMessage());
  body.put("path", request.getServletPath());

  final ObjectMapper mapper = new ObjectMapper();
  mapper.writeValue(response.getOutputStream(), body);
}

We’ve already built all things for Spring Security. The next sections of this tutorial will show you how to implement Controllers for our Rest APIs.

Define payloads for Authentication Controller

Let me summarize the payloads for our RestAPIs:
– Requests:

  • LoginRequest: { username, password }
  • SignupRequest: { username, email, password }

– Responses:

  • UserInfoResponse: { id, username, email, roles }
  • MessageResponse: { message }

To keep the tutorial not so long, I don’t show these POJOs here.
You can find details for payload classes in source code of the project on Github.

Create Spring Rest Controllers

Controller for Authentication

This controller provides APIs for register and login, logout actions.

– /api/auth/signup

  • check existing username/email
  • create new User (with ROLE_USER if not specifying role)
  • save User to database using UserRepository

– /api/auth/signin

  • authenticate { username, pasword }
  • update SecurityContext using Authentication object
  • generate JWT
  • get UserDetails from Authentication object
  • response contains JWT and UserDetails data

– /api/auth/signout: clear the Cookie.

controllers/AuthController.java

package com.bezkoder.spring.security.login.controllers;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.bezkoder.spring.security.login.models.ERole;
import com.bezkoder.spring.security.login.models.Role;
import com.bezkoder.spring.security.login.models.User;
import com.bezkoder.spring.security.login.payload.request.LoginRequest;
import com.bezkoder.spring.security.login.payload.request.SignupRequest;
import com.bezkoder.spring.security.login.payload.response.UserInfoResponse;
import com.bezkoder.spring.security.login.payload.response.MessageResponse;
import com.bezkoder.spring.security.login.repository.RoleRepository;
import com.bezkoder.spring.security.login.repository.UserRepository;
import com.bezkoder.spring.security.login.security.jwt.JwtUtils;
import com.bezkoder.spring.security.login.security.services.UserDetailsImpl;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
  @Autowired
  AuthenticationManager authenticationManager;

  @Autowired
  UserRepository userRepository;

  @Autowired
  RoleRepository roleRepository;

  @Autowired
  PasswordEncoder encoder;

  @Autowired
  JwtUtils jwtUtils;

  @PostMapping("/signin")
  public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

    Authentication authentication = authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

    SecurityContextHolder.getContext().setAuthentication(authentication);

    UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

    ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);

    List<String> roles = userDetails.getAuthorities().stream()
        .map(item -> item.getAuthority())
        .collect(Collectors.toList());

    return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
        .body(new UserInfoResponse(userDetails.getId(),
                                   userDetails.getUsername(),
                                   userDetails.getEmail(),
                                   roles));
  }

  @PostMapping("/signup")
  public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
    if (userRepository.existsByUsername(signUpRequest.getUsername())) {
      return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
    }

    if (userRepository.existsByEmail(signUpRequest.getEmail())) {
      return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!"));
    }

    // Create new user's account
    User user = new User(signUpRequest.getUsername(),
                         signUpRequest.getEmail(),
                         encoder.encode(signUpRequest.getPassword()));

    Set<String> strRoles = signUpRequest.getRole();
    Set<Role> roles = new HashSet<>();

    if (strRoles == null) {
      Role userRole = roleRepository.findByName(ERole.ROLE_USER)
          .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
      roles.add(userRole);
    } else {
      strRoles.forEach(role -> {
        switch (role) {
        case "admin":
          Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
              .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
          roles.add(adminRole);

          break;
        case "mod":
          Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
              .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
          roles.add(modRole);

          break;
        default:
          Role userRole = roleRepository.findByName(ERole.ROLE_USER)
              .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
          roles.add(userRole);
        }
      });
    }

    user.setRoles(roles);
    userRepository.save(user);

    return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
  }

  @PostMapping("/signout")
  public ResponseEntity<?> logoutUser() {
    ResponseCookie cookie = jwtUtils.getCleanJwtCookie();
    return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString())
        .body(new MessageResponse("You've been signed out!"));
  }
}

Controller for testing Authorization

There are 4 APIs:
– /api/test/all for public access
– /api/test/user for users has ROLE_USER or ROLE_MODERATOR or ROLE_ADMIN
– /api/test/mod for users has ROLE_MODERATOR
– /api/test/admin for users has ROLE_ADMIN

Do you remember that we used @EnableGlobalMethodSecurity(prePostEnabled = true) for WebSecurityConfig class?

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }

Now we can secure methods in our Apis with @PreAuthorize annotation easily.

controllers/TestController.java

package com.bezkoder.spring.security.login.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
public class TestController {
  @GetMapping("/all")
  public String allAccess() {
    return "Public Content.";
  }

  @GetMapping("/user")
  @PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
  public String userAccess() {
    return "User Content.";
  }

  @GetMapping("/mod")
  @PreAuthorize("hasRole('MODERATOR')")
  public String moderatorAccess() {
    return "Moderator Board.";
  }

  @GetMapping("/admin")
  @PreAuthorize("hasRole('ADMIN')")
  public String adminAccess() {
    return "Admin Board.";
  }
}

Run & Check

Run Spring Boot Security JWT application with command: mvn spring-boot:run

Let’s check H2 database connection with url: http://localhost:8080/h2-ui:

spring-boot-security-jwt-example-connect-database

Click on Connect button, tables that we define in models package will be automatically generated in Database.

spring-boot-security-jwt-example-h2-database

We also need to add some rows into roles table before assigning any role to User.
Run following SQL insert statements:

INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');

spring-boot-security-jwt-example-add-roles

Then check the roles table:

spring-boot-security-jwt-example-roles-table

Register some users with /signup API:

  • admin with ROLE_ADMIN
  • mod with ROLE_MODERATOR and ROLE_USER
  • zkoder with ROLE_USER

spring-boot-security-jwt-example-user-registration

Check users and user_roles tables:

spring-boot-security-jwt-example-user-table

spring-boot-security-jwt-example-user-roles-table

Access public resource: GET /api/test/all

spring-boot-security-jwt-example-public-resource

Access protected resource without Login: GET /api/test/user

spring-boot-security-jwt-example-authorization-failed

Login an account: POST /api/auth/signin

spring-boot-security-jwt-example-user-signin

Check the Cookies:

spring-boot-security-jwt-example-httponly-cookie

Access ROLE_USER and ROLE_MODERATOR resource:
–
GET /api/test/user
– GET /api/test/mod

spring-boot-security-jwt-example-authorization

Access ROLE_ADMIN resource: GET /api/test/admin, reponse will be 403 Forbidden:

spring-boot-security-jwt-example-authorization-403

Logout the Account: POST /api/auth/signout

spring-boot-security-jwt-example-logout

Solve Problem with JDK 14

If you run this Spring Boot App with JDK 14 and get following error when trying to authenticate:

FilterChain java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

Just add following dependency to pom.xml:

<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.2</version>
</dependency>

Everything’s gonna work fine.

Conclusion

Today we’ve learned so many interesting things about Spring Boot Security example with JWT and H2 database using HttpOnly Cookie.

For understanding the architecture deeply and grasp the overview more easier:
Spring Boot Architecture for JWT with Spring Security

You should continue to know how to implement Refresh Token:
Spring Boot Refresh Token with JWT example

You can also know how to deploy Spring Boot App on AWS (for free) with this tutorial.

Happy learning! See you again.

Further Reading

  • Spring Security Reference
  • In-depth Introduction to JWT-JSON Web Token

Related Posts:

  • Spring Boot JPA + H2 example: CRUD Rest APIs
  • Spring Boot Pagination & Filter example | Spring JPA, Pageable
  • CRUD GraphQL APIs example with Spring Boot & Spring JPA
  • Spring Boot Rest XML example – Web service with XML Response
  • Spring Boot File upload example with Multipart File
  • @RestControllerAdvice example in Spring Boot
  • Spring Boot @ControllerAdvice & @ExceptionHandler example

Deployment:

  • Deploy Spring Boot App on AWS – Elastic Beanstalk
  • Docker Compose: Spring Boot and MySQL example

Fullstack CRUD App:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– Angular 15 + Spring Boot example
– React + Spring Boot example

If you need a working front-end for this back-end, you can find Client App in the posts:
– Angular 12 / Angular 13 / Angular 14 / Angular 15
– React / React Redux

Other Databases:
– Spring Boot, Spring Security example with JWT and MySQL
– Spring Boot, Spring Security example with JWT and MongoDB

Associations:
– JPA One To One example with Hibernate in Spring Boot
– JPA One To Many example with Hibernate and Spring Boot
– JPA Many to Many example with Hibernate in Spring Boot

Unit Test:
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller

Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)

Source Code

You can find the complete source code for this tutorial on Github.

authentication authorization h2 database httponly cookie jwt login registration security spring boot spring security token based authentication

Post navigation

Angular 14 JWT Authentication & Authorization example
Angular 14 + Spring Boot: JWT Authentication & Authorization example
Buy me a coffee

Follow us

  • Facebook
  • Youtube
  • Github

Tools

  • Json Formatter
  • .
  • Privacy Policy
  • Contact
  • About
DMCA.com Protection Status © 2019-2022 bezkoder.com
X