In this tutorial, we will learn how to implement token-based authentication using Spring Boot 3, Spring Security, JWT, and MySQL database. We will create a Login REST API that will authenticate the request, generate a JWT token, and return that token in the response.
JWT stands for JSON Web Token, which is an open standard for securely transmitting information as a JSON object between parties. It is a compact, self-contained method of transmitting data between two parties, typically a client and a server.
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for securing both web and reactive applications, it is the de-facto standard for securing Spring-based applications.
1. Add Maven 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-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
2. Configure MySQL Database
create database login_system
spring.datasource.url=jdbc:mysql://localhost:3306/login_system
spring.datasource.username=root
spring.datasource.password=Mysql@123
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app.jwt-expiration-milliseconds=604800000
3. Create JPA Entities - User and Role (Many-to-Many Mapping)
User
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
)
private Set<Role> roles;
}
Role
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
4. Create Spring Data JPA Repositories
public interface UserRepository extends JpaRepository<User, Long> {
}
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
}
5. JWT Implementation Classes
JwtAuthenticationEntryPoint
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
JwtTokenProvider
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${app.jwt-secret}")
private String jwtSecret;
@Value("${app-jwt-expiration-milliseconds}")
private long jwtExpirationDate;
// generate JWT token
public String generateToken(Authentication authentication){
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);
String token = Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(expireDate)
.signWith(key())
.compact();
return token;
}
private Key key(){
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
// get username from JWT token
public String getUsername(String token){
return Jwts.parser()
.verifyWith((SecretKey) key())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
// validate JWT token
public boolean validateToken(String token){
Jwts.parser()
.verifyWith((SecretKey) key())
.build()
.parse(token);
return true;
}
}
JwtAuthenticationFilter
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
// Execute Before Executing Spring Security Filters
// Validate the JWT Token and Provides user details to Spring Security for Authentication
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
private UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Get JWT token from HTTP request
String token = getTokenFromRequest(request);
// Validate Token
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){
// get username from token
String username = jwtTokenProvider.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
6. Spring Security Implementation
CustomUserDetailsService
import lombok.AllArgsConstructor;
import net.javaguides.todo.entity.User;
import net.javaguides.todo.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Set;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User(
usernameOrEmail,
user.getPassword(),
authorities
);
}
}
SpringSecurityConfig
import lombok.AllArgsConstructor;
import net.javaguides.todo.security.JwtAuthenticationEntryPoint;
import net.javaguides.todo.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
@AllArgsConstructor
public class SpringSecurityConfig {
private UserDetailsService userDetailsService;
private JwtAuthenticationEntryPoint authenticationEntryPoint;
private JwtAuthenticationFilter authenticationFilter;
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests((authorize) -> {
// authorize.requestMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN");
// authorize.requestMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN");
// authorize.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN");
// authorize.requestMatchers(HttpMethod.GET, "/api/**").hasAnyRole("ADMIN", "USER");
// authorize.requestMatchers(HttpMethod.PATCH, "/api/**").hasAnyRole("ADMIN", "USER");
// authorize.requestMatchers(HttpMethod.GET, "/api/**").permitAll();
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
authorize.anyRequest().authenticated();
}).httpBasic(Customizer.withDefaults());
http.exceptionHandling( exception -> exception
.authenticationEntryPoint(authenticationEntryPoint));
http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
// @Bean
// public UserDetailsService userDetailsService(){
//
// UserDetails ramesh = User.builder()
// .username("ramesh")
// .password(passwordEncoder().encode("password"))
// .roles("USER")
// .build();
//
// UserDetails admin = User.builder()
// .username("admin")
// .password(passwordEncoder().encode("admin"))
// .roles("ADMIN")
// .build();
//
// return new InMemoryUserDetailsManager(ramesh, admin);
// }
}
7. Creating DTO classes - LoginDto and JwtAuthResponse
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
private String usernameOrEmail;
private String password;
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class JwtAuthResponse {
private String accessToken;
private String tokenType = "Bearer";
}
8. Creating Service Layer
AuthService
public interface AuthService {
String login(LoginDto loginDto);
}
AuthServiceImpl
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
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.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {
private AuthenticationManager authenticationManager;
@Override
public String login(LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginDto.getUsernameOrEmail(),
loginDto.getPassword()
));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtTokenProvider.generateToken(authentication);
return token;
}
}
9. Controller Layer - Login REST API return JWT Token
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private AuthService authService;
// Build Login REST API
@PostMapping("/login")
public ResponseEntity<JwtAuthResponse> login(@RequestBody LoginDto loginDto){
String token = authService.login(loginDto);
JwtAuthResponse jwtAuthResponse = new JwtAuthResponse();
jwtAuthResponse.setAccessToken(token);
return new ResponseEntity<>(jwtAuthResponse, HttpStatus.OK);
}
}
10. Insert SQL Scripts
Before testing Spring security and JWT, make sure that you use below SQL scripts to insert the database into respective tables:
INSERT INTO `users` VALUES
(1,'ramesh@gmail.com','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'admin@gmail.com','admin','$2a$10$gqHrslMttQWSsDSVRTK1OehkkBiXsJ/a4z2OURU./dizwOQu5Lovu','admin');
INSERT INTO `roles` VALUES (1,'ROLE_ADMIN'),(2,'ROLE_USER');
INSERT INTO `users_roles` VALUES (2,1),(1,2);
Comments
Post a Comment