O Spring Security é um projeto do eccosistema Spring que oferece mecanismos de autenticação e autorização, além de outras funcionalidades de segurança para aplicações Java. Ele pode ser integrado tanto a aplicações web baseadas em Thymeleaf ou outro template engine (utilizando sessões e formulários de login) quanto a APIs REST, geralmente por meio de tokens JWT ou outros métodos de autenticação.

Antes de avançarmos, precisamos entender a diferença entre autenticação e autorização:

Autenticação é o processo de confirmar se o usuário é realmente quem afirma ser. Para isso, suas credenciais — geralmente nome de usuário e senha — são comparadas com as informações armazenadas no sistema. Se houver correspondência, o acesso é concedido.

Autorização é o processo de verificar se um usuário já autenticado tem permissão para acessar determinada área ou recurso do sistema, ou ainda para executar operações específicas. No Spring Security, isso normalmente é feito por meio da interface GrantedAuthority, que representam permissões individuais como "LEITURA" ou "ESCRITA_PRODUTO", sempre definidas como strings. Além dessas, existe um tipo especial de autoridade chamada ROLE. Uma role funciona como um “papel” ou “função” atribuída ao usuário, englobando diversas permissões relacionadas a diferentes partes do sistema.

Na prática, uma role também é uma GrantedAuthority, mas com o prefixo ROLE_. Diversos métodos do Spring Security que trabalham com roles apenas acrescentam automaticamente esse prefixo à string informada.

Para demonstrar o uso do Spring Security com Thymeleaf, vamos utilizar um pequeno cadastro de produtos de exemplo].

Esse projeto oferece as operações básicas de listagem, criação, remoção e edição de produtos.

A partir dele, iremos configurar o mecanismo de autenticação, definindo um usuário que pode assumir três papéis no sistema: ADMIN, EDITOR e VISITANTE.

Cada role determina quais URLs o usuário pode acessar, restringindo ou liberando o uso das funcionalidades conforme suas permissões.

A tabela a seguir mostra quais URLs exigem cada role para serem acessadas:

UrlDescriçãoROLE
/produtosListar todos produtosVISITANTE, EDITOR, ADMIN
/produtos/adicionarFormulário de criação de produtoEDITOR, ADMIN
/produtos/salvarUrl para salvar dados vindo do formulário de criaçãoEDITOR, ADMIN
/produtos/{id}/deletarDeleta um produtoADMIN
/produtos/{id}/editarFormulário de edição de produtoEDITOR, ADMIN
/produtos/{id}/atualizarUrl para salvar as alterações vinda do formulário de ediçãoEDITOR, ADMIN

O primeiro passo é adicionar a dependência do Spring Security ao projeto no pom.xml:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-test</artifactId>
	<scope>test</scope>
</dependency>

Autenticação

Após adicionar as dependências, você perceberá que nenhuma URL do projeto pode ser acessada livremente. Isso ocorre porque, por padrão, o Spring Security protege todas as rotas. Ao tentar acessar qualquer URL, o framework exibe automaticamente um formulário de login padrão.

Formulário padrão de autenticação do Spring Security

O usuário padrão é user e a senha é gerada automaticamente a cada inicialização do projeto. Para descobri-la, verifique os logs de inicialização, onde deverá aparecer algo semelhante ao trecho abaixo:

Using generated security password: 2016d16f-bd8e-464d-8a1b-6053a0b0780a

Agora vamos personalizar a autenticação, criando nossos próprios usuários. Para isso, precisamos criar uma classe de configuração do Spring Security, que deve ser anotada com @Configuration e @EnableWebSecurity.

O próximo passo é criar dois Beans. O primeiro será o PasswordEncoder, responsável por aplicar o hash nas senhas dos usuários — afinal, nunca devemos armazenar senhas em texto plano. Para isso, retornamos uma instância de BCryptPasswordEncoder, que é a implementação recomendada pelo Spring Security.

O segundo Bean que precisamos criar é o UserDetailsService, responsável por fornecer os dados dos usuários do sistema durante o processo de autenticação. Essa interface define o método loadUserByUsername(String username), que deve retornar um objeto UserDetails contendo o nome de usuário, a senha e as roles/authorities atribuídas. Caso o usuário não seja encontrado, o método deve lançar a exceção UsernameNotFoundException.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder){
        UserDetails admin =
			 User
                .builder()
				.username("admin")
				.password( passwordEncoder.encode("admin"))
				.roles("ADMIN")
				.build();
		UserDetails editor = User
                .builder()
				.username("editor")
				.password( passwordEncoder.encode("editor"))
				.roles("EDITOR")
				.build();
		UserDetails visitante =User
                .builder()
				.username("visitante")
				.password( passwordEncoder.encode("visitante"))
				.roles("VISITANTE")
				.build();

		return new InMemoryUserDetailsManager(admin, editor, visitante);
    }

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

Na classe que implementamos, retornamos um InMemoryUserDetailsManager, que é uma implementação pronta do UserDetailsService e recebe objetos UserDetails no construtor. Essa abordagem é indicada para protótipos e exemplos, mas mais adiante veremos como configurar um UserDetailsService integrado a um banco de dados.

No exemplo atual, criamos três usuários de teste: admin, editor e visitante, atribuindo a cada um um username, uma senha (codificada com o PasswordEncoder) e uma role, utilizando o builder da classe User. Se necessário, também poderíamos associar múltiplas roles a um mesmo usuário, utilizando varargs. Exemplo: .roles("ADMIN", "EDITOR").

Agora, basta reiniciar a aplicação e acessar o sistema utilizando uma das credenciais configuradas. O login será feito através do formulário padrão gerado automaticamente pelo Spring Security.

Também poderíamos atribuir permissões utilizando o método .authorities(). Nesse caso, no entanto, seria necessário prefixar manualmente o nome da role com "ROLE_". Exemplo: .authorities("ROLE_ADMIN").

Vale lembrar que UserDetails é apenas uma interface. No exemplo acima utilizamos uma implementação pronta fornecida pelo Spring Security, mas nada impede que criemos nossa própria classe de usuário do sistema implementando essa interface.

Como já mencionado, o InMemoryUserDetailsManager não é indicado para uso em produção, sendo adequado apenas em testes e protótipos. Para cenários reais, uma alternativa é o JdbcUserDetailsManager, que recebe um DataSource como parâmetro no construtor e realiza a busca dos usuários diretamente no banco de dados. Esse DataSource pode ser injetado no método de criação do bean, permitindo que o JdbcUserDetailsManager utilize a configuração já definida no projeto como no arquivo application.properties.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource){
        return new JdbcUserDetailsManager(dataSource);
    }

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

}

