Spring Boot Security – JWT

A proposta deste material é dar continuidade a implementação do processo de implementação de um sistema de autenticação de usuário utilizando o Spring Boot Secutiry, mas agora extendendo para a API utilizando o mecanismo de JWT. A fonte desse passo a passo pode ser encontrado no link: https://www.youtube.com/watch?v=X80nJ5T7YpE

Importante destacar que este tutorial é a segunda parte de um material que apresenta a utilização do Spring Boot Security, segue o link para a primeira parte.

Os JSON Web Tokens (JWT) são um padrão para criação de um token que representa uma reivindicação entre duas partes de forma segura. Fonte: https://jwt.io/introduction/

How does a JSON Web Token work
Fonte: https://jwt.io/introduction/

Funcionamento, o cliente deseja acessar a API

  • 1) o Cliente envia suas credenciais para um Servidor de autenticação
  • 2) o Servidor de autenticação valida as credenciais e gera um TOKEN que identifica o usuário e a sua permissão de acesso. Esse TOKEN é retornado para o cliente
  • 3) o Cliente utiliza o TOKEN para fazer a requisição para a API, e a API é capaz de confirmar a validade do TOKEN para dai responder a requisição.

O primeiro passo é alterar o arquivo pom.xml para incluir as dependências do JJWT e o JAXB-API.

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.0</version>
    </dependency>

Em seguida vamos criar uma nova classe chamada JWTUtil dentro do pacote Security que será anotada como um Serviço. Essa classe será responsável por gerar e validar os tokens JWT.

package br.univille.dacs2020.security;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Service
public class JWTUtil {
    private String SECRET_KEY= "SECRET";
    public String extractUserName(String token){
        return extractClaim(token, Claims::getSubject);
    }

    private <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_KEY).parseClaimsJws(token).getBody();
    }
    private Boolean isTokenExpired(String token){
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public String generateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    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_KEY).compact();
    }

    public Boolean validateToken (String token, UserDetails userDetails){
        final String userName = extractUserName(token);
        return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

O próximo passo é criar uma nova classe na nossa aplicação no pacote de APIs para permitir que o Cliente passe as credenciais e receba o retorno do JWT.

package br.univille.dacs2020.api;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import br.univille.dacs2020.model.Usuario;
import br.univille.dacs2020.security.JWTUtil;
import br.univille.dacs2020.service.impl.MyUserDetailsService;

@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationControllerAPI {

    @Autowired
    private MyUserDetailsService serviceMyUserDetail;
    @Autowired
    private JWTUtil serviceJWT;

    @PostMapping("/signin")
    public ResponseEntity signin(@RequestBody Usuario usuario){
        Usuario usuarioValido = serviceMyUserDetail.buscaUsuarioSenha(usuario.getUsuario(), usuario.getSenha());
        UserDetails userDetails = serviceMyUserDetail.loadUserByUsername(usuarioValido.getUsuario());
        String token = serviceJWT.generateToken(userDetails);
        return ResponseEntity.ok(token);
    }
}

Agora devemos alterar o código da classe SecurityConfigurer para incluir dois metodos que sobreescreverão os metodos de configuração do HTTPSecurity e o authenticationManager()

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
            .anyRequest().authenticated();
            
    }
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        // TODO Auto-generated method stub
        return super.authenticationManager();
    }

Inicie a aplicação e utilizando o POSTMAN ou outro cliente de API, faça uma requisição POST para o endereço http://localhost:<PORTADASUAPALICACAO>/api/v1/auth/signin e no corpo da requisição passe um objeto JSON contendo os atributos usuario e senha.

{
    "usuario": "admin",
    "senha": "admin"
}

Se copiarmos o JWT e utilizarmos o site https://jwt.io/#debugger para decodificar o token, podemos verificar que as informações do payload podem ser visualizadas. E o mais importante com a SECRET_KEY a chave pode ter seu conteúdo validado através da verificação da assinatura.

Agora que temos como criar Tokens JWT, precisamos fazer com que todas as requisições solicitem esses tokens e façam a validação para isso vamos implementar um Filtro que será chamado toda vez q uma requisição acontecer, e ele será responsável por validar o token permitindo ou não a execução da requisição.

package br.univille.dacs2020.security;

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 br.univille.dacs2020.service.impl.MyUserDetailsService;

@Component
public class JWTRequestFilter extends OncePerRequestFilter {
    @Autowired
    private MyUserDetailsService serviceMyUserDetail;
    @Autowired
    private JWTUtil serviceJWT;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
         
        String authorizationHeader = request.getHeader("Authorization");
        String username = null;
        String token = null;

        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
            token = authorizationHeader.substring(7);
            username = serviceJWT.extractUserName(token);
        }
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = serviceMyUserDetail.loadUserByUsername(username);
            if(serviceJWT.validateToken(token, userDetails)){
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request, response);

    }
    
}

Não basta apenas criar o filtro, precisamos vincular ele ao mecanismo de segurança para que ele seja executado. Para isso vamos alterar a classe SecurityConfigurer para além de carregar o filtro e aplicá-lo modificar a forma de autenticação para STATELESS.

package br.univille.dacs2020.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import br.univille.dacs2020.service.impl.MyUserDetailsService;

@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private JWTRequestFilter jwtRequestFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
            .anyRequest().authenticated()
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
            http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        // TODO Auto-generated method stub
        return super.authenticationManager();
    }
}

Por fim podemos acessar novamente o POSTMAN, copiar o token JWT que recebemos da requisição de autenticação, e fazer uma chamada para qualquer outro end point da nossa aplicação incluindo no Headers uma chave com o nome Authorization e o valor Bearer <JWT>

Bearer = Portador

Se o JWT não for enviado podemos observar que o retorno será um HTTP Status Code 403 de acesso negado.

Opcional: para que possamos voltar a acessar a interface HTML da nossa aplicação, devemos alterar a regra de autenticação do método configure HTTPSecurity

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
            .antMatchers("/api/**").authenticated()
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
            http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }