Search buenodc

Nesta página
  1. Configurando o Spring Security
  2. Configurando o acesso às páginas
  3. Personalizando a página de login
  4. Personalizando a autenticação com usuário próprio
  5. Habilitando o registro de usuários
  6. Links Interessantes

Configurando o Spring Security

O Spring Security é um framework que permite a configuração de autenticação e autorização em aplicações Java. É importante entender esses conceitos para que você possa entender o que o Spring Security faz. A autenticação é o processo de identificação do usuário, ou seja, é o processo que verifica se o usuário é quem ele diz ser. O método mais comum de autenticação é através de um formulário de login, onde o usuário informa seu nome de usuário e senha, que são verificados pelo sistema.

A autorização é o processo que verifica se o usuário possui permissão para acessar determinado recurso. Por exemplo, um usuário X pode ter permissão para acessar a página de detalhes de um produto, mas não pode acessar a página que permita deletar esse produto. De forma resumida a autenticação vai verificar “Quem é você” e a autorização vai verificar “O que você pode fazer”.

Além de autenticação e autorização, o Spring Security também oferece outros recursos relacionados a segurança de aplicações Web como por exemplo proteção contra ataques como CSRF e XSS, que serão abordados em outro momento.

Configurando o Spring Security

Para configurar o Spring Security em uma aplicação Spring Boot, basta adicionar a dependência spring-boot-starter-security ao arquivo pom.xml:

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

Apenas ao adicionar essa dependência, o Spring Security fará o bloqueio de acesso a todas as páginas da aplicação. Isso é um comportamento padrão do Spring Security. Faça o teste, inicie a aplicação e tente acessar qualquer página. Você verá que será exibida uma tela de login.

Essa tela de login é gerada automaticamente pelo Spring Security e serve apenas para testar a autenticação e não deve ser usada em sua aplicação. Além da tela de login, o Spring Security também cria um usuário padrão, que é user e uma senha que é gerada aleatoriamente e exibida no console ao iniciar a aplicação. Com essas credenciais você pode fazer o login e acessar as páginas da aplicação.

Configurando o acesso às páginas

Claro que esse comportamento padrão apesar de ser ótimo não é suficiente pois quem vai definir quais páginas devem ser acessadas e por quem é você e para isso é necessário fazer algumas configurações. Para isso, crie uma nova classe chamada WebSecurityConfig dentro de um novo pacote chamado security.

Tome cuidado, existem diferentes versões do Spring Security e cada uma delas possui uma forma diferente de configurar. Nesse tutorial estamos usando a versão mais nova atualmente que é a versão 6+.

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

@Configuration
public class WebSecurityConfig {

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

        return http.authorizeHttpRequests((authorize) -> {
            authorize.requestMatchers("/products/**").permitAll();
            authorize.anyRequest().authenticated();
        })
        .build();

    }
}

Para que o Spring Security possa fazer a configuração, é necessário criar um método anotado com @Bean que retorne um SecurityFilterChain e que receba como parâmetro um HttpSecurity. Nesse parâmetro, utiliza-se do método authorizeHttpRequests que por sua vez utiliza de uma função lambda que recebe um AuthorizationManager. Esse objeto é responsável por definir quais páginas podem ser acessadas e por quem. Ele possui diversos métodos, tais como o requestMatchers que recebe uma expressão que define quais páginas podem ser acessadas, e o anyRequest juntamente com o authenticated que define que qualquer página deve ser acessada apenas por usuários autenticados.

Rode a aplicação novamente e tente acessar as páginas. Você verá que é possível acessar as rotas /products conforme configurado anteriormente, porém qualquer página fora dessa rota não pode ser acessada inclusive os arquivos estáticos como CSS que deixaram de funcionar. Outra observação importante é que o Spring Security não permite mais o acesso a página de login, pois ela não está configurada para ser acessada por usuários autenticados.

Caso queira testar novamente com a página de login, basta adicionar a linha .formLogin(Customizer.withDefaults()) antes de finalizar a construção do objeto - antes do build(). Essa linha é responsável por habilitar a página de login padrão do Spring Security.

