JWT2x
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
.
Header
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
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
Classe | Descrição |
---|---|
JWTObject | Classe que representa um Objeto correspondente a estrutura JWT |
JWTCreator | Classe 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);
}
}
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
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());
}
}
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.
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;
}
}
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");
}
}
}
Execute a aplicação para testar o login conforme instrução abaixo:
POST: http://localhost:8080/login
{ "username":"glysns", "password":"jwt123"}
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";
}
}