Car Store

LAB 8 - Implementando a funcionalidade de Login com Spring Security

· Enoque Leal

Objetivo

Neste laboratório iremos implementar a funcionalidade de autenticação pora os formulários da camada web da aplicação carstore-spring-boot. Para isso, iremos utilizar o Spring Security Starter. O foco é permitir que usuários façam login via uma página HTML (Thymeleaf) e que a sessão seja mantida pelo container de sessão do Spring Security.

Ao final você deverá saber:

  • Incluir dependências do Spring Security no pom.xml;
  • Configurar um SecurityConfig moderno (filter chain) com PasswordEncoder;
  • Criar um formulário de login com Thymeleaf e integrar ao fluxo do Spring Security;
  • Fornecer um usuário em memória para testes;
  • Testar a autenticação via navegador usando a UI e o console H2.

Pré-requisitos

  • Ter completado os laboratórios anteriores (Parte 1 JDBC e Parte 2 JPA);
  • Projeto carstore-spring-boot compilando localmente;
  • IDE (IntelliJ/VS Code) com Maven e JDK 11+ configurados.

Visão geral das tarefas

  1. Adicionar dependência spring-boot-starter-security no pom.xml.
  2. Criar SecurityConfig com SecurityFilterChain e PasswordEncoder (BCrypt).
  3. Criar um template login.html e roteador LoginController.
  4. Testar com um usuário em memória.

Tarefa 1: Importando a dependência do Spring Security

No pom.xml do projeto carstore-spring-boot, adicione os starters spring-boot-starter-security e thymeleaf-extras-springsecurity6:

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

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

Tarefa 2: Criando a classe de configuração do Spring Security

Crie a classe SecurityConfig no pacote src/main/java/br/com/carstore/config/.

Esta configuração irá:

  1. Permitir o acesso a recursos estáticos e rotas públicas.

  2. Exigir autenticação (.authenticated()) apenas para rotas que iniciam com /admin/**.

  3. Permitir todo o resto (.anyRequest().permitAll()).

package br.com.carstore.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.config.annotation.web.configuration.EnableWebSecurity; 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

                .authorizeHttpRequests(auth -> auth
                        // 1. Permite acesso a recursos estáticos (CSS, JS, Imagens, etc.)
                        .requestMatchers("/css/**", "/js/**", "/images/**", "/login/**").permitAll()

                        // 2. REQUER AUTENTICAÇÃO para qualquer rota que comece com /admin/**
                        .requestMatchers("/admin/**").authenticated()

                        // 3. Todas as outras rotas (incluindo /, /login, /home) são permitidas (públicas)
                        .anyRequest().permitAll()
                )
                .formLogin(form -> form
                        .loginPage("/login")               // URL para exibir o formulário de login customizado
                        .failureUrl("/login?error")        // URL em caso de falha no login
                        // Redireciona para a home após login (Se não houver URL de origem armazenada)
                        .defaultSuccessUrl("/admin", true)
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")              // URL que o formulário de logout fará POST
                        .logoutSuccessUrl("/login?logout") // Redireciona após logout bem-sucedido
                )
                // Desativa CSRF para simplificar o desenvolvimento. Mantenha em produção se usar formulários.
                .csrf(AbstractHttpConfigurer::disable)

                // Habilita a renderização de wireframe (necessário para o H2DB)
                .headers(headers -> headers
                        .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Usa BCrypt, que é o padrão seguro para hashear senhas.
        return new BCryptPasswordEncoder();
    }

}

Tarefa 3: Criando um usuário para testes (in-memory)

Para testar o fluxo de autenticação rapidamente, vamos configurar um usuário em memória. Adicione o seguinte método @Bean dentro da classe SecurityConfig criada na Tarefa 2.

// Este método deve ser adicionado DENTRO da classe SecurityConfig
@Bean
public org.springframework.security.core.userdetails.UserDetailsService users(PasswordEncoder passwordEncoder) {
  
  // Detalhes do usuário de teste
  org.springframework.security.core.userdetails.UserDetails user = 
    org.springframework.security.core.userdetails.User.builder()
      .username("admin")
      // A senha 'admin' será codificada pelo BCryptPasswordEncoder
      .password(passwordEncoder.encode("admin")) 
      .roles("USER", "ADMIN") // Roles para uso futuro em autorização
      .build();
  
  // Gerenciador em memória (apenas para testes)
  return new org.springframework.security.provisioning.InMemoryUserDetailsManager(user);

}

Credenciais de teste: admin / admin

Tarefa 4: Criando a página de login com Thymeleaf

Crie o template src/main/resources/templates/login.html. O formulário deve ter action="@{/login}", method="post", e campos name="username" e name="password".

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>CarStore</title>
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.3/css/bootstrap.min.css}">
    <style>
        body {
            background-color: #f8f9fa; /* Um cinza claro para o fundo */
        }
        .login-container {
            margin-top: 10vh; /* Margem superior para centralizar verticalmente */
            max-width: 400px; /* Largura máxima para o card de login */
        }
    </style>
