O Spring Security é o módulo que fornece suporte para autenticação, autorização e proteção contra ataques comuns em aplicações Web. A autenticação é como verificamos a identidade de usuários para termos acesso às funcionalidades de um sistema. Uma maneira comum de autenticação é a verificação de nome e senha fornecidos pelo usuário.
Depois que a autenticação é realizada, podemos executar a autorização, que é a permissão de acesso a recursos conforme um determinado papel que o usuário autenticado desempenha no sistema.
Neste Codelab você aprenderá como adicionar autenticação e autorização a uma aplicação Java Web utilizando o framework Spring Boot com Thymeleaf e acesso a dados com JdbcTemplate.
A aplicação consiste de uma página web para gerenciar um condomínio. (consulte este documento para mais detalhes). Teremos três papéis que os usuários poderão desempenhar na aplicação:
Papel | Autorização |
USUARIO | permissão para visualizar os relatórios de apartamentos e proprietários. |
MORADOR | permissão para visualizar os relatórios de apartamentos e proprietários e criar novos registros. |
ADMIN | todas as permissões. |
Para representar as informações de usuários e suas permissões vamos usar o seguinte modelo de dados:
A tabela de usuários armazena suas credenciais para autenticação e a tabela de papeis armazena suas autorizações (permissões). O relacionamento de entidade entre usuários e papeis é muitos-para-muitos porque um usuário pode ter um ou mais papeis e um papel pode ser atribuído a um ou mais usuários. É por isso que precisamos ter a tabela intermediária usuarios_papeis
para representar esse relacionamento.
Script de criação do banco de dados:
CREATE TABLE IF NOT EXISTS `papeis` ( `papel_id` INT(11) NOT NULL AUTO_INCREMENT, `nome` VARCHAR(45) NOT NULL, PRIMARY KEY (`papel_id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; CREATE TABLE IF NOT EXISTS `usuarios` ( `usuario_id` INT(11) NOT NULL AUTO_INCREMENT, `email` VARCHAR(45) NOT NULL, `senha` VARCHAR(64) NOT NULL, PRIMARY KEY (`usuario_id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; CREATE TABLE IF NOT EXISTS `usuarios_papeis` ( `usuario_id` INT(11) NOT NULL, `papel_id` INT(11) NOT NULL, CONSTRAINT `papel_fk` FOREIGN KEY (`papel_id`) REFERENCES `papeis` (`papel_id`), CONSTRAINT `user_fk` FOREIGN KEY (`usuario_id`) REFERENCES `usuarios` (`usuario_id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8; -- Script de inserção de dados INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (1, 'USUARIO'); INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (2, 'MORADOR'); INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (3, 'ADMIN'); INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (1, 'mercurio@teste.com', '$2a$10$wSa39/yk/UTovsqPt817X.c0I8xlS2s76YQy4ViDxag0mlxUoYUq2'); INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (2, 'venus@teste.com', '$2a$10$v8Wr0mf6HgmIG0ANimKJOuOIt/09qIkXIF7wCwzq8.U/LTqTs9ovq'); INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (3, 'terra@teste.com', '$2a$10$5sci59bfdcED4XxxuN9gx.SJBPsdNknirJSkLbTCouf2mFzLmX/Gi'); INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (4, 'marte@teste.com', '$2a$10$Wl1gojjJgFhXztvHIULT3e0hiEMrDbCWCys0p6LnfrqxcxYkgh9OW'); INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (5, 'jupiter@teste.com', '$2a$10$5sci59bfdcED4XxxuN9gx.SJBPsdNknirJSkLbTCouf2mFzLmX/Gi'); INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (1, 1); -- usuário mercurio tem papel USUARIO INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (2, 2); -- usuário venus tem papel MORADOR INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (3, 2); -- usuário terra tem papel MORADOR INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (3, 1); -- usuário terra tem papel USUARIO INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (4, 1); -- usuário marte tem papel USUARIO INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (5, 3); -- usuário jupiter tem papel ADMIN
Observe que as senhas dos usuários estão criptografadas. A criptografia de senha é necessário para aumentar a segurança da aplicação. Vamos usar o algoritmo bcrypt para criptografar as senhas. Note que cada usuário tem um código diferente para a senha, no entanto, todas as senhas têm o mesmo valor (123456). Para gerar este códigos você pode usar este website. O Spring Security fornece uma implementação deste algoritmo para codificar as senhas cadastradas pelo usuário. Como não teremos a implementação de CRUD para usuários neste codelab, vamos inserir manualmente os dados de usuários usando o script mostrado acima.
Para adicionar segurança à aplicação vamos adicionar as seguintes dependências ao arquivo pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
Observação: Em um caso de uso de cadastro de usuário, deve-se adicionar uma etapa de criptografia na senha para adicionar segurança, veja o exemplo abaixo:
@PostMapping(value = "novo-usuario") public String cadastraNovoUsuario(Usuario usuario) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encodedPassword = passwordEncoder.encode(usuario.getSenha()); usuario.setSenha(encodedPassword); usuarioRepository.gravaUsuario(usuario); return "redirect:/usuarios"; }
Vamos usar a autenticação de nome de usuário / senha, que utiliza um formulário HTML para obter do usuário um nome e uma senha para autenticação.
Fonte: spring.io
Quando a página de login é especificada na configuração do Spring Security, você é responsável por renderizar a página.
Existem alguns pontos-chave sobre o formulário HTML padrão:
/login
usando o método post; th:action="@{/login}"
username
password
Primeiro vamos adicionar uma nova pasta (security
) à nossa estrutura do projeto:
Antes de iniciar a codificação da autenticação, precisamos adicionar duas classes na camada model
e uma classe na camada repository
As seguintes classes representam os dados persistentes (camada model):
Arquivo model/Papel.java
package com.professorangoti.condominio.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Papel { private Long id; private String nome; }
Arquivo model/Usuario.java
package com.professorangoti.condominio.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Usuario { private Long id; private String email, senha, papeis; }
e a classe de acesso a dados do usuario repository/UsuarioRepository.java:
package com.professorangoti.condominio.repository; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import com.professorangoti.condominio.model.Usuario; @Repository public class UsuarioRepository { @Autowired JdbcTemplate jdbc; public Usuario buscaPorEmail(String email) { String sql = "SELECT usuarios.*, group_concat(distinct papeis.nome separator ', ') papeis FROM usuarios " + "inner join usuarios_papeis ON usuarios.usuario_id = usuarios_papeis.usuario_id " + "inner JOIN papeis ON usuarios_papeis.papel_id = papeis.papel_id " + "where email = ? " + "group by usuarios.usuario_id, usuarios.email;"; return jdbc.queryForObject(sql, this::mapper, email); } private Usuario mapper(ResultSet registro, int linha) throws SQLException { return new Usuario(registro.getLong("usuario_id"), registro.getString("email"), registro.getString("senha"), registro.getString("papeis")); } }
Como as credenciais do usuário estão armazenadas em banco de dados com a senha criptografada, precisamos configurar um Bean DaoAuthenticationProvider que usa um UserDetailsService e PasswordEncoder para autenticar um nome de usuário e senha.
Veja em DaoAuthenticationProvider :: Spring Security
Fonte: spring.io
Arquivo security/CondominioSecurity.java
package com.professorangoti.condominio.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class CondominioSecurity { @Bean public UserDetailsService userDetailsService() { return new CondominioUserDetailsService(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .antMatchers("/","/home").permitAll() .anyRequest().authenticated()) .formLogin(form -> form.loginPage("/login").permitAll()) .logout().logoutSuccessUrl("/"); return http.build(); } }
Arquivo security/CondominioUserDetails.java
package com.professorangoti.condominio.security; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.professorangoti.condominio.model.Usuario; public class CondominioUserDetails implements UserDetails { private Usuario usuario; public CondominioUserDetails(Usuario usuario) { this.usuario = usuario; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { String[] papeis = usuario.getPapeis().split(","); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (String papel : papeis) { authorities.add(new SimpleGrantedAuthority(papel)); } return authorities; } @Override public String getPassword() { return usuario.getSenha(); } @Override public String getUsername() { return usuario.getEmail(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
Arquivo security/CondominioUserDetailsService.java
package com.professorangoti.condominio.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.professorangoti.condominio.model.Usuario; import com.professorangoti.condominio.repository.UsuarioRepository; public class CondominioUserDetailsService implements UserDetailsService { @Autowired private UsuarioRepository usuarioRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Usuario usuario = usuarioRepository.buscaPorEmail(username); if (usuario == null) { throw new UsernameNotFoundException("Usuário não autenticado!"); } System.out.println(usuario.getPapeis()); return new CondominioUserDetails(usuario); } }
O método SecurityFilterChain filterChain(HttpSecurity http)
estabelece as regras de segurança da aplicação. O objeto http
é usado para escrever as regras da autorização e autenticação. Usamos o método authorizeHttpRequests
para restringir o acesso com base na URL da requisição HTTP. As regras são aplicadas na ordem apresentada.
Regra | Autorização |
| autoriza o acesso livre aos recursos mapeados com a URL "/" ou "/home" |
| qualquer outra requisição precisa de autenticação |
| configura a URL da página de login a ser exibida. Necessita definir um controle mapeado com GET /login. |
| configura a página a ser exibida após o logout. |
Inserir este método no arquivo HomeController.java
@GetMapping("/login") public String login() { return "login"; }
Arquivo resources/templates/login.html
<!DOCTYPE html> <html th:include="template :: modelo"> <div th:fragment="conteudo"> <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="/home">Home</a></li> <li class="breadcrumb-item active" aria-current="page">Autenticação (login)</li> </ol> </nav> <div th:if="${param.error}"> <h6 class="alert alert-danger">Usuario/senha inválidos</h6> </div> <form th:action="@{/login}" method="post"> <div class="mb-3"> <label class="form-label">Email</label> <input class="form-control" type="text" name="username" placeholder="E-mail" /> </div> <div class="mb-3"> <label class="form-label">Senha</label> <input class="form-control" type="password" name="password" placeholder="Senha" /> </div> <input class="btn btn-primary" type="submit" value="Log in" /> </form> </div> </html>
Arquivo resources/templates/template.html
<!-- Alteração do fragmento da barra de navegação para adicionar botão de logout e saudação do usuário logado --> <div class="d-flex col-2" sec:authorize="isAuthenticated()"> <span class="text-white small" sec:authentication="name">opa</span> </div> <div class="align-self-center"> <form th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()"> <input class="btn btn-secondary btn-sm p-1 small" type="submit" value="Sair" /> </form> <a href="/login" sec:authorize="!isAuthenticated()" class="btn btn-secondary btn-sm p-1 small">Entrar</a> </div> <!-- Alteração do fragmento da barra de navegação para adicionar botão de logout e saudação do usuário logado -->
A autorização consiste em estabelecer regras relacionando usuários com permissões de acesso a recursos da aplicação. Os recursos de uma aplicação Spring Web MVC consistem em mapear URLs e métodos de uma classe da camada controller.
# | Endpoint | Perfil permitido |
1 | /upload | MORADOR, ADMIN |
1 | /cad_apto | MORADOR, ADMIN |
1 | /cad_prop | MORADOR, ADMIN |
2 | /fotos_apto | USUARIO, MORADOR, ADMIN |
2 | /rel_apto | USUARIO, MORADOR, ADMIN |
2 | /rel_prop | USUARIO, MORADOR, ADMIN |
3 | /excluir_foto | ADMIN |
3 | /excluir_prop | ADMIN |
4 | /home ou / | aberto |
4 | /login | aberto |
Papel | Usuário |
MORADOR | venus@teste.com terra@teste.com |
USUARIO | mercurio@teste.com marte@teste.com |
ADMIN | jupiter@teste.com |
As autorizações também serão feitas dentro do arquivo CondominioSecurity.java
. Para cada conjunto de endpoints teremos um filtro que declara qual perfil tem autorização de acesso.
O primeiro conjunto (#1) composto pelos endpoints: /upload, /cad_apto e /cad_apto permite o acesso aos papeis MORADOR e ADMIN e a regra fica assim:
antMatchers("/upload", "/cad_apto", "/cad_prop").hasAnyAuthority("MORADOR", "ADMIN")
O segundo conjunto (#2) composto pelos endpoints: /fotos_apto, /rel_apto, /rel_prop permite o acesso aos papeis USUARIO, MORADOR, ADMIN e a regra fica assim:
antMatchers("/fotos_apto", "/rel_apto", "/rel_prop").hasAnyAuthority("USUARIO", "MORADOR", "ADMIN")
O terceiro conjunto (#3) composto pelos endpoints: /excluir_foto, /excluir_prop permite o acesso somente ao ADMIN e a regra fica assim:
antMatchers("/excluir_foto", "/excluir_prop").hasAnyAuthority("ADMIN")
O quarto conjunto (#4) composto pelos endpoints: /home, /, /login permite o acesso aberto (sem necessidade de autenticação) e a regra fica assim:
antMatchers("/", "/home").permitAll() formLogin(form -> form.loginPage("/login").permitAll())
e o método fica assim no final da configuração:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .antMatchers("/upload", "/cad_apto", "/cad_prop").hasAnyAuthority("MORADOR", "ADMIN") // permissão para criar .antMatchers("/fotos_apto", "/rel_apto", "/rel_prop").hasAnyAuthority("USUARIO", "MORADOR", "ADMIN") // permissão para visualizar .antMatchers("/excluir_foto", "/excluir_prop").hasAnyAuthority("ADMIN") // permissão para editar/excluir .antMatchers("/", "/home").permitAll() .anyRequest().authenticated()) .formLogin(form -> form.loginPage("/login").permitAll()) .logout().logoutSuccessUrl("/"); return http.build(); }
https://github.com/thymeleaf/thymeleaf-extras-springsecurity
Este módulo oferece alguns atributos que usaremos nas páginas da aplicação para exibir as funções do sistema conforme o papel do usuário autenticado.
Lista dos atributos:
Renderiza o elemento se o usuário autenticado estiver autorizado a ver a URL especificada. Exemplo:
<a class="dropdown-item" href="/rel_prop" sec:authorize-url="/rel_prop">Listagem</a>
Renderiza seu conteúdo quando a expressão do atributo é avaliada como verdadeira. A expressão isAuthenticated()
retorna true se o usuário realizou a autenticação. Exemplo:
<a href="/login" sec:authorize="!isAuthenticated()" class="btn btn-secondary btn-sm p-1 small">Entrar</a>
Este atributo oferece acesso às propriedades do objeto de autenticação. Neste exemplo, queremos exibir na barra de navegação o nome do usuário e seu perfil (authorities).
<div class="col-3 text-white small" sec:authorize="isAuthenticated()"> <span sec:authentication="name">opa</span> <span sec:authentication="principal.authorities"></span> </div>