Como o JdbcUserDetailsManager acessa diretamente as credenciais armazenadas no banco, é necessário que exista um schema no formato esperado pelo Spring Security. Para este exemplo, podemos adicionar o seguinte SQL no arquivo resources/import.sql, que cria a tabela de usuários (users) e a tabela de permissões (authorities), responsáveis por associar os usuários às suas respectivas roles. As senhas já estão codificadas com bcrypt e, para simplificação, são iguais ao próprio nome de usuário.

// utilizando o SQLite, adapte para seu banco

CREATE TABLE users ( id	INTEGER, username TEXT NOT NULL, password TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(id AUTOINCREMENT) );

CREATE TABLE authorities (id INTEGER NOT NULL, username	TEXT NOT NULL, authority TEXT NOT NULL, PRIMARY KEY(id AUTOINCREMENT) );

INSERT INTO users(username, password) VALUES('admin', '$2a$10$YAakidKSM8EVuY26ZorLKexj8Rli8V6Hf9eiKQlO92IRWpZqAjngC');
INSERT INTO users(username, password) VALUES('editor', '$2a$10$xY/dbJOSJjtzQ7Cu4qUxIu.Jdskf1kOF84B2BoIzkfqRYEriz2cJW');
INSERT INTO users(username, password) VALUES('visitante', '$2a$10$WkHu3sY9dLTu6oNELuErUe0WeaiiZVKjIWWKRvZtck0IulDihu3kO');

INSERT INTO authorities(username, authority) VALUES('admin', 'ADMIN');
INSERT INTO authorities(username, authority) VALUES('editor', 'EDITOR');
INSERT INTO authorities(username, authority) VALUES('visitante', 'VISITANTE');

Após configurar o WebSecurityConfig e adicionar o script import.sql, basta reiniciar a aplicação para que a autenticação com usuários vindos do banco de dados esteja disponível.

Caso o schema do seu banco de dados não seja compatível com os nomes de tabelas e colunas esperados pelo JdbcUserDetailsManager, é possível sobrescrever as consultas SQL utilizadas por ele. Para isso, podem ser utilizados os métodos setUsersByUsernameQuery, responsável por recuperar as credenciais do usuário, e setAuthoritiesByUsernameQuery, que busca as roles associadas. É importante destacar que o JdbcUserDetailsManager depende da ordem dos campos retornados pela query, e não dos nomes das colunas, então se a query retorna em uma ordem diferente ela irá falhar.

// exemplo se a tabela tiver a coluna email em vez da username, senha em 
// vez de password e habilitado em vez de enabled na tabela de usuarios 
// e email e permissao em vez de username e  authority  na tabela de roles

@Bean
public UserDetailsService userDetailsService(DataSource dataSource){
    JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);

    userDetailsManager.setUsersByUsernameQuery("select email,senha,habilitado from usuarios where email = ?");
    userDetailsManager.setAuthoritiesByUsernameQuery("select email, permissao from roles where email = ?");

    return userDetailsManager;
}

Uma abordagem ainda mais flexível é criar sua própria implementação da interface UserDetailsService, sobrescrevendo o método loadUserByUsername, que deve retornar um objeto do tipo UserDetails. Na maioria dos casos, sua aplicação já conta com um cadastro de usuários em uma estrutura personalizada. Nesse cenário, basta utilizar um Repository para buscar o usuário no banco de dados e então converter sua entidade de usuário para um objeto UserDetails(ou sua entidade usuário implementar diretamente a interface UserDetails), retornando-o em seguida. Vamos ver um exemplo prático, começando pela criação de uma classe de entidade Usuario.

@Entity
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String nome;
    private String email;
    private String username;
    private String senha;

    
    @Enumerated(EnumType.STRING)
    private Role role;

    public enum Role {
        ADMIN,
        EDITOR,
        VISITANTE
    }

    // gets e sets
}

A entidade é bastante simples: armazenamos o papel do usuário como um enum, onde cada usuário possui apenas um único role. Essa abordagem foi adotada para simplificar o exemplo, mas nada impede que um usuário tenha múltiplos roles. Nesse caso, poderíamos utilizar uma lista de roles, desde que o relacionamento fosse mapeado corretamente na entidade e tratado no service responsável pela autenticação.

Agora, vamos criar o Repository, que será responsável por acessar os dados da entidade Usuario no banco de dados.

public interface UsuarioRepository extends JpaRepository<Usuario, Integer> {

    Optional<Usuario> findByUsername(String username);
}

Neste ponto, seguimos um padrão comum no Spring Data JPA: criamos um Repository que estende JpaRepository e declaramos um método findByUsername, responsável por recuperar um usuário a partir do seu username.

Em seguida, vamos implementar nossa própria versão do UserDetailsService.

@Service
public class UsuarioDetailsService implements UserDetailsService  {

    private final UsuarioRepository usuarioRepository;
    
    public UsuarioDetailsService(UsuarioRepository usuarioRepository) {
        this.usuarioRepository = usuarioRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Usuario usuario = this.usuarioRepository.findByUsername(username)
            .orElseThrow( () -> new UsernameNotFoundException(username+" não encontrado") );

        UserDetails user = User.builder()
                    .username(usuario.getUsername())
                    .password(usuario.getSenha())
                    .roles(usuario.getRole().name())
                    .build();

        return user;
    }

}

Aqui, a classe foi anotada com @Service, o que permite que o Spring a detecte automaticamente como um componente. Dessa forma, não é mais necessário registrar manualmente um bean correspondente em WebSecurityConfig. Portanto, caso você tenha declarado anteriormente um método para expor o UserDetailsService como bean, lembre-se de removê-lo para evitar conflitos.

Recebemos o UsuarioRepository por injeção de dependência via construtor e implementamos o método loadUserByUsername. Dentro dele, utilizamos o findByUsername para buscar o usuário pelo username informado. Como o método retorna um Optional, encadeamos a chamada com orElseThrow, garantindo que, caso o usuário não seja encontrado, seja lançada a exceção UsernameNotFoundException, conforme exige a assinatura do método da interface. Em seguida, utilizamos o User.builder() do Spring Security para criar um objeto UserDetails, preenchendo seus atributos (username, password e roles) com os valores obtidos da nossa entidade Usuario. Por fim, retornamos o objeto construído.

Agora, para realizar os testes, precisamos inserir alguns usuários no banco de dados. No nosso caso, isso pode ser feito adicionando os comandos INSERT diretamente no arquivo import.sql.

