Capítulo 7

Apêndice: Implementando Segurança

Apêndice não faz parte do curso. Os apêndices são conteúdos adicionais que não fazem parte da carga horária regular do curso. São conteúdos extras para direcionar seus estudos após o curso.

7.1 Protegendo nossas URIs

Agora que concluímos o desenvolvimento das funcionalidades principais da compra de ingressos, precisamos proteger para que somente pessoas autorizadas possam cadastrar Filmes, Salas, Sessões. Além de exigir que um comprador autentique-se para efetuar uma compra. Para isso precisamos identificar quais URIs devemos proteger e quais devemos deixar acessíveis.

Por exemplo, sabemos que a URI /filme/em-cartaz deve ser acessível por todos, já a URI /filme só deve só deve ser acessível por pessoas autorizadas. Perceba que dessa maneira teremos que criar regras para cada URI individualmente. O que pode ser um problema de acordo com a quantidade de URIs que teremos no nosso sistema.

Para facilitar a criação dessas regras, podemos agrupar nossas URIs de uma forma que possamos aplicar as regras para cada grupo. Por exemplo, todas as URIs de cadastro que só devem ser efetuadas por administradores, podemos pré fixá-las com /admin. Nesse caso nossa URI /filme que deve ser protegida ficará como /admin/filme. Devemos repetir esse processo para as demais URIs de cadastro.

Agora é a melhor hora de respirar mais tecnologia!

Se você está gostando dessa apostila, certamente vai aproveitar os cursos online que lançamos na plataforma Alura. Você estuda a qualquer momento com a qualidade Caelum. Programação, Mobile, Design, Infra, Front-End e Business! Ex-aluno da Caelum tem 15% de desconto, siga o link!

Conheça a Alura Cursos Online.

7.2 Configurando Spring Security

Agora que temos nossas URIs agrupadas, podemos implementar a lógica para proteger nossa aplicação. Para isso teremos que a cada request verificar qual a URI que está sendo acessada, se for uma URI protegida devemos verificar se o usuário está logado e se o mesmo tem as permissões necessárias. Em caso negativo, devemos redirecioná-lo para uma tela de login.

Poderíamos implementar isso utilizando um Filtro da spec de Servlet. Porém lidar com todas as regras, navegações e segurança dos dados não é algo tão trivial.

Para facilitar essa tarefa, existem Frameworks responsáveis somente por segurança em uma aplicação WEB. Dentre eles dois que se destacam bastante são KeyCloak e Spring Security. Como estamos usando Spring em nossa aplicação, vamos implementar a segurança através do Spring Security.

Vamos começar configurando as regras de acesso baseado nas URIs. Para isso vamos criar uma classe que herde de WebSecurityConfigurerAdapter, e sobrescrever o método protected void configure(HttpSecurity http) throws Exception:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        .
        .
        .
    }
}

Dentro do método configure(HttpSecurity http) vamos usar o objeto HttpSecurity para declarar nossas regras. A classe HttpSecurity tem uma interface fluente, onde podemos encadear métodos.

Vamos começar protegendo as URIs que comecem com /admin/. Elas por sua vez só podem ser acessadas por usuário com o perfil de ADMIN.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN");
    }
}

Agora queremos proteger URIs que comecem com /compra/, essas por sua vez só podem ser acessíveis por usuários com perfil de comprador.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR");
    }
}

URIs começadas com /filme podem ser acessadas por qualquer um.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll();
    }
}

Da mesma forma a URI /sessao/{id}/lugares e / também pode ser acessada por qualquer um.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll();
    }
}

Agora precisamos definir qualquer request que tenha um perfil associado ou não mapeados devem ser autenticadas (no nosso caso são as URIs /admin e /compra).

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll()
            .anyRequest()
                .authenticated();
    }
}

Precisamos também indicar qual a URI que deve ser usada para chegar na página de Login e qual URI deve ser usada para fazer Logout. E essas devem ser liberadas para qualquer um acessar.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll()
            .anyRequest()
                .authenticated()
            .and()
                .formLogin()
                    .usernameParameter("email")
                    .loginPage("/login")
                    .permitAll()
            .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .permitAll();                    
    }
}

Por padrão o Spring Security vem habilitado a verificação de CSRF, que consiste em verificar se em todos nossos formulários tem um INPUT HIDDEN com um identificador (token) gerado aleatoriamente.

