Solucionado (ver solução)
Solucionado
(ver solução)
15
respostas

[Bug] Dúvidas para configurar permissões para utilizar H2 - Erro 403

Estou com um problema para configurar a classe SecurityConfigurations...

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Para liberar a permissão na hora de testar a aplicação no Insomnia, tive que adicionar o trecho acima para liberar o acesso. Fazendo isso, com o "permitAll()", ele vai liberar todas as permissões. Eu tentei mudar para "authenticated()" para evitar que isso aconteça, para garantir que os métodos sejam autenticados, mas não funcionaram, nem alterando para "hasRole("USER")", retornando o Erro 403.

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Uma dúvida: o comando "hasRole("ADMIN")" dito na aula, quando damos permissão para usuários admin remover dados, essa operação é feita diretamente sem eu precisar gerar token?

Outra falha que estou encontrando nos testes, não sei se coincide, no Insomnia, são os endpoints que eu montei no controller dos métodos get (passando id) e os métodos delete, que eu criei dos dois formatos diferentes, o convencional, que remove direto do banco e o lógico, ensinado no curso, que invalida, que também retornaram o Erro 403 Forbidden.

Insira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidade

15 respostas

Oi!

Acho que no seu caso a configuração deveria ser:

//somente usuario logado com perfil ADMIN pode disparar a requisição:

req..requestMatchers(HttpMethod.DELETE, "/beneficiaries/**").hasRole("ADMIN")

No seu código você está configurando a url /beneficiaries, mas a url para excluir tem o id do registro: /beneficiaries/{id}

OBS: para disparar essa requisição você precisa fazer login com algum usuário que tenha role ADMIN e enviar o token desse usuário na requisição.

Então, para excluir o registro continua não funcionando, mas no stacktrace mostra que montou a query do hibernate quando executa. O mesmo problema acontece quando eu tento fazer a consulta (GET) passando o id, que eu criei depois, no Insomnia.

Hibernate: 
    select
        u1_0.id,
        u1_0.login,
        u1_0.password 
    from
        usuarios u1_0 
    where
        u1_0.login=?