Você deve ter percebido que ao acessar a rota /products apesar de conseguirmos visualizar o conteúdo do site, o estilo que tínhamos aplicado anteriormente não está mais sendo aplicado. Isso acontece porque a rota dos arquivos estáticos é diferente da rota /products e por isso o Spring Security não permite o acesso. Para resolver esse problema, é necessário liberar o acesso aos arquivo estáticos. Para isso, basta utilizar novamente a método requestMatchers e passar a expressão "/css/**" permitindo o acesso a todos os arquivos dentro da pasta css.

authorize.requestMatchers("/products/**", "/css/**").permitAll();

Personalizando a página de login

Como já foi dito anteriormente, a página de login padrão do Spring Security não deve ser utilizada em sua aplicação. Logo será necessário criar uma nova página para que você possa ter controle sobre o login. Para isso, crie uma nova página chamada login.html dentro da pasta src/main/resources/templates/auth e adicione o seguinte conteúdo:

<h1>Login</h1>
<div th:if="${param.error}">Usuário ou senha inválidos.</div>
<div th:if="${param.logout}">Logout efetuado com sucesso</div>
<form th:action="@{/login}" th:method="post">
  <input type="text" name="username" placeholder="username" />
  <input type="password" name="password" placeholder="password" />
  <button type="submit">Login</button>
</form>

Aqui temos algumas configurações esperadas pelo Spring Security.

  • O formulário deve possui o método POST para rota /login
  • O formulário precisa incluir um Token CSRF, que é incluído automaticamente pelo Thymeleaf.
  • O formulário deve ter um campo com o name com o valor username
  • O formulário deve ter um campo com o name com o valor password
  • Se o parâmetro HTTP chamado error for encontrado, ele indica que o usuário não forneceu um nome de usuário ou senha válidos
  • Se o parâmetro HTTP chamado logout for encontrado, ele indica que o usuário efetuou logout com êxito.

Além disso, é necessário avisar o Spring Security que agora você possui uma página de login personalizada. Para isso, basta alterar a linha que habilita a página de login padrão para:

.formLogin(form -> form.loginPage("/login").permitAll())

Por fim, como a responsabilidade da página de login agora é nossa, precisamos indicar qual controlador vai escutar a rota e qual visão será chamada para exibir a página. Para isso, crie uma nova classe chamada LoginController dentro do pacote controller e adicione o seguinte conteúdo:

@Controller

public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "auth/login";
    }

    @GetMapping("/logout")
    public String logout() {
        return "auth/logout";
    }
}

Do mesmo jeito que lidamos com a rota de /login precisamos lidar com a rota de /logout. Para isso, precisamos criar uma nova página chamada logout.html dentro da pasta src/main/resources/auth com um formulário que envia uma requisição POST para a rota /logout. Além disso, é necessário configurar a rota de logout no controlador LoginController.

<h1>Logout</h1>
<form th:action="@{/logout}" th:method="post">
  <button type="submit">Logout</button>
</form>

Se testar a aplicação nesse momento você verá que o login e logout estão funcionando porém a mensagem de logout não está sendo exibida. Isso acontece porque o Spring Security não permite o acesso a página de logout. Para resolver esse problema, é necessário liberar o acesso a página de login adicionado uma nova linha de configuração no método configure do WebSecurityConfig:

authorize.requestMatchers("/login").permitAll();

Personalizando a autenticação com usuário próprio

Até agora, o Spring Security está utilizando um usuário padrão que é user e uma senha que é gerada aleatoriamente e exibida no console ao iniciar a aplicação. Para personalizar esse comportamento, é necessário criar um novo usuário com nome de usuário e senha próprios (gerenciado pela aplicação). Para isso, são necessários três passos:

  1. Uma classe que represente o usuário e implemente a interface UserDetails
  2. Um serviço que saiba como buscar esse usuário e que implemente a interface UserDetailsService
  3. Um PasswordEncoder que saiba como codificar e verificar as senhas de forma segura.

Implementando a interface UserDetails na classe User

Já temos uma classe chamada User que representa o usuário da aplicação. Para que essa classe possa ser utilizada pelo Spring Security, é necessário implementar a interface UserDetails. Para isso, basta alterar a declaração da classe para:

import org.springframework.security.core.userdetails.UserDetails;