Como não estamos gerando esse Token e nem colocando esse INPUT HIDDEN em nossos formulários, vamos desabilitar uma prevenção de segurança contra ataques CSRF.

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable().authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll()
            .anyRequest()
                .authenticated()
            .and()
                .formLogin()
                    .usernameParameter("email")
                    .loginPage("/login")
                    .permitAll()
            .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .permitAll();                    
    }
}

Além disso precisamos liberar requests para nossos arquivos estáticos dentro de webapp/assets.

Podemos adicionar um novo antMatcher para /assets/** e usar o permitAll(). Porém, para evitar colocar exceções as regras de acesso, vamos sobrescrever outro método da classe WebSecurityConfigurerAdapter.

Dessa vez vamos sobrescrever o método public void configure(WebSecurity web) throws Exception. Através do objeto WebSecurity podemos pedir para que seja ignorado uma ou mais URIs:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable().authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll()
            .anyRequest()
                .authenticated()
            .and()
                .formLogin()
                    .usernameParameter("email")
                    .loginPage("/login")
                    .permitAll()
            .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .permitAll();                    
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/assets/**");
    }
}

Por fim precisamos avisar ao Spring que essa classe deve ser utilizada para fazer as verificações de segurança. Para isso vamos anotá-la com @EnableWebSecurity:

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable().authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/compra/**").hasRole("COMPRADOR")
            .antMatchers("/filme/**").permitAll()
            .antMatchers("/sessao/**/lugares").permitAll()
            .antMatchers("/").permitAll()
            .anyRequest()
                .authenticated()
            .and()
                .formLogin()
                    .usernameParameter("email")
                    .loginPage("/login")
                    .permitAll()
            .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .permitAll();                    
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/assets/**");
    }
}

Agora precisamos registrar um filtro do Spring Security para que ele possa observar todas nossas requests e assim aplicar as regras de segurança que definimos.

O Spring Security disponibiliza uma classe chamada AbstractSecurityWebApplicationInitializer que já faz toda a parte de registrar o filtro e ficar observando as requests. Basta apenas que uma classe no nosso projeto herde de AbstractSecurityWebApplicationInitializer. E precisamos informar para o Spring que essa é uma classe de configuração, anotando-a com @Configuration.

@Configuration
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {

}

Além disso precisamos que o ao inicializar o filtro seja carregado nossa classe de configuração de acesso.

Para isso vamos sobrescrever o construtor sem argumentos da nossa classe SecurityInitializer e chamar o construtor de AbstractSecurityWebApplicationInitializer passando para ele a classe de configuração.

@Configuration
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    public SecurityInitializer() {
        super(SecurityConfiguration.class);
    }
}

7.3 Exercício - Implementando segurança em nossa aplicação

  1. Crie a classe SecurityInitializer no pacote br.com.caelum.ingresso.configuracao e faça com que ela herde de AbstractSecurityWebApplicationInitializer e anote-a com @Configuration:

     @Configuration
     public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    
     }
    
  2. Crie a classe SecurityConfiguration e faça com que ela herde de WebSecurityConfigurerAdapter anote-a com @EnableWebSecurity:

     @EnableWebSecurity
     public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
     }
    
  3. Sobrescreva o método configure que recebe um objeto HttpSecurity:

     @EnableWebSecurity
     public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
         @Override
         protected void configure(HttpSecurity http) throws Exception {   
    
         }
    
     }
    
  4. Implemente nesse método as restrições para as _URI_s:

     @EnableWebSecurity
     public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
         @Override
         protected void configure(HttpSecurity http) throws Exception {   
             http
                 .csrf().disable().authorizeRequests()
                     .antMatchers("/admin/**").hasRole("ADMIN")
                     .antMatchers("/compra/**").hasRole("COMPRADOR")
                     .antMatchers("/filme/**").permitAll()
                     .antMatchers("/sessao/**/lugares").permitAll()
                     .antMatchers("/magic/**").permitAll()
                     .antMatchers("/").permitAll()
                 .anyRequest()
                     .authenticated()
                 .and()
                     .formLogin()
                         .usernameParameter("email")
                         .loginPage("/login")
                         .permitAll()
                 .and()
                     .logout()
                         .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                         .permitAll();
         }
    
     }
    
  5. Sobrescreva o método configure que recebe um objeto WebSecurity, e libere os arquivos estáticos da nossa aplicação:

     @EnableWebSecurity
     public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
         @Override
         protected void configure(HttpSecurity http) throws Exception {   
             http
                 .csrf().disable().authorizeRequests()
                     .antMatchers("/admin/**").hasRole("ADMIN")
                     .antMatchers("/compra/**").hasRole("COMPRADOR")
                     .antMatchers("/filme/**").permitAll()
                     .antMatchers("/sessao/**/lugares").permitAll()
                     .antMatchers("/magic/**").permitAll()
                     .antMatchers("/").permitAll()
                 .anyRequest()
                     .authenticated()
                 .and()
                     .formLogin()
                         .usernameParameter("email")
                         .loginPage("/login")
                         .permitAll()
                 .and()
                     .logout()
                         .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                         .permitAll();
         }
    
         @Override
         public void configure(WebSecurity web) throws Exception {
             web.ignoring().antMatchers("/assets/**");
         }
    
     }
    
  6. Crie um construtor sem argumentos para a classe SecurityInitializere nele chame o super e passe a classe SecurityConfiguration para ele.

     @Configuration
     public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    
         public SecurityInitializer() {
             super(SecurityConfiguration.class);
         }
    
     }
    
  7. Crie um controller chamado LoginController e crie uma action para /login com método GET e retorne para a view login:

     @Controller
     public class LoginController {
    
         @GetMapping("/login")
         public String login(){
             return "login";
         }
     }
    

