Implementando Autenticação e Autorização Multirrole com Spring Security e JWT

Configurando Autenticação Multirrole com Spring Security e JWT

Este artigo explora estratégias para implementar um sistema de autenticação e autorização que gerencie múltiplos tipos de usuários — por exemplo, estudantes, empresas e administradores — em uma aplicação Spring Boot, utilizando Spring Security e JSON Web Tokens (JWT).

Abordagem 1: Unificação de Credenciais com Identificador Global

Inicialmente, um projeto pode ser concebido com tabelas de banco de dados separadas para cada tipo de usuário (e.g., alunos, empresas, administradores), cada uma contendo informações específicas do seu perfil (ID, nome de usuário, senha, e-mail, etc.). Embora cada tabela possa ter atributos distintos, a autenticação requer um conjunto comum de credenciais.

Para otimizar o processo de autenticação, é recomendável criar uma tabela central de credenciais ou usuarios_base. Esta tabela armazenaria informações comuns a todos os usuários, como id_global (um identificador único para cada usuário no sistema), nome_usuario e senha_hash. Adicionalmente, um campo tipo_usuario (e.g., 'ESTUDANTE', 'EMPRESA', 'ADMIN') pode ser incluído para identificar a qual entidade específica o registro de credencial se refere.

As tabelas específicas de cada perfil (alunos, empresas, administradores) manteriam suas colunas únicas e fariam referência ao id_global da tabela de credenciais como chave estrangeira. Isso garante uma fonte única de verdade para a autenticação, enquanto permite flexibilidade no armazenamento de dados específicos de cada perfil.

Abordagem 2: Incorporando o Tipo de Usuário no JWT

Independentemente da estrutura de banco de dados escolhida, é crucial que o JWT, após a autenticação, contenha informações suficientes para identificar não apenas o usuário, mas também o seu tipo. Isso permite que a camada de autorização tome decisões baseadas na função do usuário, mesmo que os IDs possam se sobrepor em tabelas separadas ou para simplificar a lógica de busca.

Modificando a Geração do JWT

Ao criar o token JWT, além do identificador único do usuário, devemos incluir explicitamente o seu tipo. Isso pode ser feito concatenando ambos no campo subject (assunto) do token ou adicionando uma reivindicação (claim) personalizada.

Exemplo de alteração na lógica de geração de token (arquivo backend/util/GeradorTokenJwt.java):


import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims; // Para o método decodificarToken

import java.util.Date;
import java.util.UUID;

public class GeradorTokenJwt {

    // A chave secreta deve ser forte e armazenada com segurança, idealmente via variáveis de ambiente.
    private static final String SECRET_KEY = "SuaChaveSecretaFortementeSeguraParaAssinaturaDeJWTs"; // Exemplo

    private static JwtBuilder obterConstrutorBase(String assunto, String idToken) {
        long tempoAtual = System.currentTimeMillis();
        return Jwts.builder()
                .setId(idToken)
                .setSubject(assunto)
                .setIssuedAt(new Date(tempoAtual))
                // Definir tempo de expiração, e.g., 24 horas a partir de agora
                // .setExpiration(new Date(tempoAtual + (24 * 60 * 60 * 1000)))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY);
    }

    private static String gerarIdUnico() {
        return UUID.randomUUID().toString();
    }

    /**
     * Gera um token JWT contendo o ID do usuário e seu tipo.
     * O 'subject' do token será uma string formatada como "idUsuario,tipoUsuario".
     *
     * @param idUsuario O identificador único do usuário (e.g., ID do banco de dados).
     * @param tipoUsuario O tipo de perfil do usuário (e.g., "ESTUDANTE", "EMPRESA", "ADMIN").
     * @return O token JWT assinado.
     */
    public static String gerarToken(String idUsuario, String tipoUsuario) {
        String assuntoToken = idUsuario + "," + tipoUsuario; // Formato "id,tipo"
        JwtBuilder construtor = obterConstrutorBase(assuntoToken, gerarIdUnico());
        return construtor.compact(); // Finaliza a construção e compacta o token
    }

    /**
     * Decodifica um token JWT e retorna suas reivindicações (claims).
     *
     * @param token O token JWT a ser decodificado.
     * @return Um objeto Claims contendo as reivindicações do token.
     * @throws io.jsonwebtoken.SignatureException Se a assinatura do token for inválida.
     * @throws io.jsonwebtoken.ExpiredJwtException Se o token estiver expirado.
     * @throws io.jsonwebtoken.MalformedJwtException Se o token não estiver bem formatado.
     */
    public static Claims decodificarToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

Modificando a Lógica de Parsing do JWT

Ao receber um token JWT, o filtro de autenticação precisa decodificá-lo e extrair tanto o ID do usuário quanto o seu tipo para construir o contexto de segurança do Spring Security.

Exemplo de alteração no filtro de autenticação (arquivo backend/config/filtro/FiltroAutenticacaoJwt.java):


import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
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;
import java.util.List;
import java.util.stream.Collectors;

// Importa a classe de utilitário JWT
import seu.pacote.util.GeradorTokenJwt;

public class FiltroAutenticacaoJwt extends OncePerRequestFilter {