INSERT INTO usuario(id,  nome, username, senha, email, role) VALUES(1, 'Administrador', 'admin', '$2a$10$YAakidKSM8EVuY26ZorLKexj8Rli8V6Hf9eiKQlO92IRWpZqAjngC', 'admin@botecodigital.dev.br', 'ADMIN' );
INSERT INTO usuario(id,  nome, username, senha, email, role) VALUES(2, 'Editor', 'editor', '$2a$10$xY/dbJOSJjtzQ7Cu4qUxIu.Jdskf1kOF84B2BoIzkfqRYEriz2cJW', 'editor@botecodigital.dev.br', 'EDITOR');
INSERT INTO usuario(id,  nome, username, senha, email, role) VALUES(3, 'Visitante', 'visitante', '$2a$10$WkHu3sY9dLTu6oNELuErUe0WeaiiZVKjIWWKRvZtck0IulDihu3kO', 'visitante@botecodigital.dev.br', 'VISITANTE');

Reinicie a aplicação e, a partir de agora, os dados de autenticação serão buscados diretamente pelo nosso serviço personalizado.

Definindo as regras de Autorização

Agora vamos configurar as regras de autorização para as rotas da aplicação, ou seja, definir quais usuários ou perfis podem acessar determinados recursos.

Essa configuração é feita por meio do SecurityFilterChain, que permite declarar de forma fluente e centralizada as políticas de segurança de cada endpoint.

O SecurityFilterChain nada mais é do que uma sequência de filtros de segurança que o Spring Security executa em cada requisição, antes mesmo que ela chegue ao controller responsável pelo processamento.

Por padrão, diversos filtros já são aplicados automaticamente. Para visualizar em detalhes a execução desses filtros, basta ajustar o nível de log do Spring Security para DEBUG.

logging.level.org.springframework.security=DEBUG

Em seguida, basta verificar nos logs de inicialização a presença da seguinte entrada:

DEBUG 8956 --- [security-form] [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultResourcesFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter

Ao analisar os filtros exibidos nos logs, percebemos que cada um deles possui um papel específico dentro do fluxo de segurança. Embora a cadeia seja extensa, alguns filtros se destacam por sua importância prática no controle de autenticação e autorização. A tabela abaixo alguns dos principais filtros:

FiltroDescrição
CsrfFilter
Responsável por proteger a aplicação contra ataques de Cross-Site Request Forgery (CSRF), garantindo que requisições sensíveis só sejam executadas quando acompanhadas de um token válido.
UsernamePasswordAuthenticationFilterLida com o processo de autenticação via formulário HTML, validando credenciais de usuário e senha. Caso um usuário não autenticado tente acessar um recurso protegido, este filtro o redireciona para a página de login.
AuthorizationFilterControla a autorização de acesso aos recursos da aplicação. Por padrão, exige que todas as requisições estejam autenticadas antes de prosseguir.
LogoutFilter Gerencia o processo de logout, invalidando a sessão do usuário e limpando o contexto de segurança quando a URL configurada para logout é acessada.

Para definir quais endpoints exigem autenticação, quais permissões (roles) terão acesso a eles e o mecanismo de autenticação utilizado, é necessário registrar um bean do tipo SecurityFilterChain dentro da classe de configuração WebSecurityConfig.

@Bean 
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    return http.build();
}

Ao verificar os logs de inicialização da aplicação, é possível identificar os filtros que o Spring Security adiciona automaticamente à cadeia de execução após a configuração do SecurityFilterChain.

DEBUG 10972 --- [security-form] [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter

Ao observarmos o log, percebemos que alguns filtros padrão não foram adicionados. Isso acontece porque, ao declararmos explicitamente um SecurityFilterChain, o Spring Security entende que seremos responsáveis por definir as configurações desejadas e adiciona somente alguns dos filtros automaticamente e adiciona outros a medida que aplicamos nossas configurações. Essas configurações são aplicadas por meio da API fluente do objeto HttpSecurity, onde podemos habilitar, desabilitar ou personalizar cada aspecto da segurança.

O primeiro passo será habilitar o suporte a login via formulário. Para isso, utilizaremos o método formLogin() do objeto HttpSecurity, que ativa a autenticação baseada em um formulário HTML padrão do Spring Security.

@Bean 
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

    http.formLogin( Customizer.withDefaults());

    return http.build();
}

Utilizamos o método formLogin com as configurações padrão. Esse método ativa o suporte ao login baseado em formulário e, além disso, permite customizações, como definir a URL da página de login personalizada, o endpoint de autenticação e os redirecionamentos após login ou falha de autenticação. Esses detalhes de personalização serão explorados mais adiante.

Para configurar as regras de autorização dos endpoints, utilizamos o método authorizeHttpRequests, que recebe uma lambda com um objeto authorize. Esse objeto fornece métodos para definir quais URLs ou padrões de URL serão protegidos ou liberados, além de permitir especificar as condições de acesso, como autenticação obrigatória ou a necessidade de determinadas roles ou authorities para acessar um recurso.

É importante destacar que as URLs e padrões são avaliados na ordem em que são definidos. A primeira correspondência encontrada aplica as condições configuradas, enquanto as regras subsequentes para a mesma URL são ignoradas. Por isso, devemos definir as regras das correspondências mais específicas para as mais genéricas, evitando que uma regra ampla sobrescreva uma regra mais restrita.

Para especificar as URLs ou padrões de endpoints aos quais desejamos aplicar regras de autorização, podemos utilizar um dos métodos disponíveis no builder fornecido pelo authorizeHttpRequests().

anyRequest()

O método anyRequest() seleciona todas as requisições que não foram correspondidas por regras definidas anteriormente. Funciona como um “coringa” no final da configuração, garantindo que qualquer solicitação que não coincida com um requestMatcher específico seja protegida ou liberada conforme a regra aplicada a ele.

requestMatchers(String… patterns)

O método requestMatchers(String… patterns) permite definir padrões de URL como Strings, possibilitando o uso de curingas (* e **) para correspondência flexível.

  • O curinga * representa exatamente um nível de caminho. Exemplo: "/produtos/*/editar" corresponde a "/produtos/1/editar" ou "/produtos/abc/editar", mas não a "/produtos/1/teste/editar".
  • O curinga ** representa múltiplos níveis de caminho, incluindo zero níveis adicionais. Exemplo: "/produtos/**" corresponde a "/produtos", "/produtos/adicionar", "/produtos/1/editar" ou qualquer outro caminho que comece com "/produtos/".

requestMatchers(HttpMethod method, String pattern)

O método requestMatchers(HttpMethod method, String pattern) funciona de forma semelhante ao requestMatchers(String… patterns), mas permite adicionar restrição pelo método HTTP. O método HTTP é especificado por uma constante de HttpMethod (como HttpMethod.GET, HttpMethod.POST, etc.).

Com isso, a correspondência ocorrerá apenas se tanto o padrão da URL quanto o método HTTP coincidirem. Exemplo: .requestMatchers(HttpMethod.POST, "/produtos/**") aplica a regra apenas para requisições POST cujo caminho comece com /produtos/.