7.4 Usuário, Senha e Permissão

Agora que já configuramos a segurança da nossa aplicação, precisamos criar algo que represente nosso usuário e quais permissões ele deve ter. Vamos começar modelando a classe Permissao e depois a classe Usuario:

    @Entity
    public class Permissao {
        @Id
        private String nome;

        public Permissao(String nome) {
            this.nome = nome;
        }

        /**
        * @deprecated hibernate only
        */
        public Permissao() {
        }

        //getters e setters
    }

    @Entity
    public class Usuario {

        @Id
        @GeneratedValue
        private Integer id;

        private String email;
        private String password;

        @ManyToMany(fetch = FetchType.EAGER)
        private Set<Permissao> permissoes = new HashSet<>();

        /**
        * @deprecated hibernate only
        */
        public Usuario() {
        }

        public Usuario(String email, String password, Set<Permissao> permissoes) {
            this.email = email;
            this.password = password;     
            this.permissoes = permissoes;       
        }

        //getters e setters
    }

Precisamos que o Spring Security saiba pegar as informações de login, senha e permissão. Para isso precisamos que nossa classe Permissao implemente a interface GrantedAuthority e nossa classe Usuario implemente a interface UserDetails.

Dessa forma vamos ser obrigados a implementar os métodos que retornam exatamente as informações de login, senha e permissão (além de algumas outras se quisermos implementar).


    @Entity
    public class Permissao implements GrantedAuthority {
        // ... restante da implementação

        @Override
        public String getAuthority() {
            return nome;
        }
    }


    @Entity
    public class Usuario implements UserDetails {
        // ... restante da implementação

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

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

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

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

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

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

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

Além disso precisamos de uma classe que busque no banco de dados um UserDetails a partir de um username. Para isso vamos criar um DAO e implementar a interface UserDetailsService.

    @Repository
    public class LoginDao implements UserDetailsService {

        @PersistenceContext
        private EntityManager manager;

        @Override
        public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
            try {
                return manager
                        .createQuery("select u from Usuario u where u.email = :email", Usuario.class)
                        .setParameter("email", email)
                        .getSingleResult();
            } catch (NoResultException e){
                throw new UsernameNotFoundException("Email " + email + "Não encontrado!");
            }

        }
    }

Por fim precisamos alterar nossas configurações de segurança para que seja utilizado UserDetailsService para validar se o usuário existe e/ou se ele tem as devidas permissões para acessar o recurso.

Para isso vamos alterar a classe SecurityConfiguration para que ela receba injetado um UserDetailsService e sobrescrever o método protected void configure(AuthenticationManagerBuilder auth) throws Exception. e fazer com que o objeto AuthenticationManagerBuilder use nosso UserDetailsService como meio de autenticação.

É uma má prática salvar a senha em texto puro no banco de dados. Por isso precisamos codificá-la/criptografá-la antes de salvar. Com isso, nossa autenticação deve saber como comparar a senha que foi digitada no formulário de login com a senha que está salva no banco.

Para isso precisamos informar qual o algoritmo para codificar/criptografar a senha foi utilizado na hora de salvar a senha no banco de dados. Existem diversos algoritmos para fazer essa tarefa por exemplo: MD5, SHA1 , SHA1-256, SHA1-512, BCrypt entre outros.

No nosso caso iremos utilizar uma implementação do BCrypt que é recomendado na documentação do Spring Security.

