In this tutorial, we will implement token-based authentication and role-based authorization 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. Next, we will pass that JWT token in subsequent requests.
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
Let's first create a database in MySQL server using the below command:
create database login_system
Since we’re using MySQL as our database, we need to configure the database URL, username, and password so that Spring can establish a connection with the database on startup. Open the src/main/resources/application.properties file and add the following properties to it:
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 a 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);
11. Testing using Postman
Refer to the below screenshot to test the Login REST API that returns the JWT token in the response for the Admin User:12. Implement Role-Based Authorization
Create a Simple REST API and Secure using Admin Role
@RestController
@RequestMapping("/api/")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public ResponseEntity<String> helloAdmin(){
return ResponseEntity.ok("Hello Admin");
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/user")
public ResponseEntity<String> helloUser(){
return ResponseEntity.ok("Hello User");
}
}
Comments
Post a Comment