requestMatchers(RegexRequestMatcher regexMatcher)

O método requestMatchers(RegexRequestMatcher regexMatcher) permite definir padrões de correspondência de URL usando expressões regulares (regex), em vez de simples curingas (* ou **).

Isso oferece maior flexibilidade para capturar formatos específicos de rota, como IDs numéricos, slugs ou combinações complexas.

Exemplo: .requestMatchers(new RegexRequestMatcher("/produtos/[0-9]+/editar", null))

Nesse caso, apenas URLs como /produtos/1/editar ou /produtos/25/editar serão correspondidas. URLs que não se encaixem no padrão, como /produtos/abc/editar, não serão afetadas.

requestMatchers(RequestMatcher requestMatcher)

O método requestMatchers(RequestMatcher requestMatcher) permite definir um matcher personalizado para uma requisição, oferecendo total liberdade para determinar as condições de correspondência.

Ele recebe um RequestMatcher, que pode ser implementado por meio de uma classe ou usando uma lambda. O parâmetro da lambda é um objeto HttpServletRequest, permitindo acessar qualquer informação da requisição, como parâmetros, cabeçalhos, cookies, método HTTP, URL completa, entre outros.

Exemplo:

.requestMatchers(request -> 
    "ok".equals(request.getParameter("param1"))
)

Nesse caso, a regra será aplicada apenas às requisições que possuam o parâmetro param1 com valor "ok". Exemplo /produtos?param1=ok

Definindo regras de autorização para endpoints

Após definir a correspondência de uma requisição usando um requestMatcher, devemos encadear métodos para definir as regras de autorização. A seguir estão os principais métodos disponíveis no Spring Security:

  • permitAll() – Permite o acesso à requisição sem qualquer restrição; o endpoint se torna público.
  • denyAll() – Bloqueia o acesso à requisição em todas as circunstâncias.
  • hasRole(String role) – Permite o acesso apenas se o usuário estiver autenticado e possuir o papel (role) especificado.
  • hasAnyRole(String… roles) – Permite o acesso se o usuário estiver autenticado e possuir pelo menos um dos papéis fornecidos.
  • hasAuthority(String authority) – Permite o acesso apenas se o usuário tiver a GrantedAuthority especificada.
  • hasAnyAuthority(String... authority) – Permite o acesso se o usuário estiver autenticado e possuir pelo menos uma das GrantedAuthority especificada.
  • authenticated() – Permite o acesso apenas para usuários autenticados, independentemente dos papéis. Usuários anônimos não são autorizados.

Agora podemos combinar os métodos que definem a correspondência das requisições com aqueles que estabelecem as regras de autorização, criando uma configuração completa de acesso para cada endpoint.

@Bean 
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

    http.authorizeHttpRequests( (authorize) ->{
        authorize
            .requestMatchers("/login").permitAll()
            .requestMatchers("/produtos").hasAnyRole("ADMIN", "EDITOR","VISITANTE")
            .requestMatchers("/produtos/*/deletar").hasRole("ADMIN")
            .requestMatchers("/produtos/**").hasAnyRole("ADMIN", "EDITOR")
            .anyRequest().authenticated();
        })
        .formLogin( Customizer.withDefaults());

    return http.build();
}

No nosso exemplo, a primeira correspondência definida é para a URL “/login”, concedendo acesso a qualquer usuário. Essa configuração é essencial, pois todos os usuários precisam ter acesso à página de login para realizar o processo de autenticação no sistema.

Em seguida, definimos a correspondência para a URL “/produtos”, permitindo acesso apenas a usuários autenticados que possuam um dos papéis “ADMIN”, “EDITOR” ou “VISITANTE”. Dessa forma, garantimos que somente perfis autorizados possam visualizar ou interagir com esse recurso.

