Springboot

JWT2x

Json Web Token com Spring Boot 2x

JWT - JSON Web Token

O JSON Web Token - JWT é um padrão da Internet para a criação de dados com assinatura opcional e/ou criptografia, cujo conteúdo contém o JSON que afirma algum número de declarações. Os tokens são assinados usando um segredo privado ou uma chave pública/privada.

Qual a estrutura do JWT?

  • Header
  • Payload
  • Signature

Portanto, um JWT normalmente se parece com o seguinte: xxxxx.yyyyy.zzzzz.

O header ou cabeçalho normalmente consiste em duas partes: o tipo de token, que é JWT e o algoritmo de assinatura que está sendo utilizado, como HMAC SHA256 ou RSA.

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

De fato, a estrutura do corpo contendo as informações de autenticação e autorização de um usuário.

{
  "sub": "glysns",
  "name": "GLEYSON SAMPAIO",
  "roles": ["USERS","MANAGERS"]
}

Signature

Para criar a parte da assinatura, você deve pegar o cabeçalho codificado, o payload codificado, a chave secreta, o algoritmo especificado no cabeçalho e assiná-lo.

Spring Security + JWT

Neste tutorial, desenvolveremos um aplicativo Spring Boot que usa autenticação JWT para proteger uma API REST. Usaremos um usuário com perfis de acesso para geração e validação do token, que é transferido pelos clientes da nossa API.

Criando um projeto pelo Spring Initialzr

🔔 Atenção
Iremos configurar segurança via JWT considerando o tutorial do projeto Spring Boot 2x Security

Já iniciamos nosso projeto com as dependências abaixo:

  • Spring Web:
  • Spring Security
  • Spring Data JPA
  • H2 Database

Versão 2x do Spring Boot

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.13</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>

Confirme no pom.xml as dependências abaixo:

<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-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>

Classes Utilitárias

ClasseDescrição
JWTObjectClasse que representa um Objeto correspondente a estrutura JWT
JWTCreatorClasse responsável por gerar o Token com base no Objeto e ou instanciar o Objeto JWT com base no Token

Criando o novo método na classe de repositório UserRepository.java.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<User, Integer> {
    @Query("SELECT e FROM User e JOIN FETCH e.roles WHERE e.username= (:username)")
    public User findByUsername(@Param("username") String username);
    
    //novo método
    boolean existsByUsername(String username);
}

Criando uma nova classe contendo a regra de negócio UserService.java para adicionar um usuário via API.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserRepository repository;

    //iremos utilizar para começar a criptogragar as senhas no banco de dados
    @Autowired
    private PasswordEncoder encoder;
    public void createUser(UserEntity user){
        String pass = user.getPassword();
        //criptografando antes de salvar no banco
        user.setPassword(encoder.encode(pass));
        repository.save(user);
    }
}

Criando a classe que disponibiliza um recurso HTTP para cadastrar um usuário, UserController.java.

import org.springframework.beans.factory.annotation.Autowired;
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;

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService service;
    @PostMapping
    public void postUser(@RequestBody UserEntity user){
        service.createUser(user);
    }
}
ℹ️ O cadastro deve possuir um atributo de confirmação antes estar disponível para acessar nossa API.

Configurando o JWT no projeto

Como sabemos, JWT é um Objeto JSON criptografado, então basta criar uma representação deste objeto e o mecanismo de geração e interpretação do token.

JWTObject: Classe que representará um objeto para gerar o token.


import java.util.Arrays;
import java.util.Date;
import java.util.List;

public class JWTObject {
    private String subject; //nome do usuario
    private Date issuedAt; //data de criação do token
    private Date expiration; // data de expiração do token
    private List<String> roles; //perfis de acesso

    //getters e setters

    public void setRoles(String... roles){
        this.roles = Arrays.asList(roles);
    }
}

SecurityConfig: Classe componente que receberá as propriedades e credenciais do token via application.properties. Este recurso possibilita você definir suas chaves de segurança em um arquivo externo e por ambiente em sua empresa.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "security.config")
public class SecurityConfig {
    //singleton + atributos estáticos
    public static String PREFIX;
    public static String KEY;
    public static Long EXPIRATION;

