Do Zero à Segurança Máxima: Explorando JWT com Spring Security

Spring Security With JWT for REST API 


Segurança de aplicações é uma prioridade absoluta. Com a crescente demanda por sistemas distribuídos e a necessidade de proteger dados sensíveis, autenticação e autorização se tornam pilares fundamentais para qualquer aplicação robusta. É aqui que surgem ferramentas poderosas como o JSON Web Token (JWT) e o Spring Security, que permitem implementar mecanismos seguros de autenticação e controle de acesso em aplicações Java. Neste artigo, exploraremos como essas duas tecnologias funcionam e como elas podem ser integradas para alcançar segurança máxima.

 

O que é JWT?

JSON Web Token (JWT) é um padrão aberto (RFC 7519) utilizado para transmitir informações de forma compacta e segura entre diferentes partes, geralmente em um formato JSON. Ele é amplamente utilizado em autenticação e autorização de APIs RESTful.

Um JWT é composto de três partes principais:

  1. Header: Contém informações sobre o tipo de token (JWT) e o algoritmo de assinatura utilizado, como HMAC SHA256 ou RSA.
  2. Payload: Carrega as informações (claims) que descrevem o usuário ou dados adicionais, como permissões e escopos.
  3. Signature: Garante a integridade do token, usando uma chave secreta ou privada para assinar as duas partes anteriores.

O que é Spring Security?

Spring Security é um framework robusto de autenticação e controle de acesso, amplamente utilizado em aplicações Java. Ele é parte integrante do ecossistema Spring e fornece um conjunto de ferramentas e extensões para proteger aplicações de forma flexível e configurável.

Algumas características-chave do Spring Security incluem:

  • Autenticação: Suporte a múltiplos mecanismos de autenticação, como logins baseados em formulários, autenticação básica, OAuth2, e, claro, JWT.
  • Autorização: Controle granular de acesso a recursos, permitindo a definição de permissões e regras de acesso específicas.
  • Proteção contra vulnerabilidades: Implementação de boas práticas de segurança, como prevenção contra ataques CSRF (Cross-Site Request Forgery) e cabeçalhos de segurança HTTP.

Ao combinar JWT e Spring Security, é possível criar soluções de autenticação modernas, seguras e eficientes, eliminando a necessidade de armazenamento de estado no servidor e melhorando a escalabilidade da aplicação.

Nos próximos tópicos, veremos como configurar e implementar essa combinação poderosa em sua aplicação Spring Boot.

 Como implementar em um Projeto 

Para isso vá em https://start.spring.io/ para termos uma aplicação segura precisamos de duas dependencias essenciais sendo eles o spring WEB e o spring security cada um deles tem carateristicas sendo eles o

Spring Web

Essa dependência é necessária para criar uma aplicação baseada em APIs RESTful, permitindo lidar com requisições HTTP e fornecer respostas JSON.

  • Características principais:
    • Criação de endpoints REST.
    • Manipulação de requisições HTTP (GET, POST, PUT, DELETE).
    • Conversão automática entre objetos Java e JSON.

Spring Security

Como discutido anteriormente, o Spring Security é o framework que será responsável pela autenticação e autorização na aplicação, garantindo controle sobre o acesso aos recursos protegidos.

  • Características principais:
    • Configuração de autenticação personalizada (usando JWT, por exemplo).
    • Definição de regras de autorização para proteger endpoints.
    • Implementação de boas práticas de segurança, como proteção contra CSRF e configuração de cabeçalhos de segurança.
    •  
     
Após baixar o projeto e inicializar, entre na porta 8080 e vera que ele pedira credenciais de acesso, mesmo não termos configurado ele já gera uma espécie de login isso é uma característica do spring security e suas funcionalidades. 

Autenticação Simples

Uma das características do Spring Security é proteger todas as rotas da aplicação por padrão, exigindo autenticação para qualquer requisição. Embora isso seja excelente para segurança, em alguns cenários, como APIs públicas ou páginas que não requerem autenticação, essa abordagem pode ser inconveniente.

