Before seeing this tutorial please go and see this tutorial first – Spring Boot 3.x Security Using Username Password. This tutorial is a continuation of the same.
In addition to the earlier dependencies you need the below JWT dependencies to be added:
implementation 'io.jsonwebtoken:jjwt-api:0.10.5' implementation 'io.jsonwebtoken:jjwt-impl:0.10.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.10.5'
So the final dependencies section will look something like below:
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'io.jsonwebtoken:jjwt-api:0.10.5' implementation 'io.jsonwebtoken:jjwt-impl:0.10.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.10.5' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' }
Create a util class- MyJwtUtil.java and add some standard methods like createToken(), validateToken(), etc.. like below :
package com.heapsteep.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Service public class MyJwtUtil { private String secret = "HeapsteepIsAveryGoodBloggingSiteForJavaTechStack"; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String generateToken(String username) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, username); } private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
In MySecurityConfig.java add a AuthenticationManager Bean:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }
Create a User Transfer Object: UserTO.java:
@Data @AllArgsConstructor @NoArgsConstructor public class UserTO { private String userName; private String password; }
In MyController.java class add below endpoint so that anyone can generate a JWT token:
@PostMapping("/authenticate") public String generateToken(@RequestBody UserTO userTO) throws Exception { try { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(userTO.getUserName(), userTO.getPassword()) ); } catch (Exception ex) { throw new Exception("inavalid username/password"); } return myJwtUtil.generateToken(userTO.getUserName()); }
In the above method we are doing 2 steps – first authenticating the credentials and if it succeeds then only generating token.
Now we need to do permitAll to /authenticate endpoint in Spring Security config class so that everyone should be able to access it without any credentials. Below is the code snippte:
.requestMatchers("/","/authenticate").permitAll())
Lets now run our application and see whether we are able to generate JWT token or not.
Run using gradle bootrun.
Open a postman and do a POST call to this URL: http://localhost:8080/authenticate
Send the below JSON request:
{ "userName": "Prasanna", "password": "1234" }
We should get the generated JWT token.
If we take this JWT key and put it here: jwt.io, we could see something like below:
Put the secret key in the “VERIFY SIGNATURE” and it will validate the signature.
Now, lets call our existing endpoints and see whether we are able to access that using JWT token.
So, lets do a GET call to http://localhost:8080 passing below values in Headers:
Content-Type: application/json
Authorization: Bearer <JWT token>
You will get 403 forbidden error – Access Denied.
Because Spring does not understand what we are giving as part of the Authorization header.
So we have to tell Spring to extract credentials from the token and validate it. For that we have to add an additional layer, basically a filter before it reaches the controller class.
Create a new filter class like – MyJWTFilter.java. Extend it from OncePerRequestFilter and override the doFilterInternal(request, response, filterChain) method. Annotate this class as @Component.
In the doFilter method we are doing the below things:
1. Extracting the token from the Authorization header.
2. Extracting the User Name from the token.
3. Fetch the User Details using the User Name.
4. Validates the token.
5. If everything is Ok then we are setting it to SecurityContext object.
6. Call the doFilter() method.
package com.heapsteep.filter; import com.heapsteep.service.MyUserDetailsService; import com.heapsteep.util.MyJwtUtil; 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.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class MyJWTFilter extends OncePerRequestFilter { @Autowired MyJwtUtil myJwtUtil; @Autowired MyUserDetailsService myUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String authorizationHeader = httpServletRequest.getHeader("Authorization"); String token = null; String userName = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { token = authorizationHeader.substring(7); userName = myJwtUtil.extractUsername(token); } if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = myUserDetailsService.loadUserByUsername(userName); if (myJwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } filterChain.doFilter(httpServletRequest, httpServletResponse); } }
Now we have to register your filter to your Security Config class by adding the below statements:
@Autowired MyJWTFilter myJWTFilter; //Partial added code snippet: .addFilterBefore(myJWTFilter, UsernamePasswordAuthenticationFilter.class)
Next we have to enable the Session Policy which has to be Stateless. Actually JWT follows Stateless session mechanism by not saving the client info neither in server or browser cookies.
//Partial added code snippet: .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
The complete MySecurityConfig.java class would look something like below:
package com.heapsteep.config; import com.heapsteep.filter.MyJWTFilter; import com.heapsteep.service.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; 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.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @Configuration @EnableWebSecurity public class MySecurityConfig { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired MyJWTFilter myJWTFilter; @Bean public PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Bean public AuthenticationProvider myAuthenticationProvider(){ DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(myUserDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } @Bean public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception { http .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(myJWTFilter, UsernamePasswordAuthenticationFilter.class) .csrf((csrf) -> csrf.disable()) .authorizeHttpRequests((requests) -> requests .requestMatchers("/api1").hasAnyAuthority("ADMIN") .requestMatchers("/api2").hasAnyAuthority("HR") //.requestMatchers("/api1","/api2").authenticated() .requestMatchers("/","/authenticate").permitAll()) .formLogin(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
Start the application by: gradle bootrun
Now you will be able to access the endpoints successfully !!..
The source code of this project: https://github.com/heapsteep/spring-security-3-jwt