Autenticação com JWT em projetos Spring Boot

Neste post vou mostrar uma forma prática e simples para implementar em seus projetos spring boot visando protejer as requisições usando o padrão JWT

Wolmir Cezer Garbin por Wolmir Cezer Garbin - - Spring Boot - TUTORIAL

Última atualização em: | 22343 Visualizações

Para quem já conhece ou ouvio falar em Json Web Token (JWT), sabe que, este é um padrão para transmitir mensagens seguras utilizando um token.

O token gerado é compacto, leve e não mantem estado no servidor ou seja, se precisar escalar a aplicação isso será ótimo.

Crinando o projeto Spring Boot

Para criar o projeto em spring boot pode utilizar o http://start.spring.io.

Após criar o projeto confira se o mesmo possui todas as dependências e classe como mostrado abaixo.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>demo</name>
	<description>Demo project for Spring Boot</description>

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

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Antes de qualquer coisa, precisamos criar a classe principal, contendo apenas um acesso / que mostra uma string Home Page.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@GetMapping("/")
	public String home() {
		return "Home Page";
	}
}

Após executar o projeto, podemos abrir o navegador e digitar http://localhost:8080, isso vai mostrar a mensagem, sem nenhuma restrição.



Autenticação com JWT

Após criar o projete, daremos inicio a criação da rotina de autenticação. Para isso utilizaremos o spring security e JWT adicionando as dependências ao pom.xml.

pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.7.0</version>
</dependency>

Após adicionar as dependências, crie um package chamado de jwt em sua aplicação, dentro dele, crie outro package chamado filter para adicionarmos as classes do JWT.

Logo após criamos as classes de filtro e autenticação do JWT, para isso começaremos com o objeto que representa o usuário e senha a serem enviados UserCredentials.

package /jwt/filter
import lombok.Data;

@Data
public class UserCredentials {

  private String username;
  private String password;

}

Note que estamos utilizando lombok para criar os getters e setters dos atributos, caso não utilize lombok implemente os métodos na mão.

Criada a classe que receberá a autenticação, passamos para a criação dos filters.

package /jwt/filter
import com.example.demo.jwt.service.TokenAuthenticationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {
        UserCredentials creds = new ObjectMapper().readValue(req.getInputStream(), UserCredentials.class);

        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getUsername(),
                        creds.getPassword(),
                        Collections.<GrantedAuthority>emptyList()
                )
        );
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest req,
            HttpServletResponse res, FilterChain chain,
            Authentication auth
    ) throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }
}

O primeiro, JWTLoginFilter é utilizado para a chamada do login, irá autenticar o usuário e retornar o token de acesso, que permitirá acessar as requisições.

O segundo filter JWTAuthenticationFilter, filtra as requisições para verificar a presença do token de acesso para permitir as requisições.

package /jwt/filter
import com.example.demo.jwt.service.TokenAuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class JWTAuthenticationFilter extends GenericFilterBean {

    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain
        ) throws IOException, ServletException {

        Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest) request);

        SecurityContextHolder.getContext().setAuthentication(authentication);

        filterChain.doFilter(request, response);
    }

}

Após criamos a classe TokenAuthenticationService que será utilizada para fazer a verificação do usuário e senha com base em sua configuração.

package /jwt/service
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

public class TokenAuthenticationService {

    private static final long EXPIRATIONTIME = 864000000;
    private static final String SECRET = "MySecreteApp";
    private static final String TOKEN_PREFIX = "Bearer";
    private static final String HEADER_STRING = "Authorization";

    public static void addAuthentication(HttpServletResponse res, String username) {
        String JWT = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();

        String token = TOKEN_PREFIX + " " + JWT;
        res.addHeader(HEADER_STRING, token);

        try {
            res.getOutputStream().print(token);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Authentication getByToken(String token) {
        String user = Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                .getBody()
                .getSubject();

        return user != null ? new UsernamePasswordAuthenticationToken(user, null, null) : null;
    }

    public static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            return getByToken(token);
        }
        return null;
    }
}

Para finalizar, configuramos o spring security para fazer uso das classes que criamos:

package /config
import com.example.demo.jwt.filter.JWTAuthenticationFilter;
import com.example.demo.jwt.filter.JWTLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
		httpSecurity.csrf().disable().authorizeRequests()
			.antMatchers("/home").permitAll()
			.antMatchers(HttpMethod.POST, "/login").permitAll()
			.anyRequest().authenticated()
			.and()
			
			// filtra requisições de login
			.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
			
			// filtra outras requisições para verificar a presença do JWT no header
			.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// cria uma conta default
		auth.inMemoryAuthentication()
			.withUser("admin")
			.password("password")
			.roles("ADMIN");
	}
}

Pronto, já pode ser realizado os testes e verificar o bloqueio das requisições sempre que não for enviado o token.

Configuração dos Cors

Em outro post já falei sobre os cors Configurar os cors no Spring Boot, porém, já deixo o código caso queira permitir que APIs em javascript possam acessar este serviço.

package /config
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        String origin = request.getHeader("Origin");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin", origin != null && origin.contains("ws") ? "" : origin );
        response.setHeader("Vary", "Origin");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Authorization, Content-Type, Accept, X-CSRF-TOKEN");
        response.setHeader("Access-Control-Max-Age", "3600");
        if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    public void destroy() {
    }

    public void init(FilterConfig config) throws ServletException {
    }
}

Agora sim, já pode acessar usando Angula, vue ou qualquer outro framework de single page.

Estrutura final do projeto

A estrutura final do projeto ficou da seguinte forma:

Estrutura Final do Projeto Spring Boot com JWT

Usando o ARC para testar

Para testar o login e o acesso ao rest podemos utilizar o Advanced REST client (ARC), uma extenção do chrome que pode ser instalada facilmente.

Primeiro abra ARC e preencha os dados conforme a imagem abaixo:

ARC teste login 01

Lembrando que estou usando a porta 18085 para fazer os testes, se estiver usando a porta 8080, informe esta.

Perceba que foi retornado uma mensagem informando que o acesso não é permitido. Então vamos para a segunda etapa que é a realização do login, veja:

ARC teste login 02

Obtido o token que está sendo mostrado em Raw agora podemos copiar e adicionar no cabeçalho da nossa primeira requisição para verificar o resultado. Veja:

ARC teste login 03

Pronto recebemos o retorno conforme o esperado.

Duvidas?

Deixe suas dúvidas nos comentários que te ajudaremos a resolver seu problema.

Não esqueca de deixar suas dúvidas nos comentários e compartilhar este post.


Publique seu post no Receitas de Código

Aguarde, estamos trabalhando para que você possa publicar sua postagem no Receitas de Código!