    public void setPrefix(String prefix){
        PREFIX = prefix;
    }
    public void setKey(String key){
        KEY = key;
    }
    public void setExpiration(Long expiration){
        EXPIRATION = expiration;
    }
}
  • security.config.prefix= prefixo do token
  • security.config.key= sua chave privada
  • security.config.expiration= tempo de expiração do token - 1h em milisegundos
✔️ No application.properties adicione as propriedades para o token conforme abaixo:
security.config.prefix=Bearer
security.config.key=SECRET_KEY
security.config.expiration=3600000

JWTCreator: Classe responsável por gerar o Token com base no Objeto e vice-versa.


import java.util.List;
import java.util.stream.Collectors;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

public class JWTCreator {
    public static final String HEADER_AUTHORIZATION = "Authorization";
    public static final String ROLES_AUTHORITIES = "authorities";

    public static String create(String prefix,String key, JWTObject jwtObject) {
        String token = Jwts.builder()
                .setSubject(jwtObject.getSubject()) // proprietario ou sujeito
                .setIssuedAt(jwtObject.getIssuedAt()) // data da ativação
                .setExpiration(jwtObject.getExpiration()) // data de expiração
                .claim(ROLES_AUTHORITIES, checkRoles(jwtObject.getRoles())) //definição dos perfis de acesso
                .signWith(SignatureAlgorithm.HS512, key) // método de assinatura do token
                .compact();
        return prefix + " " + token;
    }
    public static JWTObject create(String token,String prefix,String key)
            throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException {

        JWTObject object = new JWTObject();
        token = token.replace(prefix, "");

        //verifica as credencias, expiração e perfis do usuário que serão usados no etapa de filtro de requisição
        Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        object.setSubject(claims.getSubject());
        object.setExpiration(claims.getExpiration());
        object.setIssuedAt(claims.getIssuedAt());
        object.setRoles((List) claims.get(ROLES_AUTHORITIES));
        return object;

    }
    private static List<String> checkRoles(List<String> roles) {
        return roles.stream().map(s -> "ROLE_".concat(s.replaceAll("ROLE_",""))).collect(Collectors.toList());
    }


}

JWTFilter: Classe que possui toda a lógica de validação quanto a integridade do token.


import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

public class JWTFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //obtem o token da request com AUTHORIZATION
        String token =  request.getHeader(JWTCreator.HEADER_AUTHORIZATION);
        //esta implementação só esta validando a integridade do token
        try {
            if(token!=null && !token.isEmpty()) {
                JWTObject tokenObject = JWTCreator.create(token,SecurityConfig.PREFIX, SecurityConfig.KEY);

                List<SimpleGrantedAuthority> authorities = authorities(tokenObject.getRoles());

                UsernamePasswordAuthenticationToken userToken =
                        new UsernamePasswordAuthenticationToken(
                                tokenObject.getSubject(),
                                null,
                                authorities);

                SecurityContextHolder.getContext().setAuthentication(userToken);

            }else {
                SecurityContextHolder.clearContext();
            }
            filterChain.doFilter(request, response);
        }catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException e) {
            e.printStackTrace();
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return;
        }
    }
    private List<SimpleGrantedAuthority> authorities(List<String> roles){
        return roles.stream().map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}
ℹ️ Falando em application.properties, vamos adicionar algumas configurações de banco de dados para visualizar o H2 Database na WEB
##H2 Database Connection Properties
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=sa
spring.jpa.show-sql: true
spring.h2.console.enabled=true

WebSecurityConfig: Classe responsável por centralizar toda configuração de segurança da API.