Hibernate: 
    insert 
    into
        beneficiarios
        (ativo, dataAtualizacao, dataInclusao, dataNascimento, bairro, cep, cidade, complemento, logradouro, numero, uf, nome, telefone, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate: 
    insert 
    into
        documentos
        (ativo, beneficiario_id, dataAtualizacao, dataExpedicao, dataInclusao, descricao, numero, tipoDocumento, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate: 
    select
        u1_0.id,
        u1_0.login,
        u1_0.password 
    from
        usuarios u1_0 
    where
        u1_0.login=?
Hibernate: 
    insert 
    into
        beneficiarios
        (ativo, dataAtualizacao, dataInclusao, dataNascimento, bairro, cep, cidade, complemento, logradouro, numero, uf, nome, telefone, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate: 
    insert 
    into
        documentos
        (ativo, beneficiario_id, dataAtualizacao, dataExpedicao, dataInclusao, descricao, numero, tipoDocumento, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate: 
    insert 
    into
        documentos
        (ativo, beneficiario_id, dataAtualizacao, dataExpedicao, dataInclusao, descricao, numero, tipoDocumento, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate: 
    select
        u1_0.id,
        u1_0.login,
        u1_0.password 
    from
        usuarios u1_0 
    where
        u1_0.login=?
Hibernate: 
    select
        b1_0.id,
        b1_0.ativo,
        b1_0.dataAtualizacao,
        b1_0.dataInclusao,
        b1_0.dataNascimento,
        b1_0.bairro,
        b1_0.cep,
        b1_0.cidade,
        b1_0.complemento,
        b1_0.logradouro,
        b1_0.numero,
        b1_0.uf,
        b1_0.nome,
        b1_0.telefone 
    from
        beneficiarios b1_0 
    where
        b1_0.ativo 
    order by
        b1_0.nome 
    offset
        ? rows 
    fetch
        first ? rows only
Hibernate: 
    select
        d1_0.beneficiario_id,
        d1_0.id,
        d1_0.ativo,
        d1_0.dataAtualizacao,
        d1_0.dataExpedicao,
        d1_0.dataInclusao,
        d1_0.descricao,
        d1_0.numero,
        d1_0.tipoDocumento 
    from
        documentos d1_0 
    where
        d1_0.beneficiario_id=?
Hibernate: 
    select
        d1_0.beneficiario_id,
        d1_0.id,
        d1_0.ativo,
        d1_0.dataAtualizacao,
        d1_0.dataExpedicao,
        d1_0.dataInclusao,
        d1_0.descricao,
        d1_0.numero,
        d1_0.tipoDocumento 
    from
        documentos d1_0 
    where
        d1_0.beneficiario_id=?
Hibernate: 
    select
        d1_0.beneficiario_id,
        d1_0.id,
        d1_0.ativo,
        d1_0.dataAtualizacao,
        d1_0.dataExpedicao,
        d1_0.dataInclusao,
        d1_0.descricao,
        d1_0.numero,
        d1_0.tipoDocumento 
    from
        documentos d1_0 
    where
        d1_0.beneficiario_id=?
Hibernate: 
    select
        d1_0.beneficiario_id,
        d1_0.id,
        d1_0.ativo,
        d1_0.dataAtualizacao,
        d1_0.dataExpedicao,
        d1_0.dataInclusao,
        d1_0.descricao,
        d1_0.numero,
        d1_0.tipoDocumento 
    from
        documentos d1_0 
    where
        d1_0.beneficiario_id=?

Quando eu gero o token, o usuário que eu criei no Banco foi apenas este. Então é este usuário que eu devo alterar na role? Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Erros de requisição: Fazendo consulta, passando id: Insira aqui a descrição dessa imagem para ajudar na acessibilidadeAções diferentes que criei para excluir (em ambas estou passando o token): Insira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidade

Manda aqui o seu código atual das configs de segurança

Segue códigos:

package com.plano.saude.cadastro.infra.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;

@Configuration
@EnableWebSecurity
public class SecurityConfigurations {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.ignoringRequestMatchers(toH2Console()).disable())
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(req -> {
                    req.requestMatchers(toH2Console()).permitAll();
                    req.requestMatchers("/login").permitAll();
                    req.requestMatchers("/beneficiaries").permitAll();
                    req.requestMatchers(HttpMethod.DELETE, "/beneficiaries/**").hasRole("ADMIN");
                })
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .build();
    }

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

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

}
package com.plano.saude.cadastro.infra.security;

public record DadosTokenJWT(String token) {
}
package com.plano.saude.cadastro.infra.security;

import com.plano.saude.cadastro.domain.usuario.UsuarioRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class SecurityFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;

    @Autowired
    private UsuarioRepository usuarioRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        var tokenJWT = recoverToken(request);

        if (tokenJWT != null){
            var subject = tokenService.getSubject(tokenJWT);
            var userName = usuarioRepository.findByLogin(subject);

            var authentication = new UsernamePasswordAuthenticationToken(userName, null, userName.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String recoverToken(HttpServletRequest request) {
        var authorizationHeader = request.getHeader("Authorization");
        if (authorizationHeader != null) {
            return authorizationHeader.replace("Bearer ", "");
        }

        return null;
    }
}
package com.plano.saude.cadastro.infra.security;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.plano.saude.cadastro.domain.usuario.Usuario;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

@Service
public class TokenService {
    @Value("${api.security.token.secret}")
    private String secret;
    private static final String ISSUER = "API plano.saude";
    public String generateToken(Usuario usuario){
        try {
            var algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withIssuer(ISSUER)
                    .withSubject(usuario.getLogin())
                    .withExpiresAt(expirationDate())
                    .sign(algorithm);
        } catch (JWTCreationException exception){
            throw new RuntimeException("Error to create JWT token", exception);
        }
    }
    public String getSubject(String tokenJWT){
        try {
            var algorithm = Algorithm.HMAC256(secret);
            return JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .build()
                    .verify(tokenJWT)
                    .getSubject();
        } catch (JWTVerificationException exception){
            throw new RuntimeException("Invalid or expired JWT token!", exception);
        }
    }
    private Instant expirationDate() {
        return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
    }
}

Precisa configurar as outras urls:

.authorizeHttpRequests(req -> {
    req.requestMatchers(toH2Console()).permitAll();
    
    req.requestMatchers("/login").permitAll();
    
    //leitura (requisições get) esta liberado o acesso:
    req.requestMatchers(HttpMethod.GET, "/beneficiaries").permitAll();
    req.requestMatchers(HttpMethod.GET, "/beneficiaries/**").permitAll();
    
    //exclusao somente logado e com perfil ADMIN:
    req.requestMatchers(HttpMethod.DELETE, "/beneficiaries/**").hasRole("ADMIN");
})

As requisições GET devem funcionar sem precisar passar o token.

Eu configurei as urls e funcionaram, menos o delete, que continua com o mesmo erro 403.

Insira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidade

Agora deve ser a questão do perfil. Esse usuário que você fez login tem qual role?

Obs: No curso não mostrei como configurar diferentes roles no projeto e deixei apenas um role fixo na classe usuário

Boa pergunta, professor. A classe Usuario, eu criei igual como está no curso, deixando configurada como "ROLE_USER"

@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_USER"));
    }

Até tentei mudar, colocando o login que eu criei no banco para forçar no teste, mas também não funcionou, dando o mesmo erro:

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.ignoringRequestMatchers(toH2Console()).disable())
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(req -> {
                    req.requestMatchers(toH2Console()).permitAll();
                    req.requestMatchers("/login").permitAll();
                    req.requestMatchers("/beneficiaries").permitAll();
                    req.requestMatchers(HttpMethod.GET, "/beneficiaries").permitAll();
                    req.requestMatchers(HttpMethod.GET, "/beneficiaries/**").permitAll();
                    req.requestMatchers(HttpMethod.DELETE, "/beneficiaries/**").hasRole("alexandre.freitas");
                })
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .build();
    }

No método hasRole você precisa passar o role mesmo e não o username.

O problema está aqui mesmo:

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}

