We will create a sample application for JWT + Stateless Token + Spring Security + REST(JSON) API
- Create a Spring Boot Project with following dependencies:
- Spring Web
- Spring Security
- Spring Boot DevTools and Other dependencies
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
-
Create packages
- controller
- filter
- services
- model
- utility
-
Implement a UserDetailsService:
package com.example.demo.service;
import java.util.ArrayList;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Logic to get user from database
// for now just creating a dummy user
return new User("admin", "password", new ArrayList<>()); // ArrayList is for Roles
}
}
- Then create a SecurityConfiguration for Spring Security:
package com.example.demo.config;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.demo.filter.JwtFilter;
import com.example.demo.service.UserService;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService);
}
// Need during authentication when user gives username and password
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
@SuppressWarnings("deprecation")
public PasswordEncoder passwordEncoder() {
return MyNoOpPasswordEncoder.getInstance();
}
}
Create MyNoOpPasswordEncoder.java
file in utility
package as NoOpPasswordEncoder
is deprecated;
package com.example.demo.utility;
import org.springframework.security.crypto.password.PasswordEncoder;
public final class MyNoOpPasswordEncoder implements PasswordEncoder {
private static final PasswordEncoder INSTANCE = new MyNoOpPasswordEncoder();
private MyNoOpPasswordEncoder() {
}
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
/**
* Get the singleton {@link NoOpPasswordEncoder}.
*/
public static PasswordEncoder getInstance() {
return INSTANCE;
}
}
Override an overloaded configure method with Param AuthenticationManagerBuilder and pass UserDetailsService.
- Create a Controller
@GetMapping("/")
public String home() {
return "Welcome";
}
- Create a JWT Utility class for methods:
package com.example.demo.utility;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JWTUtility implements Serializable {
private static final long serialVersionUID = 234234523523L;
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
@Value("${jwt.secret}")
private String secretKey;
//retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
//for retrieving any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}
//check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
//generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
//while creating the token -
//1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secretKey).compact();
}
//validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
- Create JWT DTO Request and Response:
package com.example.demo.model;
public class JwtRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public JwtRequest(String username, String password) {
super();
this.username = username;
this.password = password;
}
public JwtRequest() {
super();
}
}
package com.example.demo.model;
public class JwtResponse {
private String jwtToken;
public String getJwtToken() {
return jwtToken;
}
public void setJwtToken(String jwtToken) {
this.jwtToken = jwtToken;
}
public JwtResponse(String jwtToken) {
super();
this.jwtToken = jwtToken;
}
public JwtResponse() {
super();
// TODO Auto-generated constructor stub
}
}
- Add Controller to Authenticate
@PostMapping("/authenticate")
public JwtResponse authenticate(@RequestBody JwtRequest jwtRequest) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(jwtRequest.getUsername(), jwtRequest.getPassword()));
} catch(BadCredentialsException e) {
throw new Exception("INVALID CREDENTIALS", e);
}
// If it succeeds then
final UserDetails userDetails = userService.loadUserByUsername(jwtRequest.getUsername());
// generate token
final String token = jwtUtility.generateToken(userDetails);
return new JwtResponse(token);
}
- Add Routes in SecurityConfig:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/authenticate")
.permitAll()
.anyRequest()
.authenticated();
}
- Specify
jwt.secret
inapplication.properties
.
Now, try hitting the url /authenticate
in postman with POST request and body as raw JSON.
{
"username": "admin",
"password": "password"
}
- Now, we will create filter for checking for
Authorization
Header, its token and a valid user from token.
package com.example.demo.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 com.example.demo.service.UserService;
import com.example.demo.utility.JWTUtility;
@Component
public class JwtFilter extends OncePerRequestFilter {
private String TOKEN_PREFIX = "Bearer ";
@Autowired
private JWTUtility jwtUtility;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check for token in header
String authorization = request.getHeader("Authorization");
String token = null;
String username = null;
// Check if Header has Bearer and extract the token and get user from token
if(authorization != null && authorization.startsWith(TOKEN_PREFIX)) {
token = authorization.substring(7);
username = jwtUtility.getUsernameFromToken(token);
}
// Check if valid user or not
if(username != null &&
// SecurityContext should be null
SecurityContextHolder.getContext().getAuthentication() == null
) {
UserDetails userDetails = userService.loadUserByUsername(username);
// Add this to security context
if(jwtUtility.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// Seriously, dont know what this one is about
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
// Continue to the next filter
filterChain.doFilter(request, response);
}
}
Remember until now spring security is using session behaviour, you need to tell spring security to not to use it check SecurityConfig sessionmanagementblock.
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/authenticate")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
Remember, to add in SecurityConfig.java
@Autowired
private JwtFilter jwtFilter;