public class User implements UserDetails {

Ao implementar essa interface, o Spring Security exige que alguns métodos sejam implementados. Esses métodos são:

  • getAuthorities(): Retorna as autoridades concedidas ao usuário.
  • getUsername(): Retorna o nome de usuário do usuário.
  • getPassword(): Retorna a senha do usuário.
  • isAccountNonExpired(): Retorna se a conta do usuário está expirada.
  • isAccountNonLocked(): Retorna se a conta do usuário está bloqueada.
  • isCredentialsNonExpired(): Retorna se as credenciais do usuário estão expiradas.
  • isEnabled(): Retorna se o usuário está habilitado.

O método getAuthorities() deve retornar uma coleção de objetos do tipo GrantedAuthority, que representam as permissões do usuário. Isso significa, o que o usuário pode fazer no sistema. Por enquanto vamos retornar uma lista vazia, mas em um futuro próximo vamos implementar o controle de acesso baseado em papéis (roles).

Os métodos getUsername(), getPassword() são intuitivos e devem retornar o nome de usuário e a senha do usuário, respectivamente.

Já os métodos isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired() e isEnabled() são métodos de status da conta e permitem um controle fino sobre a validade da conta. Eles respondem à pergunta geral: “Esta conta está apta para realizar o login neste momento?”. Sendo seus usos:

  • isAccountNonExpired(): Útil para contas temporárias ou de teste que devem ser válidas apenas por um determinado período. Se retornar false, o login falha.
  • isAccountNonLocked(): Ideal para implementar lógicas de segurança, como bloquear uma conta após várias tentativas de login falhas ou para um administrador bloquear manualmente um usuário suspeito. Se retornar false, o login falha.
  • isCredentialsNonExpired(): Usado em ambientes corporativos que exigem que os usuários troquem suas senhas periodicamente (a cada 90 dias, por exemplo). Se retornar false, o usuário pode ser redirecionado para uma página de troca de senha.
  • isEnabled(): Este é o mais comum de ser implementado. É perfeito para cenários de “soft delete” (onde você não apaga o usuário do banco, apenas o desativa) ou para contas que precisam de verificação por e-mail antes de serem ativadas.

Aqui está um exemplo de como implementar esses métodos na classe User:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;

public class User implements UserDetails {

    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Uma dica importante, caso seu atributo de usuário ou senha possua um nome diferente de username ou password não tem problema, o importante é que os métodos getUsername() e getPassword() retornem os valores corretos.

Outra dica importante, como estamos utilizando o email, é importante que esse atributo seja único. Para isso, basta adicionar a anotação @Column(unique = true) no atributo email da classe User.

Implementando a interface UserDetailsService no UserService

Agora que temos a classe User implementando a interface UserDetails, precisamos criar um serviço que saiba como buscar esse usuário. Para isso, basta alterar a classe UserService para implementar a interface UserDetailsService e implementar o método loadUserByUsername.

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<User> user = userRepository.findByEmail(username);
        if (user.isEmpty()) {
            throw new UsernameNotFoundException("Usuário não encontrado");
        }
        return user.get();
    }
}

Aqui estamos utilizando o UserRepository para buscar o usuário pelo nome de usuário. Caso o usuário não seja encontrado, uma exceção UsernameNotFoundException é lançada. Caso contrário, o usuário é retornado, porém precisamos implementar o método findByEmail no repositório.

import java.util.Optional; // Importe o Optional
import jakarta.persistence.NoResultException; // Importe a exceção

// ... dentro da sua classe UserRepository

public Optional<User> findByEmail(String email) {
    String sql = "SELECT * FROM user WHERE email = :email"; // Use o nome da tabela correto
    Query q = em.createNativeQuery(sql, User.class);
    q.setParameter("email", email);
    try {
        User user = (User) q.getSingleResult();
        return Optional.of(user); // Retorna o usuário encontrado dentro de um Optional
    } catch (NoResultException e) {
        return Optional.empty(); // Retorna um Optional vazio se ninguém for encontrado
    }
}

Aqui estamos utilizando uma consulta nativa para buscar o usuário pelo email. Caso o usuário seja encontrado, ele é retornado dentro de um Optional. Caso contrário, um Optional vazio é retornado.