Autenticação Simples permite configurar rotas específicas que não exigem autenticação, proporcionando uma melhor usabilidade para o sistema enquanto mantém o restante das rotas protegidas.

Exemplo de Cenário

  • Endpoints de login e cadastro devem ser acessíveis sem autenticação.
  • Endpoints protegidos, como dados do usuário, devem exigir credenciais válidas.

Neste tópico, veremos como configurar essas permissões no Spring Security

 

Configurando o Spring Security para Autenticação Simples

Crie a Classe de Configuração de Segurança
No pacote config, crie a classe SecurityConfig que irá personalizar o comportamento do Spring Security.


package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // Desabilitando CSRF para simplificar o exemplo
        http.csrf().disable();

        // Configurando permissões de rotas
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/register").permitAll() // Rotas públicas
                .anyRequest().authenticated() // Qualquer outra rota exige autenticação
        );

        // Configurando login básico (para desenvolvimento/teste)
        http.httpBasic();

        return http.build();
    }
}

Definindo Rotas Públicas
No método securityFilterChain, as rotas /login e /register são configuradas para permitir acesso sem autenticação, utilizando o método permitAll()
 
 

Controladores para Login e Registro

Crie controladores simples para os endpoints de login e registro:

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

@GetMapping("/login")
public String login() {
return "Esta é a rota de login pública!";
}

@GetMapping("/register")
public String register() {
return "Esta é a rota de registro pública!";
}

@GetMapping("/secure")
public String secure() {
return "Este é um endpoint protegido!";
}
}


Acessando Rotas Públicas
Faça uma requisição para http://localhost:8080/login ou http://localhost:8080/register.
Você verá a mensagem configurada sem precisar fornecer credenciais.

Acessando Rotas Protegidas
Faça uma requisição para http://localhost:8080/secure. O servidor retornará um 401 Unauthorized se as credenciais não forem fornecidas.

Implementando Spring Security com JWT

Integrar o Spring Security com JWT (JSON Web Token) é um passo essencial para criar APIs modernas, seguras e escaláveis. Com JWT, você elimina a necessidade de sessões baseadas no servidor, pois o token carrega todas as informações necessárias para autenticação e autorização.

A configuração do Spring Security será ajustada para suportar a validação de tokens JWT:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/login", "/auth/register").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}


O filtro JWT interceptará as requisições, validará o token e configurará o contexto de segurança:

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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 java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, UserDetailsServiceImpl userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String jwt = authHeader.substring(7);
        String username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}


Este serviço será responsável por criar e validar tokens JWT.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.function.Function;

import io.jsonwebtoken.security.Keys;

@Service
public class JwtService {

    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public String generateToken(UserDetails userDetails) {
        return generateToken(Map.of(), userDetails);
    }

    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts.builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 horas
                .signWith(key)
                .compact();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    private boolean isTokenExpired(String token) {
        return extractClaim(token, Claims::getExpiration).before(new Date());
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}

Por fim configurar a controller configurar as requisições:

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final UserDetailsServiceImpl userDetailsService;

    public AuthController(AuthenticationManager authenticationManager, JwtService jwtService, UserDetailsServiceImpl userDetailsService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/login")
    public String login(@RequestBody AuthRequest authRequest) {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );
        UserDetails user = userDetailsService.loadUserByUsername(authRequest.getUsername());
        return jwtService.generateToken(user);
    }
}

Com essas configurações, sua aplicação estará segura e usando JWT para autenticação. Você pode estender essa base para incluir mais validações, roles e integrações externas.

Back-end Developer Java | Spring Boot | Quarkus | Microservices | SQL & NOSQL | SOLID | Design Patterns

Comments

Popular posts from this blog

Spring VS Micronaut VS Quarkus

Spring + Redis: Como melhorar sua aplicação com caching

Java + Kafka, Como essa parceria fucniona ?