Objectives
Today I would like to extend my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html) and add protection API based on JWT Token. At the beginning I would like to briefly describe the idea of tokens. Below is added BPMN process flow which shows the end-to-end path (this is a simple process flow without filters, authentication controllers or authentication managers)
The token structure
Token consists of three elements:
- Header (algorithm and type) - {"alg":"HS256"}
- Payload - {"sub":"user","exp":1590655192}
- Signature (defined secret was used to create signature)
Application
I created application based on my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html). The main goal is to create JWT token to protect communication between server and clients.
I selected files which I added or replaced. Let's examine the application's code.
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-openid</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-openid</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
UserDTO:
SecurityConfig.class
@Data
public class UserDTO {
private String username;
private String password;
}
SecurityConfig.class
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String prefix = "Bearer";
public static final String header = "Authorization";
public static final String secret = Base64.getEncoder().encodeToString("artsci".getBytes());;
public static final Long expir = new Long(3600000);
@SuppressWarnings("deprecation")
@Bean
public UserDetailsService userDetailsService() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("user").roles("USER").build());
manager.createUser(users.username("admin").password("admin").roles("USER", "ADMIN").build());
return manager;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(new LoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AuthJwtFilter(), UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
}
}
LoginFilter.class
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
public LoginFilter(String url, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
response) throws AuthenticationException, IOException, ServletException {
UserDTO user = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
Collections.emptyList());
return getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(authResult.getName())
.signWith(SignatureAlgorithm.HS256, secret)
.setExpiration(new Date(System.currentTimeMillis() + expir))
.compact();
response.addHeader(header, prefix + " " + token);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"" + header + "\":\"" + prefix + " " + token + "\"}");
}
}
AuthJwtFilter.class
public class AuthJwtFilter extends GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
Authentication auth = null;
if(!((HttpServletRequest) request).getHeader(header).isEmpty()) {
String claim = ((HttpServletRequest)request).getHeader(header)
.replace(prefix,"").substring(1);
String user = Jwts.parser().setSigningKey(secret).parseClaimsJws(claim)
.getBody().getSubject();
auth = new UsernamePasswordAuthenticationToken(user, null,
Collections.emptyList());
}
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request,response);
}
}
The results
So, our application should work correctly. Let's try to call protected API. At the beginning it is necessary to to generate token. To achieve this goal I attempt to use URL: "/login" and pass my credential. Below is example of CURL's request.
curl --location --request POST http://localhost:8080/login --header "Content-Type: application/json" --data-raw "{\"username\":\"user\",\"password\":\"user\"}"
{"Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8"}
Fantastic, JWT Token was generated. Let's try to call "/regions" service using previously generated token.
curl -H "Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8" --request GET http://localhost:8080/regions/
[{"regionId":1,"name":"Europe"},{"regionId":2,"name":"Americas"},{"regionId":3,"name":"Asia"},{"regionId":4,"name":"Middle East and Africa"}]
It seems that the solution works exactly as I expected :)