Essa implementação é o BCryptPasswordEncoder:

    @EnableWebSecurity    
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private UserDetailsService userDetailsService;

        //demais métodos

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }

    }

Como nossas configurações do Spring estão em XML precisamos que nossa classe SecurityConfiguration leia esse XML, do contrário não será possível injetar UserDetailsService. Para isso vamos anotar nossa classe com @ImportResource("/WEB-INF/spring-context.xml")

    @EnableWebSecurity    
    @ImportResource("/WEB-INF/spring-context.xml")
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private UserDetailsService userDetailsService;

        //demais métodos

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }

    }

Editora Casa do Código com livros de uma forma diferente

Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam tecnicamente o assunto para revisar os livros a fundo. Não têm anos de experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e obsessão por livros de qualidade a preços justos.

Casa do Código, ebook com preço de ebook.

7.5 Exercício - Implementando UserDetails, UserDetailsService e GrantedAuthority

  1. Crie a classe Permissao no pacote br.com.caelum.ingresso.modelo:

     @Entity
     public class Permissao {
         @Id
         private String nome;
    
         public Permissao(String nome) {
             this.nome = nome;
         }
    
         /**
         * @deprecated hibernate only
         */
         public Permissao() {
         }
    
         public String getNome() {
             return nome;
         }
    
         public void setNome(String nome) {
            this.nome = nome;
         }
     }
    
  2. Crie a classe Usuario no pacote br.com.caelum.ingresso.modelo:

    
     @Entity
     public class Usuario {
    
         @Id
         @GeneratedValue
         private Integer id;
    
         private String email;
         private String password;
    
         @ManyToMany(fetch = FetchType.EAGER)
         private Set<Permissao> permissoes = new HashSet<>();
    
         /**
         * @deprecated hibernate only
         */
         public Usuario() {
         }
    
         public Usuario(String email, String password, Set<Permissao> permissoes) {
             this.email = email;
             this.password = password;   
             this.permissoes = permissoes;         
         }
    
         //getters e setters
     }
    
  3. Faça com que a classe Permissao implemente a interface GrantedAuthority e faça com que o método getAuthority retorne o nome da permissão:

    
     @Entity
     public class Permissao implements GrantedAuthority {
         @Id
         private String nome;
    
         public Permissao(String nome) {
             this.nome = nome;
         }
    
         /**
         * @deprecated hibernate only
         */
         public Permissao() {
         }
    
         public String getNome() {
             return nome;
         }
    
         public void setNome(String nome) {
            this.nome = nome;
         }
    
         @Override
         public String getAuthority() {
             return nome;
         }
     }
    
  4. Faça com que a classe Usuario implemente a interface UserDetail:

     @Entity
     public class Usuario implements UserDetails {
         ...
    
         @Override
         public Collection<? extends GrantedAuthority> getAuthorities() {
             return permissoes;
         }
    
         @Override
         public String getPassword() {
             return password;
         }
    
         @Override
         public String getUsername() {
             return email;
         }
    
         @Override
         public boolean isAccountNonExpired() {
             return true;
         }
    
         @Override
         public boolean isAccountNonLocked() {
             return true;
         }
    
         @Override
         public boolean isCredentialsNonExpired() {
             return true;
         }
    
         @Override
         public boolean isEnabled() {
             return true;
         }
    
     }
    
  5. Crie a classe LoginDao e implemente a interface UserDetailsService faça uma query para retornar um Usuario por email:

     @Repository
     public class LoginDao implements UserDetailsService {
    
         @PersistenceContext
         private EntityManager manager;
    
         @Override
         public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
             try {
                 return manager
                         .createQuery("select u from Usuario u where u.email = :email", Usuario.class)
                         .setParameter("email", email)
                         .getSingleResult();
             } catch (NoResultException e){
                 throw new UsernameNotFoundException("Email " + email + "Não encontrado!");
             }
    
         }
     }
    
  6. Na classe SecurityConfiguration vamos adicionar a anotação @ImportResource para podermos injetar nosso UserDetailsService:

    @EnableWebSecurity
    @ImportResource("/WEB-INF/spring-context.xml")
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
     //restante da implementação
    }
    
  7. Injete UserDetailsService e sobrescreva o método configure que recebe um AuthenticationManagerBuilder:

     @EnableWebSecurity
     @ImportResource("/WEB-INF/spring-context.xml")
     public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
         @Autowired
         private UserDetailsService userDetailsService;
    
         //demais métodos
    
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
         }
    
     }