Desse jeito, todos os usuários vão ter o perfil USER fixo. Mas no seu caso você precisa que seja dinâmico e cada usuário pode ter roles distintos.

Isso não foi mostrado no curso e vai exigir bastante mudanças no projeto, além de ser necessário criar novas tabelas no banco de dados para armazenar os perfis de cada usuário.

Quais são as mudanças que eu devo fazer no projeto?

Nesse trecho, eu mantenho como estava antes:

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.ignoringRequestMatchers(toH2Console()).disable())
                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(req -> {
                    req.requestMatchers(toH2Console()).permitAll();
                    req.requestMatchers("/login").permitAll();
                    req.requestMatchers("/beneficiaries").permitAll();
                    req.requestMatchers(HttpMethod.GET, "/beneficiaries").permitAll();
                    req.requestMatchers(HttpMethod.GET, "/beneficiaries/**").permitAll();
                    req.requestMatchers(HttpMethod.DELETE, "/beneficiaries/**").hasRole("ADMIN");
                })
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .build();
    }

E nesse trecho, eu teria que colocar algo desse tipo?

//Por exemplo:
@Value("${api.security.access.roles}")
private String roles;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority("roles"));
}

Sendo assim, a tabela usuario, que foi criada dessa forma (abaixo), precisa ser adicionada mais colunas com as roles de permissão, correto?

create table usuarios(

    id bigint not null auto_increment,
    login varchar(100) not null,
    password varchar(255) not null,

    primary key(id)

);
solução!

Eu consegui encontrar uma solução, fazendo para o H2, colocando esta linha em destaque, que funcionou para os cenários de DELETE, sem retornar o erro 403 - Forbidden. Pode até não ser a melhor, mas para efeitos de conhecimento, experimentais e por atuar em um banco persistente em memória, pelo menos, para entender o funcionamento, acredito que serve, sem problemas. Do contrário, mantemos o que foi aprendido com as dicas que vimos no curso. Obrigado, professor! ;) Insira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidade

Show!

Um exemplo de como fazer com perfis de acesso:

@Entity
@Table(name = "usuarios")
public class Usuario implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nome;
    private String email;
    private String senha;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "usuarios_perfis", joinColumns = @JoinColumn(name = "usuario_id"), inverseJoinColumns = @JoinColumn(name = "perfil_id"))
    private List<Perfil> perfis = new ArrayList<>();

    public Usuario() {
    }

    public Usuario(DadosCadastroUsuario dados, String senha, List<Perfil> perfis) {
        this.nome = dados.nome();
        this.email = dados.email();
        this.senha = senha;
        this.perfis = perfis;
    }

    public Long getId() {
        return id;
    }

    public String getNome() {
        return nome;
    }

    public String getEmail() {
        return email;
    }

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

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

    @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;
    }

}
@Entity
@Table(name = "perfis")
public class Perfil implements GrantedAuthority {

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

    private String nome;

    public Long getId() {
        return id;
    }

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

}
@Service
public class UsuarioService implements UserDetailsService {

    @Autowired
    private UsuarioRepository repository;