🔔 Atenção
Substitua todo o código existente pelo código abaixo:
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.h2.server.web.WebServlet;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //instância do método de criptografia
    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }
    //se você tiver o swagger confiurado, esta configuração permitirá visualizar a documentação da sua API
     private static final String[] SWAGGER_WHITELIST = {
            "/v2/api-docs/**",
            "/v3/api-docs/**",
            "/swagger-resources",
            "/swagger-resources/**",
            "/configuration/ui",
            "/configuration/security",
            "/swagger-ui/**",
            "/webjars/**"
    };
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();
        http.cors().and().csrf().disable()
                .addFilterAfter(new JWTFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers(SWAGGER_WHITELIST).permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST,"/login").permitAll()
                .antMatchers(HttpMethod.POST,"/users").permitAll()
                .antMatchers(HttpMethod.GET,"/users").hasAnyRole("USERS","MANAGERS")
                .antMatchers("/managers").hasAnyRole("MANAGERS")
                .anyRequest().authenticated()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean //HABILITANDO ACESSAR O H2-DATABSE NA WEB
    public ServletRegistrationBean h2servletRegistration(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean( new WebServlet());
        registrationBean.addUrlMappings("/h2-console/*");
        return registrationBean;
    }
}
🔔 Atenção
Para habilitar o console do H2 database é necessário remover no pom.xml a propriedade <scope>runtime</scope> da dependência do H2.

Salvando um usuário

Para incluir um usuário vamos executar um POST: http://localhost:8080/users passando o json no body, conforme conteúdo abaixo:

{"name":"GLEYSON", "username":"glysns", "password":"jwt123","roles":["USERS","MANAGERS"]}

Em seguida vamos confirmar no console do H2 Database.

http://localhost:8080/h2-console/
JDBC URL: jdbc:h2:mem:testdb
User Name: sa
Pasword: sa

Gerando o token

Com o nosso usuário devidamente inserido na base de dados, é hora de gerar o token com base nos dados passados pelo Login de acesso.

Login: Classe que receberá os dados para a realização do Login na aplicação.

public class Login {
    private String username;
    private String password;
    
    //getters e setters
}

Sessao: Classe que representa uma sessão do sistema contento o token gerado.

public class Sessao {
   private String login;
   private String token;
   
   //getters e setters
}

LoginController: Esta classe terá o recurso de realizar o login e geração do token.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class LoginController {
    @Autowired
    private PasswordEncoder encoder;
    @Autowired
    private SecurityConfig securityConfig;
    @Autowired
    private UserRepository repository;

    @PostMapping("/login")
    public Sessao logar(@RequestBody Login login){
        UserEntity user = repository.findByUsername(login.getUsername());
        if(user!=null) {
            boolean passwordOk =  encoder.matches(login.getPassword(), user.getPassword());
            if (!passwordOk) {
                throw new RuntimeException("Senha inválida para o login: " + login.getUsername());
            }
            //Estamos enviando um objeto Sessão para retornar mais informações do usuário
            Sessao sessao = new Sessao();
            sessao.setLogin(user.getUsername());

            JWTObject jwtObject = new JWTObject();
            jwtObject.setSubject(user.getUsername());
            jwtObject.setIssuedAt(new Date(System.currentTimeMillis()));
            jwtObject.setExpiration((new Date(System.currentTimeMillis() + SecurityConfig.EXPIRATION)));
            jwtObject.setRoles(user.getRoles());
            sessao.setToken(JWTCreator.create(SecurityConfig.PREFIX, SecurityConfig.KEY, jwtObject));
            return sessao;
        }else {
            throw new RuntimeException("Erro ao tentar fazer login");
        }
    }
}
ℹ️ Esta implementação poderia estar em um LoginService.

Execute a aplicação para testar o login conforme instrução abaixo:

POST: http://localhost:8080/login

{ "username":"glysns", "password":"jwt123"}
✔️ Acesso o site jwt.io para analisar o token gerado

Testando o Token

Vamos criar um serviço no qual precisará validar os dados do token para retornar as respostas abaixo:

WelcomeController: Classes com algumas operações de nossa API.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
    @GetMapping
    public String welcome(){
        return "Welcome to My Spring Boot Web API";
    }
    @GetMapping("/users")
    public String users() {
        return "Authorized user";
    }
    @GetMapping("/managers")
    public String managers() {
        return "Authorized manager";
    }
}

✔️ Agora nossa API está segura conforme nossa política de segurança, que tal documentar os recursos via Swagger seguindo nosso tutorial? click aqui