Spring Boot 3 API Security With JWT
An example of a RESTful WebServer developed using Spring & SpringBoot. This example explained about spring boot3 with Security & JWT. Also explained about swagger config and customize some on it. 0 commentsBy Sopheaktra Eang | April 02, 2024
The fully fledged server uses the following:
- Spring Framework
- SpringBoot
- Log4j2
- Spring Data JPA
- Postgres Database
- Spring Security
- JWT
- Lombok
- Swagger
There are a number of third-party dependencies used in the project. Browse the Maven pom.xml file for details of libraries and versions used.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Let's start to configuration
In this project I will also manage on profile for easy to switch profile environment configuration base on dev, uat, prod.
First you need to rename application.properties to application.yml (it's optional you can also using properties as well). After that put this configuration:
server:
port: 8080
spring:
profiles:
active: dev
* Note: You can switch your environment configuration base on active profile
Also please create application-dev.yml, application-uat.yml and application-prod.yml and put this configuration:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/jwt_security
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
properties:
hibernate:
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
application:
security:
jwt:
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
expiration: 86400000 # a day
refresh-token:
expiration: 604800000 # 7 days
*Note: You can define your configuration base on your profile and it's example so I am trying to put the same.
Create ApplicationConfig class for configuration on spring security:
package com.tra22.security.config;
import com.tra22.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository repository;
@Bean
public UserDetailsService userDetailsService() {
return username -> repository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- UserDetailsService: will implement override configuration spring security to my user repository for find user by email.
- AuthenticationProvider: will implement override to add provider to spring security (you can also add more provider to n)
- AuthenticationManager: will override to get configuration from AuthenticationConfiguration default from spring security
- PasswordEncoder: will override to hash password (you can also use another hash)
Then create AuthenticationConfiguration for filter our request to validate jwt token
package com.tra22.security.config;
import com.tra22.security.repository.TokenRepository;
import com.tra22.security.service.interf.IJwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final IJwtService jwtService;
private final UserDetailsService userDetailsService;
private final TokenRepository tokenRepository;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
var isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElse(false);
if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Also create LogoutHandlerImpl for handle logout and invalid our token when logout successfully
package com.tra22.security.config;
import com.tra22.security.repository.TokenRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogoutHandlerImpl implements LogoutHandler {
private final TokenRepository tokenRepository;
@Override
public void logout(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) {
final String authHeader = request.getHeader("Authorization");
final String jwt;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
return;
}
jwt = authHeader.substring(7);
var storedToken = tokenRepository.findByToken(jwt)
.orElse(null);
if (storedToken != null) {
storedToken.setExpired(true);
storedToken.setRevoked(true);
tokenRepository.save(storedToken);
SecurityContextHolder.clearContext();
}
}
}
Create OpenApiConfig class for implement on swagger configuration
package com.tra22.security.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
@OpenAPIDefinition(
info = @Info(
contact = @Contact(
name = "Sopheaktra Eang",
email = "eang.sopheaktra.kh@gmail.com",
url = "#"
),
description = "OpenApi documentation for Spring Security",
title = "OpenApi specification",
version = "1.0",
license = @License(
name = "Licence name",
url = "#"
),
termsOfService = "Terms"
),
servers = {
@Server(
description = "Local ENV",
url = "http://localhost:8080"
),
@Server(
description = "PROD ENV",
url = "https://192.168.201.4:8080"
)
},
security = {
@SecurityRequirement(
name = "bearerAuth"
)
}
)
@SecurityScheme(
name = "bearerAuth",
description = "JWT auth description",
scheme = "bearer",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER
)
public class OpenApiConfig {}
And here is final configuration is filter request for permission, disable session and csrf, and implement our jwt filter, logout handler.
package com.tra22.security.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import static com.tra22.security.constant.user.Permission.ADMIN_CREATE;
import static com.tra22.security.constant.user.Permission.ADMIN_DELETE;
import static com.tra22.security.constant.user.Permission.ADMIN_READ;
import static com.tra22.security.constant.user.Permission.ADMIN_UPDATE;
import static com.tra22.security.constant.user.Permission.MANAGER_CREATE;
import static com.tra22.security.constant.user.Permission.MANAGER_DELETE;
import static com.tra22.security.constant.user.Permission.MANAGER_READ;
import static com.tra22.security.constant.user.Permission.MANAGER_UPDATE;
import static com.tra22.security.constant.user.Role.ADMIN;
import static com.tra22.security.constant.user.Role.MANAGER;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(req -> req
//public access
.requestMatchers(
"/api/v1/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"
).permitAll()
//all management access
.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
//all management access with Method GET
.requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())
//all management access with Method POST
.requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())
//all management access with Method PUT
.requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())
//all management access with Method DELETE
.requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())
//otherwise need to authenticated first before access
.anyRequest()
.authenticated()
)
//disable session with STATELESS
.sessionManagement(ses -> ses.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//set provide for authentication
.authenticationProvider(authenticationProvider)
//set filter for logic check token and authenticate
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
//logout to new endpoint and set logout handler
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
);
return http.build();
}
}
You will need:
- Java JDK 17 or higher
- Maven 3+ or higher
- Tomcat 10.1
Clone the project and use Maven to build the server
mvn clean install
Swagger UI access
http://localhost:8080/swagger-ui.html
Summary
Download the source code for the sample application Spring Boot Web API with JWT. And you can understand how to build web API with secure our resource (Role and Permission).