    // O GeradorTokenJwt é usado aqui estaticamente, mas em um cenário real pode ser injetado.

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String cabecalhoAutorizacao = request.getHeader("Authorization");

        // Verifica se o cabeçalho de autorização existe e começa com "Bearer "
        if (cabecalhoAutorizacao == null || !cabecalhoAutorizacao.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String tokenJwtPuro = cabecalhoAutorizacao.substring(7); // Remove "Bearer "

        String idUsuario;
        String tipoUsuario;

        try {
            // Decodifica o token usando o utilitário
            Claims reivindicacoes = GeradorTokenJwt.decodificarToken(tokenJwtPuro);
            String assuntoDoToken = reivindicacoes.getSubject();

            // Divide o assunto para obter ID e Tipo
            String[] partesDoAssunto = assuntoDoToken.split(",");

            if (partesDoAssunto.length != 2) {
                logger.warn("Assunto do JWT com formato inválido: " + assuntoDoToken);
                throw new IllegalArgumentException("Formato de 'subject' do JWT inválido.");
            }

            idUsuario = partesDoAssunto[0];
            tipoUsuario = partesDoAssunto[1];

            // TODO: Em um sistema real, você buscaria UserDetails do banco de dados
            // e construiria as GrantedAuthority a partir dos roles/tipos do usuário.
            // Por simplicidade, criamos uma lista de autoridades baseada no tipo.
            List<grantedauthority> authorities = Collections.singletonList(
                new SimpleGrantedAuthority("ROLE_" + tipoUsuario.toUpperCase())
            );

            // Cria o objeto de autenticação para o Spring Security
            UsernamePasswordAuthenticationToken autenticacao = new UsernamePasswordAuthenticationToken(
                    idUsuario, // Principal pode ser o ID do usuário
                    null,      // Credenciais (senha) não são necessárias após autenticação JWT
                    authorities // Autoridades/Roles do usuário
            );
            // Define os detalhes da autenticação (endereço IP, sessão ID, etc.)
            autenticacao.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // Define a autenticação no contexto de segurança do Spring
            SecurityContextHolder.getContext().setAuthentication(autenticacao);

        } catch (Exception e) {
            logger.error("Falha ao processar token JWT: " + e.getMessage(), e);
            // Uma resposta de erro pode ser mais granular em produção, e.g., 403 para token inválido
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401
            return;
        }

        filterChain.doFilter(request, response);
    }
}
</grantedauthority>

Ajustando a Lógica de Consulta ao Banco de Dados

Uma vez que o filtro JWT extraiu o ID do usuário e seu tipo, a implementação de UserDetailsService (ou um serviço equivalente) precisará usar essas duas informações para carergar os detalhes completos do usuário. Em vez de um único método loadUserByUsername, pode ser necessário um método mais genérico ou múltiplos métodos especializados:

  • Crie um serviço que aceite tanto o ID quanto o tipo de usuário para buscar informações (e.g., servicoUsuario.carregarDetalhes(String id, String tipo)).
  • Dentro desse serviço, use uma lógica condicionla (if/else ou um Map de provedores) para direcionar a consulta à tabela correta (alunos, empresas, administradores).
  • Após carregar os dados específicos do perfil, adapte-os para um objeto UserDetails que o Spring Security possa entender, garantindo que as autoridades (roles) corretas sejam atribuídas.

Validação no Frontend e Backend

É fundamental que a validação de tipo de usuário seja reforçada em todas as camadas da aplicação:

  • Backand: Utilize anotações de segurança do Spring Security, como @PreAuthorize("hasRole('ADMIN')") ou @PreAuthorize("hasAnyRole('ESTUDANTE', 'EMPRESA')"), diretamente nos métodos dos controladores ou serviços. Isso garante que apenas usuários com os tipos de perfil e papéis adequados possam acessar determinados endpoints.
  • Frontend: Adapte a interface do usuário para exibir funcionalidades ou rotas específicas apenas se o tipo de usuário autenticado permitir. Por exemplo, um usuário com perfil 'ESTUDANTE' não deve ver links ou botões que levariam a páginas de gerenciamento de 'ADMIN'. Essa validação é principalmente para UX, mas complementa a segurança do backend.

Tags: SpringSecurity jwt SpringBoot autenticacao Autorizacao

Publicado em 6-13 21:03 por Thomas