Em seguida, configuramos a correspondência para o padrão “/produtos/*/deletar”, onde o caractere curinga * representa exatamente um nível da URL — nesse caso, o identificador do produto a ser deletado. Para essa rota, estabelecemos que apenas usuários autenticados com o papel ADMIN terão permissão de acesso.

Em seguida, configuramos o padrão de URL “/produtos/“**, utilizando o curinga **, que corresponde a zero ou mais níveis de caminho. Isso significa que qualquer rota iniciada por “/produtos” será contemplada. No nosso exemplo, esse padrão cobre URLs como: /produtos/adicionar, /produtos/salvar, /produtos/{id}/editar e /produtos/{id}/atualizar. Para essas rotas, definimos que o acesso será permitido apenas a usuários autenticados que possuam os papéis EDITOR ou ADMIN.

É importante destacar que o padrão “/produtos/“** também abrange a rota “/produtos/{id}/deletar”. No entanto, como essa operação exige uma autorização mais restrita — permitindo acesso apenas a usuários com o papel ADMIN —, a regra específica de exclusão precisa ser declarada antes da regra mais genérica. Isso ocorre porque o Spring Security avalia as correspondências na ordem em que foram definidas, aplicando a autorização da primeira regra válida encontrada e ignorando as seguintes.

Por fim, utilizamos o método anyRequest() para capturar todas as requisições que não corresponderam a nenhuma regra anterior. Para essas, definimos que apenas usuários autenticados poderão acessá-las, garantindo assim que nenhum endpoint fique exposto de forma inadvertida.

Personalizando o formulário de login

Como mencionado anteriormente, o Spring Security permite personalizar o formulário de login utilizando qualquer página HTML desejada. Para isso, utilizamos o método formLogin(), que pode receber uma lambda com um objeto do tipo FormLoginConfigurer. Esse objeto disponibiliza diversos métodos de configuração, possibilitando definir, por exemplo, a página de login personalizada, a URL de processamento da autenticação, bem como os redirecionamentos após sucesso ou falha no login.

.formLogin( (form) ->{
        form
            .loginPage("/usuarios/login")
            .loginProcessingUrl("/usuarios/login-process")
            .defaultSuccessUrl("/produtos")
            .failureUrl("/usuarios/login?error=true")
            .permitAll();
})

Utilizamos o método loginPage() para definir a URL da página de login personalizada. Ao configurá-la, o Spring Security deixa de fornecer automaticamente a página padrão, o que significa que precisamos criar tanto a rota no controller quanto o formulário correspondente no template engine. Para alterar o endpoint responsável por processar a autenticação, usamos o método loginProcessingUrl(), lembrando que ele deve receber requisições via POST. O método defaultSuccessUrl() define o endereço para onde o usuário será redirecionado após uma autenticação bem-sucedida, enquanto o failureUrl() especifica a página que será exibida em caso de erro — geralmente a mesma página de login, acrescida de um parâmetro (ex.: ?error=true) que indica a falha. Por fim, chamamos permitAll() para garantir que qualquer usuário, mesmo não autenticado, possa acessar o formulário de login e realizar o processo de autenticação.

O formulário de login personalizado deve ter o atributo action apontando para a URL definida em loginProcessingUrl(), utilizar o método POST e os campos precisam usar os nomes padrão username e password . Assim, o Spring Security conseguirá processar corretamente as credenciais enviadas.

<div id="login">

        <h1>Login - Sistema Produtos</h1>

        <div th:if="${param.error}" class="error-message">
            Username ou senha errados.
        </div>
        <div th:if="${param.logout}" class="success-message">
            Você saiu do sistema.
        </div>

        <form method="post" th:action="@{/usuarios/login-process}">
            <div class="field">
                <label for="username">Username:</label>
                <input type="text" name="username" id="username">
            </div>
            <div class="field">
                <label for="password">Senha:</label>
                <input type="password" name="password" id="password">
            </div>

            <div class="field">
                <button type="submit">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75" />
                    </svg>
                    Entrar
                </button>
            </div>
        </form>

Da mesma forma que é possível personalizar as URLs de login, também podemos configurar o processo de logout utilizando o método logout(). Com ele, podemos definir a URL responsável por encerrar a sessão, a página para onde o usuário será redirecionado após sair do sistema.

.logout( (logout) -> {
            logout
                .logoutUrl("/usuarios/logout")
                .logoutSuccessUrl("/usuarios/login?logout=true")
                .permitAll();
})

É importante destacar que o Spring Security, por padrão, exige que a requisição para a URL de logout seja feita utilizando o método POST.

Recuperando o usuário autenticado no controller

Uma forma simples de obter o usuário autenticado em um controller é injetar o objeto Principal, disponibilizado automaticamente pelo Spring Security. Esse objeto fornece o username do usuário logado, que pode servir como chave para buscar os demais dados completos do usuário na base de dados ou em outro serviço da aplicação.

@GetMapping("/usuarios/visualizar-usuario")
public String visualizar(Model model, Principal principal){
        
    String name = principal.getName();

    Usuario usuario = this.usuarioService.buscarUsuarioPorUsername(name);
        
    model.addAttribute("usuario", usuario);

    return "usuarios/visualizar";
}

Integrando Thymeleaf com Spring Security

O Thymeleaf conta com um módulo de integração específico para o Spring Security, chamado thymeleaf-extras-springsecurity6. Esse módulo disponibiliza diretivas que permitem, por exemplo, acessar o nome do usuário autenticado diretamente no template ou controlar a exibição de elementos HTML de forma condicional, com base no estado de autenticação ou nos papéis (roles) atribuídos ao usuário.

Para utilizar esses recursos, o primeiro passo é adicionar a dependência no arquivo pom.xml:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

Na sequência, é necessário adicionar o namespace do módulo de integração ao template Thymeleaf, permitindo o uso das diretivas do Spring Security diretamente nas páginas HTML.

xmlns:sec="http://www.thymeleaf.org/extras/spring-security"

Para mostrar o nome de usuário autenticado no template, utilizamos a diretiva: sec:authentication="name". Essa diretiva acessa diretamente o atributo username do usuário logado e o renderiza no HTML.

<div>Olá <span sec:authentication="name"></span></div>

Também é possível controlar a exibição de elementos na página com base nas permissões do usuário. Para isso, o Thymeleaf Spring Security fornece a diretiva sec:authorize, que permite condicionar a renderização de trechos de HTML utilizando uma expressão ou mais expressões combinadas com operadores lógicos. Por exemplo, podemos exibir determinado conteúdo apenas se o usuário possuir um role específico:

<a sec:authorize="hasRole('EDITOR')" th:href="@{/produtos/adicionar}" class="bt">Adicionar</a>

<!-- ou hasAnyRole para várias roles -->

<a sec:authorize="hasAnyRole('EDITOR', 'ADMIN')" th:href="@{/produtos/adicionar}" class="bt">Adicionar</a>

<!-- ou isAuthenticated() para usuários autenticados-->

<a sec:authorize="isAuthenticated()" th:href="@{/produtos/adicionar}" class="bt">Adicionar</a>

No exemplo acima, o primeiro link só será exibido se o usuário autenticado possuir a role EDITOR. O segundo link, configurado com hasAnyRole(), será mostrado caso o usuário tenha pelo menos uma das roles informadas — neste caso, EDITOR ou ADMIN. Já o terceiro link será exibido para qualquer usuário autenticado, independentemente de roles específicas, pois utiliza a expressão isAuthenticated(). Vale lembrar que o Spring Security exige o prefixo ROLE_ antes do nome de cada role. Além disso, também é possível criar expressões mais complexas combinando operadores lógicos, como em hasRole('EDITOR') && !hasRole('ADMIN'), que exige que o usuário tenha a role EDITOR, mas não a ADMIN.

Outra maneira de exibir condicionalmente um elemento é utilizando a diretiva sec:authorize-url. Nela, você informa uma URL, e o Thymeleaf verifica se o usuário autenticado possui autorização para acessá-la. Caso o usuário não tenha permissão, o elemento não será renderizado no template.

<a sec:authorize-url="/produtos/adicionar" th:href="@{/produtos/adicionar}" class="bt">Adicionar</a>

Agora veja como ficou o projeto após adicionarmos a autenticação e autorização até este ponto do artigo.

Utilizando Spring Security em APis

O Spring Security também oferece suporte para autenticação e autorização em APIs. A forma mais simples é a Autenticação Basic, na qual o username e a senha são codificados em Base64 e enviados no cabeçalho da requisição HTTP.

Para testar, podemos reutilizar a mesma estrutura do projeto anterior, incluindo o UserDetailsService, a classe Usuario e os demais componentes. A única adição será a criação de um controller específico para a API.

@RestController
@RequestMapping(("/api/produtos"))
public class ProdutosApiController {

    private final ProdutoService produtoService;

    public ProdutosApiController(ProdutoService produtoService) {
        this.produtoService = produtoService;
    }

    @GetMapping
    public ResponseEntity<List<Produto>> listar(){

        List<Produto> produtos = this.produtoService.listarTodos();

        return ResponseEntity.ok(produtos);

    }

    @GetMapping("/{id}")
    public ResponseEntity<Produto> buscarPorId(@PathVariable("id") Integer id){

        Produto produto = this.produtoService.buscarPorId(id);

        return ResponseEntity.ok(produto);

    }

    @PostMapping
    public ResponseEntity<Produto> salvar(@RequestBody ProdutoDTO produto){

        Produto produtoSalvo = this.produtoService.adicionar(produto);

        return ResponseEntity
            .created(URI.create("/api/produtos/"+produtoSalvo.getId()))
            .body(produtoSalvo);

    }

    @PutMapping("/{id}")
    public ResponseEntity<Produto> atualizar(
        @PathVariable("id") Integer id,
        @RequestBody ProdutoDTO produto
    ){

        Produto produtoSalvo = this.produtoService.atualizar(id, produto);

        return ResponseEntity.ok(produtoSalvo);

    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> atualizar(@PathVariable("id") Integer id){

        this.produtoService.deletar(id);

        return ResponseEntity.noContent().build();

    }
}

Agora precisamos fazer algumas alterações na classe WebSecurityConfig para adaptar a segurança aos endpoints da API.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean 
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http.authorizeHttpRequests( (authorize) ->{
            authorize
                .requestMatchers(HttpMethod.GET, "/api/produtos").hasAnyRole("ADMIN", "EDITOR","VISITANTE")
                .requestMatchers(HttpMethod.POST, "/api/produtos").hasAnyRole("ADMIN", "EDITOR")
                .requestMatchers(HttpMethod.DELETE,"/api/produtos/*").hasRole("ADMIN")
                .requestMatchers("/api/produtos/**").hasAnyRole("ADMIN", "EDITOR")
                .anyRequest().authenticated();
        })
        .csrf( (csrf) -> csrf.disable())
        .httpBasic( Customizer.withDefaults() );

        return http.build();
    }

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

}

Aqui utilizamos o método httpBasic com as configurações padrão para habilitar a autenticação Basic e desativamos o CSRF com csrf.disable(), evitando a necessidade de enviar o token de proteção em cada requisição (nos exemplos anteriores, o Thymeleaf cuidava disso automaticamente). Note também que alteramos nossos requestMatchers para a API, lembrando que não temos mais endpoints de formulários.

Agora podemos testar o projeto utilizando o curl. Para facilitar, você pode baixar o projeto de exemplo até este ponto e executaro projeto para testar.

// Listar Produtos
curl --location --request GET 'localhost:8080/api/produtos' \
--header 'Authorization: Basic dmlzaXRhbnRlOnZpc2l0YW50ZQ==' \
--header 'Content-Type: application/json'

// Adicionar Produto
curl --location --request POST 'localhost:8080/api/produtos' \
--header 'Authorization: Basic ZWRpdG9yOmVkaXRvcg==' \
--header 'Content-Type: application/json' \
--data '{
        "nome": "Produto teste ",
        "descricao": "Um produto de teste",
        "preco": 111
    }'

// Alterar Produto
curl --location --request PUT 'localhost:8080/api/produtos/1' \
--header 'Authorization: Basic ZWRpdG9yOmVkaXRvcg==' \
--header 'Content-Type: application/json' \
--data '{
        "nome": "Outro nome",
        "descricao": "Outra descrição",
        "preco": 42
    }'
	
	
// Deletar Produto
curl --location --request DELETE 'localhost:8080/api/produtos/1' \
--header 'Authorization: Basic YWRtaW46YWRtaW4' \
--header 'Content-Type: application/json' 

Lembre-se que o cabeçalho Authorization: Basic dmlzaXRhbnRlOnZpc2l0YW50ZQ== envia o username e a senha codificados em Base64. A parte que vem após Basic corresponde ao formato username:password codificado. Por exemplo:

visitante:visitante -> base64 ->dmlzaXRhbnRlOnZpc2l0YW50ZQ==
editor:editor-> base64 ->ZWRpdG9yOmVkaXRvcg==
admin:admin -> base64 ->YWRtaW46YWRtaW4=

É importante reforçar que a autenticação Basic só deve ser utilizada sobre HTTPS. Como o username e a senha são enviados codificados em Base64, mas não criptografados, qualquer tráfego HTTP não seguro poderia ser interceptado e expor as credenciais dos usuários.

Spring Security com JWT

No modelo de autenticação Basic, é necessário transmitir as credenciais em todas as requisições, o que representa um risco, já que dados sensíveis como usuário e senha precisam trafegar repetidamente. Uma alternativa mais adequada é a utilização de um endpoint de autenticação dedicado, responsável por validar as credenciais apenas uma vez e, em seguida, emitir um token. Esse token, então, passa a ser utilizado nas requisições subsequentes, reduzindo a exposição direta das credenciais do usuário.

Um padrão amplamente utilizado para esse tipo de token é o JWT (JSON Web Token). Em resumo, trata-se de um objeto JSON que é codificado em Base64 e assinado digitalmente com uma chave secreta. Dentro dele, podem ser armazenadas informações sobre o usuário autenticado, como seu identificador e as respectivas roles (permissões ou perfis de acesso). Para mais detalhes veja esta nesta postagem sobre o JWT com PHP.

Vamos então ao exemplo de uso do JWT. Para isso, adicionamos a dependência responsável por gerar e validar tokens.

<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Para gerarmos e validarmos tokens JWT, precisamos registrar no WebSecurityConfig dois beans essenciais: o JwtEncoder, responsável pela criação dos tokens, e o JwtDecoder, responsável por validá-los. Nesse exemplo, utilizaremos uma chave simétrica para assinar e verificar os tokens.

private String secretString = "uma-chave-secreta-para-assinar-o-jwt-token";

@Bean
public JwtDecoder jwtDecoder() {
    SecretKey originalKey = new SecretKeySpec(secretString.getBytes(), "HmacSHA256");
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(originalKey).build();
    return jwtDecoder;
}

@Bean
public JwtEncoder jwtEncoder() throws JOSEException {
    SecretKey secretKey = new SecretKeySpec(
        secretString.getBytes(StandardCharsets.UTF_8), 
        "HmacSHA256"
    );
        
    JWK jwk = new OctetSequenceKey.Builder(secretKey)
        .keyID("jwt-key-id") 
        .algorithm(JWSAlgorithm.HS256)
        .build();
        
    JWKSet jwkSet = new JWKSet(jwk);
    JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
        
    return new NimbusJwtEncoder(jwkSource);
}

Iniciamos definindo uma chave secreta como atributo da classe. Para seguir boas práticas de segurança, é recomendável carregar essa chave a partir do application.properties usando a anotação @Value, evitando expor segredos diretamente no código. E é recomendável uma chave maior.

Em seguida, definimos o bean JwtDecoder. Primeiro, criamos um objeto SecretKeySpec usando a chave secreta convertida em bytes e especificamos o algoritmo HmacSHA256. Depois, utilizamos o NimbusJwtDecoder.withSecretKey(secretKey).build() para instanciar o JwtDecoder, que será responsável por decodificar e validar os tokens JWT recebidos.

Em seguida, criamos o bean JwtEncoder, que exige uma configuração um pouco mais detalhada. Primeiro, definimos um SecretKey a partir da nossa chave secreta, da mesma forma que no JwtDecoder. Depois, construímos um JWK (JSON Web Key) por meio do OctetSequenceKey.Builder, passando o SecretKey e encadeando os métodos keyID() — onde informamos um identificador para a chave (pode ser qualquer valor, mas deve ser consistente, pois será reutilizado na criação do token) — e algorithm(), que especifica o algoritmo de assinatura. Finalizamos com build() para gerar o objeto JWK.

Com o JWK em mãos, criamos um JWKSet (conjunto de chaves) e, a partir dele, instanciamos um JWKSource utilizando ImmutableJWKSet. Por fim, passamos esse JWKSource para o NimbusJwtEncoder, que será o responsável por assinar os tokens emitidos pela aplicação.

Com JwtEncoder e JwtDecoder configurados, o próximo passo é criar um TokenService, responsável por gerar tokens JWT a partir das informações de autenticação.

@Service
public class JWTService {

    private final JwtEncoder encoder;

    public JWTService(JwtEncoder encoder) {
        this.encoder = encoder;
    }

    public String gerarToken(Authentication authentication){
        Instant agora = Instant.now();
        long expiracao = 3600L;

        String scopes = authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .map( (authority -> authority.replace("ROLE_", "")))
            .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("jwt-boteco")
            .issuedAt(agora)
            .expiresAt(agora.plusSeconds(expiracao))
            .subject(authentication.getName())
            .claim("scope", scopes)
            .build();

        JwsHeader header = JwsHeader.with(MacAlgorithm.HS256)
            .keyId("jwt-key-id") // Deve corresponder ao ID definido no JWK no WebSecurityConfig
            .build();
        
        return encoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
    }
}

Injetamos a dependência do JwtEncoder via construtor. Em seguida, implementamos o método gerarToken, que recebe um objeto Authentication. Esse objeto é a principal abstração do Spring Security para representar o usuário autenticado, contendo credenciais, nome de usuário e as authorities . Vale destacar que a interface Authentication estende Principal. Esse objeto será obtido posteriormente, no momento da autenticação manual.

Definimos uma variável para registrar a data de criação do token e, em seguida, configuramos o tempo de expiração em segundos.

Em seguida, mapeamos as authorities do usuário (no nosso exemplo, apenas uma) para uma única String. Como definimos roles para os usuários, o método getAuthorities() retorna cada authority com o prefixo ROLE_. Para incluí-las na claim do token JWT, precisamos remover esse prefixo, pois, ao decodificar o token, o Spring Security adiciona automaticamente o prefixo SCOPE_ para verificar permissões. Isso exige ajustes correspondentes nas regras de autorização no WebSecurityConfig.

Com as informações em mãos, construímos os claims do token utilizando JwtClaimsSet.builder(). Nesse processo, encadeamos métodos para definir issuer() (emissor), issuedAt() (data de criação), expiresAt() (data de expiração), subject() (username obtido do objeto Authentication) e adicionamos, via claim(), o scope com as authorities do usuário.

Em seguida, construímos o header do token com a classe JwsHeader, definindo o algoritmo de assinatura e o identificador da chave configurada no bean JwtEncoder via JWK. Com o header e os claims prontos, utilizamos o método encode() do JwtEncoder, passando ambos como JwtEncoderParameters. Por fim, chamamos getTokenValue() para obter o token em formato de string.

O próximo passo é ajustar a configuração de segurança, realizando modificações no bean SecurityFilterChain dentro da classe WebSecurityConfig.

@Bean 
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

    http.authorizeHttpRequests( (authorize) ->{
        authorize
            .requestMatchers(HttpMethod.GET, "/api/produtos").hasAnyAuthority("SCOPE_ADMIN", "SCOPE_EDITOR","SCOPE_VISITANTE")
            .requestMatchers(HttpMethod.POST, "/api/produtos").hasAnyAuthority("SCOPE_ADMIN", "SCOPE_EDITOR")
            .requestMatchers(HttpMethod.DELETE,"/api/produtos/*").hasAuthority("SCOPE_ADMIN")
            .requestMatchers("/api/produtos/**").hasAnyAuthority("SCOPE_ADMIN", "SCOPE_EDITOR")
            .anyRequest().permitAll();
        })
    .csrf( (csrf) -> csrf.disable())
    .oauth2ResourceServer(configurer -> configurer.jwt(Customizer.withDefaults()));
    return http.build();
}

Conforme mencionado anteriormente, foi necessário ajustar a configuração das permissões. Antes utilizávamos roles (ROLE_), mas ao trabalhar com JWT o Spring Security decodifica a claim scope do token e converte automaticamente os valores em authorities com o prefixo SCOPE_. Por esse motivo, as regras de acesso foram adaptadas para usar hasAuthority e hasAnyAuthority, sempre considerando o prefixo SCOPE_ no lugar de ROLE_.

Além disso, é necessário chamar o método oauth2ResourceServer na configuração de segurança para registrar a aplicação como Resource Server. Essa configuração permite que o Spring Security valide e decodifique o token JWT, utilizando o JWTDecoder, recebido em cada requisição, utilizando-o para determinar a autorização do usuário.

No WebSecurityConfig, também adicionamos a definição de um bean que será utilizado posteriormente no processo de autenticação do usuário dentro do controller.

// WebSecurityConfig

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

Em seguida, vamos implementar no controller um endpoint de autenticação responsável por validar as credenciais do usuário e, em caso de sucesso, gerar o token JWT.

@RestController
public class UsuarioController {

    private final JWTService jwtService;
    private final AuthenticationManager authenticationManager;

    public UsuarioController(JWTService jwtService, AuthenticationManager authenticationManager) {
        this.jwtService = jwtService;
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/api/usuarios/autenticar")
    public ResponseEntity<Map<String,String>> autenticar(
        @RequestBody Map<String,String> data
    ){
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                data.get("username"), 
                data.get("password")
            )
        );
        
        Map<String,String> tokenMap = new HashMap<String,String>();
        tokenMap.put("token", jwtService.gerarToken( authentication ));

        return ResponseEntity.ok(tokenMap);
    } 

}

No controller, injetamos duas dependências principais: o JWTService, responsável pela geração dos tokens JWT, e o AuthenticationManager, previamente configurado no WebSecurityConfig, que é utilizado para autenticar as credenciais recebidas.

Em seguida, mapeamos a rota POST /api/usuarios/autenticar, que recebe as credenciais do usuário no corpo da requisição (RequestBody). Nesse exemplo, utilizamos um Map<String, String> para capturar os dados, mas uma prática mais adequada seria definir um DTO dedicado, contendo os campos username e password, garantindo maior clareza e validação estruturada.

Chamamos o método authenticate do AuthenticationManager, fornecendo um objeto UsernamePasswordAuthenticationToken construído com o username e password extraídos do corpo da requisição. Caso as credenciais sejam válidas, o método retorna um objeto Authentication representando o usuário autenticado. Se as credenciais forem inválidas, o Spring lança uma AuthenticationException.

Após obter o objeto Authentication com as credenciais validadas, passamos esse objeto para o método gerarToken do JWTService. Esse método retorna um token JWT, que então é inserido em um Map e enviado ao cliente encapsulado em um ResponseEntity.

Com toda a configuração concluída, já podemos testar a requisição de autenticação utilizando o cURL ou outra ferramenta como Postman. Para facilitar, você pode baixar o projeto completo de exemplo aqui e executar os testes localmente.

curl --location --request POST 'http://localhost:8080/api/usuarios/autenticar' \
--header 'Content-Type: application/json' \
--data '{
        "username":"admin",
        "password": "admin"
    }'

Como resultado da requisição, receberemos uma resposta no formato JSON contendo o token JWT gerado.

{
    "token": "eyJraWQiOiJqd3Qta2V5LWlkIiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiJqd3QtYm90ZWNvIiwic3ViIjoiYWRtaW4iLCJleHAiOjE3NTU0MzgwNTcsImlhdCI6MTc1NTQzNDQ1Nywic2NvcGUiOiJBRE1JTiJ9.RdQCGiWS4G0LVY3oT22T7spEvPH4Y5oaJ-hlJ4ix2DY"
}

Você pode utilizar a ferramenta JWT.io Debugger para decodificar o token e inspecionar seu conteúdo em tempo real, visualizando o header e payload e a verificar a assinatura fornecendo a chave.

Com o token em mãos, podemos acessar um endpoint protegido enviando-o no cabeçalho Authorization da requisição. Para isso, utilizamos o esquema Bearer, conforme demonstrado no exemplo com curl.

curl --location --request POST 'http://localhost:8080/api/produtos' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJraWQiOiJqd3Qta2V5LWlkIiwiYWxnIjoiSFMyNTYifQ.eyJpc3MiOiJqd3QtYm90ZWNvIiwic3ViIjoiYWRtaW4iLCJleHAiOjE3NTU0MzgwNTcsImlhdCI6MTc1NTQzNDQ1Nywic2NvcGUiOiJBRE1JTiJ9.RdQCGiWS4G0LVY3oT22T7spEvPH4Y5oaJ-hlJ4ix2DY' \
--data '{
        "nome": "Produto",
        "descricao": "Descrição do Produto",
        "preco": 200
    }'

Assinatura de Token JWT com Chaves Pública e Privada

Em vez de utilizarmos uma chave simétrica para assinar o token, podemos optar por um par de chaves pública e privada. Nesse caso, é necessário gerar essas chaves e configurar o JwtEncoder para utilizar a chave privada na assinatura e o JwtDecoder para validar o token com a chave pública.

Podemos gerar a chave privada utilizando o OpenSSL, uma ferramenta amplamente usada para criação e manipulação de chaves e certificados.

openssl genrsa > private.pem

O comando gera uma chave privada RSA e a armazena no arquivo private.pem. A partir dessa chave privada, podemos derivar a chave pública correspondente com o seguinte comando.

openssl rsa -in private.pem -pubout -out public.pem

Com esse comando utilizamos o arquivo private.pem para extrair a chave pública correspondente, que é salva no arquivo public.pem.

Outra opção é gerar o par de chaves pelo site CryptoTools RSA Generator, mas não é recomendável utilizar chaves geradas em sites em produção.

Depois de gerar as chaves, devemos mover os arquivos private.pem e public.pem para a pasta resources do projeto. Em seguida, ajustamos a configuração no WebSecurityConfig, modificando a criação dos beans JwtEncoder e JwtDecoder para utilizar as chaves assimétricas.

@Value("${jwt.public.key}")
private RSAPublicKey publicKey;

@Value("${jwt.private.key}")
private RSAPrivateKey privateKey;

@Bean 
public JwtEncoder jwtEncoder(){
    JWK jwk = new RSAKey
        .Builder(this.publicKey)
        .privateKey(this.privateKey)
        .build();
    ImmutableJWKSet jwks = new ImmutableJWKSet<>( new JWKSet(jwk));
     
    return new NimbusJwtEncoder(jwks);
}

@Bean 
public JwtDecoder jwtDecoder(){
    return NimbusJwtDecoder.withPublicKey(publicKey).build();
}

Primeiro, definimos os atributos RSAPublicKey e RSAPrivateKey, que serão utilizados respectivamente para validar e assinar os tokens. Esses valores são carregados a partir do arquivo application.properties por meio da anotação @Value. É importante garantir que as chaves estejam corretamente configuradas nesse arquivo de propriedades, apontando para os arquivos .pem correspondentes.

jwt.public.key=classpath:public.pem
jwt.private.key=classpath:private.pem

Para criar o JwtEncoder, instanciamos um JWK a partir da chave pública utilizando o Builder e adicionamos a chave privada com o método privateKey(). Em seguida, encapsulamos o JWK em um ImmutableJWKSet, que é utilizado como base para inicializar o NimbusJwtEncoder.

Para construir o JwtDecoder, utilizamos o método withPublicKey() da classe NimbusJwtDecoder, fornecendo a chave pública como parâmetro, e finalizamos a configuração chamando o método build() para obter a instância do objeto.

No serviço responsável pela geração do token (JWTService), é necessário ajustar a implementação, removendo a criação manual do header, já que ao utilizar chaves pública e privada o JwtEncoder passa a gerenciá-lo automaticamente.

public String gerarToken(Authentication authentication){
        Instant agora = Instant.now();
        long expiracao = 3600L;
        
        String scopes = authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .map( (authority -> authority.replace("ROLE_", "")))
            .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("jwt-boteco")
            .issuedAt(agora)
            .expiresAt(agora.plusSeconds(expiracao))
            .subject(authentication.getName())
            .claim("scope", scopes)
            .build();
        
        return encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

É importante lembrar que o token JWT é assinado, mas não criptografado. Isso significa que qualquer pessoa que o intercepte pode decodificar suas claims e visualizar as informações nele contidas. Por esse motivo, nunca se deve armazenar dados sensíveis em um JWT, já que a assinatura garante apenas a integridade e autenticidade, mas não a confidencialidade.

Bom, por hoje é isso! Esta postagem já ficou bastante extensa, mas acredito que já deu para obter uma visão geral sobre o sistema de autenticação e autorização fornecido pelo Spring Security. Ele tem a intenção de ser um ponto de partida para um estudo mais aprofundado, principalmente sobre boas práticas de segurança. T++!