</head>
<body>

<div class="container login-container">
    <div class="card shadow-lg">

        <div class="card-body">
            <h3 class="card-title text-center mb-4">Login</h3>

            <form th:action="@{/login}" method="post">
                <div class="mb-3">
                    <label for="username" class="form-label">Usuário</label>
                    <input type="text" class="form-control" id="username" name="username" placeholder="Digite seu usuário" required>
                </div>
                <div class="mb-3">
                    <label for="password" class="form-label">Senha</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="Digite sua senha" required>
                </div>

                <div th:if="${param.error}" class="alert alert-danger text-center" role="alert">
                    Usuário ou senha inválidos.
                </div>
                <div th:if="${param.logout}" class="alert alert-success text-center" role="alert">
                    Você saiu com sucesso.
                </div>

                <div class="d-grid gap-2 mt-4">
                    <button type="submit" class="btn btn-primary btn-lg">Entrar</button>
                </div>
            </form>
        </div>
        <div class="card-footer text-center text-muted">
            <small>CarStore &copy; 2025</small>
        </div>
    </div>
</div>

<script th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"></script>

</body>
</html>

Tarefa 5: Criando a Controller responsável pelo fluxo de login

Crie a classe LoginController no pacote src/main/java/br/com/carstore/controller para resolver explicitamente a rota GET /login.

package br.com.carstore.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {

        return "login"; // templates/login.html

    }

}

Tarefa 6: Validando a implementação

  1. Inicie a aplicação via CLI utilizando o comando mvn spring-boot:run ou pela IDE.

  2. Teste 1 - Acesso Público: Acesse http://localhost:8080/. O acesso deve ser liberado.

  3. Teste 2 - Acesso Restrito: Tente acessar uma rota restrita, como http://localhost:8080/admin/. Você deverá ser redirecionado para http://localhost:8080/login pois trata-se de uma rota protegida pelo spring security.

  4. Faça o login: Na página de login, use as credenciais: admin / admin.

  5. Acesso Pós-Login: Após o login, você deve ser levado à rota que tentou acessar originalmente (/admin/cars) ou, caso contrário, para a rota de sucesso padrão (/).

  6. Logout: Para testar o logout, adicione na navbar que foi criada na fragment, o formulário de POST para a rota de logout:

<form th:action="@{/logout}" method="post">
    <button type="submit">Sair do Sistema</button>
</form>

Após clicar, você deve ser redirecionado para http://localhost:8080/login?logout.

Conclusão e próximos passos

Neste laboratório você configurou o Spring Security, protegeu rotas de administração (/admin/**) e implementou o fluxo de autenticação por formulário com Thymeleaf.

LAB 9 (breve visão)

Objetivos do LAB 9:

  • Proteger endpoints REST (URL base /api/**);
  • Implementar autenticação baseada em JWT e cabeçalhos Authorization;

Parabéns — Você implementou a jornada de login na camada web! :)