    @Autowired
    private PerfilRepository perfilRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return this.repository.findByEmail(email);
    }

    public DadosUsuario cadastrar(DadosCadastroUsuario dados) {
        var emailJaCadastrado = this.repository.existsByEmail(dados.email());
        if (emailJaCadastrado) {
            throw new ValidacaoException("Email já cadastrado para outro usuário!");
        }

        var senhaBCrypt = passwordEncoder.encode(dados.senha());
        var perfis = carregarPerfis(dados.admin());

        var usuario = new Usuario(dados, senhaBCrypt, perfis);

        this.repository.save(usuario);
        return new DadosUsuario(usuario);
    }

    private List<Perfil> carregarPerfis(Boolean admin) {
        var perfis = new ArrayList<Perfil>();
        var perfilUser = perfilRepository.findByNome("ROLE_USER");
        perfis.add(perfilUser);

        if (admin != null && admin) {
            var perfilAdmin = perfilRepository.findByNome("ROLE_ADMIN");
            perfis.add(perfilAdmin);
        }

        return perfis;
    }

}
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {

    Usuario findByEmail(String email);

}
public interface PerfilRepository extends JpaRepository<Perfil, Long> {

    Perfil findByNome(String nome);

}
RestController
@RequestMapping("login")
public class LoginController {

    @Autowired
    private AuthenticationManager manager;

    @Autowired
    private TokenService tokenService;

    @Autowired
    private UsuarioService service;

    @PostMapping
    public ResponseEntity<DadosToken> efetuarLogin(@RequestBody @Valid DadosAutenticacao dados) {
        var authenticationToken = new UsernamePasswordAuthenticationToken(dados.email(), dados.senha());
        var authentication = manager.authenticate(authenticationToken);

        var tokenJWT = tokenService.gerarToken((Usuario) authentication.getPrincipal());

        return ResponseEntity.ok(new DadosToken(tokenJWT));
    }

    @PostMapping
    @Transactional
    public ResponseEntity<DadosUsuario> cadastrar(@RequestBody @Valid DadosCadastroUsuario dados) {
        var usuario = this.service.cadastrar(dados);
        return ResponseEntity.ok(usuario);
    }

}
public record DadosAutenticacao(
        @NotBlank(message = "Email é obrigatório!")
        @Email(message = "Email no formato inválido!")
        String email,
        @NotBlank(message = "Senha é obrigatória!")
        String senha
) {
}
public record DadosCadastroUsuario(
        @NotBlank(message = "Nome é origatório!")
        String nome,
        @NotBlank(message = "Email é origatório!")
        @Email(message = "Email em formato inválido!")
        String email,
        @NotBlank(message = "Senha é origatória!")
        String senha,
        Boolean admin) {
}
public record DadosToken(String token) {}
public record DadosUsuario(Long id, String nome, String email, Boolean admin) {

    public DadosUsuario(Usuario usuario) {
        this(usuario.getId(), usuario.getNome(), usuario.getEmail(), usuario.isAdmin());
    }

}
CREATE TABLE usuarios(
    id BIGINT NOT NULL AUTO_INCREMENT,
    nome VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    senha VARCHAR(100) NOT NULL,

    PRIMARY KEY(id)
);

CREATE TABLE perfis(
    id BIGINT NOT NULL AUTO_INCREMENT,
    nome VARCHAR(100) NOT NULL UNIQUE,

    PRIMARY KEY(id)
);

CREATE TABLE usuarios_perfis(
    usuario_id BIGINT NOT NULL,
    perfil_id BIGINT NOT NULL,

    PRIMARY KEY(usuario_id, perfil_id),
    CONSTRAINT USUARIOS_PERFIS_FK_USUARIO FOREIGN KEY(usuario_id) REFERENCES usuarios(id),
    CONSTRAINT USUARIOS_PERFIS_FK_PERFIL FOREIGN KEY(perfil_id) REFERENCES perfis(id)

);

INSERT INTO perfis VALUES(1, 'ROLE_ADMIN');
INSERT INTO perfis VALUES(2, 'ROLE_USER');

INSERT INTO usuarios VALUES(1, 'Administrador', 'admin@email.com.br', '$2a$10$Y50UaMFOxteibQEYLrwuHeehHYfcoafCopUazP12.rqB41bsolF5.');

INSERT INTO usuarios_perfis values(1, 1);

Professor, um adendo! Na classe UsuarioService, faltou fazer alguns ajustes, junto com a interface UsuarioRepository para funcionar, que são esses ajustes:

package com.plano.saude.cadastro.domain.usuario;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;

public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
    UserDetails findByLogin(String login);
    Usuario findByEmail(String login);

    boolean existsByEmail(String login); // Adicionei no final, para tratar no método cadastrar, na classe UsuarioService, que dava erro de compilação
}

Na classe Usuario, faltou adicionar no código:

public Boolean isAdmin() {
        return true;
    }

Quer mergulhar em tecnologia e aprendizagem?

Receba a newsletter que o nosso CEO escreve pessoalmente, com insights do mercado de trabalho, ciência e desenvolvimento de software