Implementando o PasswordEncoder no UserService

Por fim, precisamos implementar um PasswordEncoder que saiba como codificar e verificar as senhas de forma segura. Para isso, basta adicionar o seguinte método na classe WebSecurityConfig:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

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

Aqui estamos utilizando o BCryptPasswordEncoder, que é um dos algoritmos mais seguros atualmente. Com isso, o Spring Security já sabe como codificar e verificar as senhas. É a implementação padrão e recomendada pelo Spring Security para codificar (ou fazer “hash”) de senhas.

Sua função principal é transformar uma senha em texto puro (ex: "123456") em uma string longa e indecifrável (ex: "$2a$10$N9qo8uLOickgx2ZMRZoMyeNS..."), que é armazenada de forma segura no banco de dados. Isso é crucial porque, em caso de violação de dados, mesmo que alguém consiga acessar o banco de dados, não será capaz de ver as senhas reais dos usuários.

Com isso, o Spring Boot detectará automaticamente seu UserDetailsService e PasswordEncoder e os usará para autenticação, desativando o usuário padrão gerado no console.

Testando a autenticação

Agora que o usuário padrão não existe mais, você precisa de um usuário no seu banco de dados para poder fazer login. Você pode criar um serviço para cadastrar usuários ou, para um teste rápido, usar um `CommandLineRunner“ para inserir um usuário na inicialização da aplicação.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DataInitializer implements CommandLineRunner {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) throws Exception {
        // Verifica se o usuário admin já existe para não criar duplicado
        if (userRepository.findByUsername("admin").isEmpty()) {
            User admin = new User();
            admin.setUsername("admin");
            // Codifica a senha antes de salvar!
            admin.setPassword(passwordEncoder.encode("123456"));
            admin.setEnabled(true);

            userRepository.save(admin);
            System.out.println(">>> Usuário 'admin' criado com senha '123456'");
        }
    }
}

Habilitando o registro de usuários

Estamos com o login/logout funcionando porém não temos uma forma de registrar novos usuários, apesar de termos a página de registro. Primeiro, você precisa dizer ao Spring Security que qualquer pessoa, mesmo sem estar logada, pode acessar as páginas de registro.

Abra sua classe WebSecurityConfig.java e adicione as rotas /users/create (para mostrar o formulário) e /users/store (para salvar o usuário) à lista de permissões.

// Em WebSecurityConfig.java

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

    return http.authorizeHttpRequests((authorize) -> {
        // ... suas outras permissões
        authorize.requestMatchers("/login").permitAll();

        // ADICIONE ESTA LINHAS:
        authorize.requestMatchers("/users/create", "/users/store").permitAll();

        authorize.anyRequest().authenticated();
    })
    .formLogin(login -> login.loginPage("/login").permitAll())
    .build();
}

Para poder salvar o usuário, você precisa garantir que a senha seja codificada antes de ser salva no banco de dados. Para isso, podemos usar a classe de serviço UserService criando um novo método chamado registerNewUser que recebe um objeto User, codifica a senha e salva o usuário no banco de dados.

// Em UserService.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerNewUser(User user) {
        // Ponto CRÍTICO: Criptografa a senha antes de salvar!
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);

        // Aqui você poderia adicionar outras lógicas, como:
        // - Verificar se o e-mail já existe
        // - Definir uma role padrão para o novo usuário
        // - Enviar um e-mail de confirmação

        userRepository.save(user); // Salva o usuário com a senha já criptografada
        return user;
    }
}

Agora precisamos alterar o controlador UserController para usar esse novo método ao invés de salvar o usuário diretamente.

// Em UserController.java  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/users")
public class UserController {   

    @Autowired
    private UserDetailsServiceImpl userService;

    @GetMapping("/create")
    public String create(Model model) {
        model.addAttribute("user", new User());
        return "users/create";
    }

    @PostMapping("/store")
    public String store(@ModelAttribute User user) {
        userService.registerNewUser(user); // Usa o novo método para registrar o usuário
        return "redirect:/login?registrationSuccess"; // Redireciona para a página de login após o registro
    }
}   

Agora você pode iniciar a aplicação, acessar a página de registro em /users/create, criar um novo usuário e depois fazer login com esse usuário.