3

I have this `application.properties' file:

security.basic.enabled=false

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/appdata
spring.datasource.username=kleber
spring.datasource.password=123456
spring.datasource.continue-on-error=true

sprinf.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.cache=false

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.file-size-threshold=10MB

server.tomcat.max-http-post-size=10MB

and this App class:

@SpringBootApplication
@Controller
public class AppApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(AppApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(AppApplication.class);
    }

    @Bean
    public WebSecurityCustomizer ignoringCustomizer() {
        return (web) -> web
                .ignoring()
                        .antMatchers("/", "/login", "/logout", "/register", "/error", "/css/**", "/js/**", "/img/**");
    }
    
    @Bean
    public SpringSecurityDialect springSecurityDialect() {
        return new SpringSecurityDialect();
    }

    @RequestMapping(value = "/")
    public String index(Model model) {
        return "index";
    }

    @RequestMapping(value = "/login")
    public String login(Model model) {
        return "login";
    }

    @RequestMapping(value = "/register", method=RequestMethod.GET)
    public String register(Model model) {
        model.addAttribute("obj", new Usuario());
        return "register";
    }

    @RequestMapping(value = "/register", method=RequestMethod.POST)
    public String register(@ModelAttribute Usuario usuario) {
        return "redirect:/login";
    }
}

I have tried add a Bean to the class above, like that:

@Bean
public UserDetailsService userDetailsService() {
    return new UserDetailsService() { ... }
}

@Bean PasswordEncoder passwordEncoder() {
    return new PasswordEncoder() { ... }
}

but this do not work. My guess is I need some way to configure them in the method WebSecurityCustomizer ignoringCustomizer() , but looking the documentation for the class WebSecurityCustomizer I do not see any way to do that.

Anyone can give any hints of how to do that?

UPDATE #1

Searching through the official site, I found some reference documentation and blog post telling the recommended way to do some actions close to what I need, but I am still struggling to get right.

First link, it's the reference page for the deprecated class WebSecurityConfigurerAdapter, where it's said to:

Use a SecurityFilterChain Bean to configure HttpSecurity or a WebSecurityCustomizer Bean to configure WebSecurity

HttpSecurity have a method to define a UserDetailsService Bean, but how I use it in my code?

The other link it's a blog post describing the old way to do some authentication tasks, and the new recommended way. The closest examples to what I want it's in the section about JDBC Authentication and In-Memory Authentication, and both of them are based on the use of a UseDetailsManager, if I am not wrong. I also tried add a new Bean like that:

@Bean
public UserDetailsManager userDetailsManager() {
    return new UserDetailsManager() { ... }
}

but does not work. What's the right way to do override the beans I want now?

UPDATE 2

I currently have this code, which is still not working properly. with this configuration, I can register a new user (which is created in the database with success), but I cannot login with this user.

@SpringBootApplication
@Controller
public class App extends SpringBootServletInitializer {
    @Autowired
    UsuarioDao usuarioDao;

    @Autowired
    CredencialDao credencialDao;

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(App.class);
    }

    @Bean
    public SpringSecurityDialect springSecurityDialect() {
        return new SpringSecurityDialect();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .cors().disable()
            .authorizeRequests()
                .antMatchers("/", "/login", "/logout", "/register", "/error", "/css/**", "/js/**", "/img/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .authenticationProvider(authProvider());
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService());
        return provider;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) {
                System.out.println("loadUserByUsername: " + username);
                return usuarioDao.findBy("username", username).get(0);
            }
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                try {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    md.update(rawPassword.toString().getBytes());
                    byte[] digest = md.digest();

                    StringBuilder sb = new StringBuilder();
                    for(int i=0; i<digest.length; i++) sb.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1));
                    return sb.toString();
                } catch (Exception e) {
                    return null;
                }
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(encode(rawPassword));
            }
        };
    }

    @RequestMapping(value = "/")
    public String index(Model model) {
        return "index";
    }

    @RequestMapping(value = "/login")
    public String login(Model model) {
        return "login";
    }

    @RequestMapping(value = "/register", method=RequestMethod.GET)
    public String register(Model model) {
        model.addAttribute("obj", new Usuario());
        return "register";
    }

    @RequestMapping(value = "/register", method=RequestMethod.POST)
    public String register(@ModelAttribute Usuario usuario) {
        try {
            usuario.setPassword(passwordEncoder().encode(usuario.getPassword()));
            usuario.setCredenciais(new ArrayList<Credencial>());
            usuario.getCredenciais().add(credencialDao.findBy("nome", "USER").get(0));
            usuarioDao.insert(usuario);
            return "login";
        } catch (Exception e) {
            e.printStackTrace();
            return "register";
        }
    }
}
Kleber Mota
  • 8,521
  • 31
  • 94
  • 188
  • Please, could you indicate in your answer the error you are facing? In addition, could you include in your code the `UserDAO` implementation? It looks strange to me the way in which you implemented the method `findBy`. Are you using Spring Data JPA? Did you try `findByUsername(String username)`? – jccampanero Jul 21 '22 at 16:36
  • @jccampanero I am unable to login with the application. I can register a new user, and this user is stored in the database, but if try login with this user (or with a user imported via `import.sql` file), the application always return to the login page without proceed with the login. the full code for this project can be found here: https://github.com/klebermo/minimal_spring_boot – Kleber Mota Jul 23 '22 at 01:11
  • 1
    @Thank you very much for creating a reproducible example of your problem. I posted an answer based on it. I hope it helps. – jccampanero Jul 23 '22 at 15:11

2 Answers2

3

I think there could be several issues with your actual code.

First, due to the fact you are using form login, please, try providing the appropriate configuration when defining your filter chain, for example:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .cors().disable()
        .authorizeHttpRequests()
            .antMatchers("/", "/login", "/logout", "/register", "/error", "/css/**", "/js/**", "/img/**").permitAll()
            .anyRequest().authenticated()
            .and()
                .formLogin()
                    .loginPage("/login")
                     // Note the inclusion of the login processing url value
                    .loginProcessingUrl("/authenticate")
                     // One that you consider appropriate
                    .defaultSuccessUrl("/home")
                    .failureUrl("/login?error=true")
            .and().authenticationProvider(authProvider());
    return http.build();
}

As you can see, we are indicating that the login page will be handled by your controller /login mapping:

@RequestMapping(value = "/login")
public String login(Model model) {
    return "login";
}

In addition, we indicated /authenticate as the login processing url configuration: this will activate all the authentication stuff provided by you and Spring Security to authenticate your users.

Note that you need to change your login.html page as well, because in your current implementation the username/password form is being submitted as well against /login - this is probably the cause of the problem you described in your update #2. Following the example, the form should be submitted against /authenticate:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
    <linl rel="stylesheet" th:href="@{/css/style.css}"></linl>
</head>
<body>
    <form method="post" th:action="@{/authenticate}">
        <input type="text" name="username" placeholder="Usuário">
        <input type="password" name="password" placeholder="Senha">
        <input type="submit" value="Entrar">
    </form>
    
    <script th:src="@{/js/script.js}"></script>
</body>
</html>

Note that we included as well a route, /home to request your app after a successful authentication. Yu can define in App something like:

@RequestMapping(value = "/home")
public String home(Model model) {
    return "home";
}

And a simple HTML test page, home.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
    Authenticated!!

    <script th:src="@{/js/script.js}"></script>
</body>
</html>

In order to make this work you should change an additional piece in your software. According to the way you defined your code, when Spring Security tries reading your Usuario GrantedAuthorities Hibernate will issue the well known failed to lazily initialize a collection of role... because in your current implementation you are reading the credentials stored in your database but there is no session:

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    List<Autorizacao> lista = new ArrayList<Autorizacao>();
    for(Credencial credencial : credenciais) {
        lista.addAll(credencial.getAutorizacoes());
    }
    return lista;
}

You can probably solve the issue in different ways, especially consider using @Transactional, but one straight forward solution could be the folllowing.

First, modify your Usuario object and include a transient property for storing the Spring Security granted credentials:

package org.kleber.app.model.usuario;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Transient;

import org.kleber.app.model.credencial.Credencial;
import org.kleber.app.model.Model;
import org.kleber.app.model.autorizacao.Autorizacao;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
public class Usuario extends Model implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;

    @Column
    String username;

    @Column
    String password;

    @Column
    String firstName;

    @Column
    String lastName;

    @Column
    String email;

    @ManyToMany
    List<Credencial> credenciais;

    @Transient
    Collection<? extends GrantedAuthority> authorities = Collections.emptySet();

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public List<Credencial> getCredenciais() {
        return credenciais;
    }

    public void setCredenciais(List<Credencial> credenciais) {
        this.credenciais = credenciais;
    }

    public boolean isEnabled() {
        return true;
    }

    public boolean isCredentialsNonExpired() {
        return true;
    }

    public boolean isAccountNonExpired() {
        return true;
    }

    public boolean isAccountNonLocked() {
        return true;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

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

Next, include a custom method in UsuarioDao in order to obtain the credentials when you have an active Session:

package org.kleber.app.model.usuario;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;

import org.kleber.app.model.Dao;
import org.kleber.app.model.autorizacao.Autorizacao;
import org.kleber.app.model.credencial.Credencial;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class UsuarioDao extends Dao<Usuario> {
    UsuarioDao() {
        super(Usuario.class);
    }

    public Usuario findByUsername(String username) {
        EntityManager entityManager = getEntityManager();
        entityManager.getTransaction().begin();
        Usuario usuario = (Usuario) entityManager.createQuery("SELECT a FROM Usuario a WHERE a.username = :value").setParameter("value", username).getSingleResult();
        // Retrieve the credentials here
        // On the contrary, you will face: failed to lazily initialize a collection of role...
        // Please consider using @Transactional instead
        List<Credencial> credenciais = usuario.getCredenciais();
        List<Autorizacao> autorizacaos = new ArrayList<Autorizacao>();
        for(Credencial credencial : credenciais) {
            autorizacaos.addAll(credencial.getAutorizacoes());
        }
        usuario.setAuthorities(autorizacaos);
        entityManager.getTransaction().commit();
        entityManager.close();
        return usuario;
    }
}

Please, as I mentioned, consider use @Transactional and Spring built-in transaction demarcation mechanisms instead of handling your transactions on your own in this way.

Finally, user this new method in your UserDetailsService implementation:

@Bean
public UserDetailsService userDetailsService() {
    return new UserDetailsService() {
        @Override
        public UserDetails loadUserByUsername(String username) {
            System.out.println("loadUserByUsername: " + username);
            return usuarioDao.findByUsername(username);
        }
    };
}

Instead of creating this new method in UsuarioDao another, perhaps better, possibility would be creating a UserService @Service that wrap this Usuario and Credentials initialization process: this service would be then the one used by your UserDetailsService implementation.

For completeness, this is how the App class would end looking like:

package org.kleber.app;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

import org.springframework.context.annotation.Bean;
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;

import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.security.MessageDigest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.ui.Model;

import java.util.ArrayList;

import org.springframework.beans.factory.annotation.Autowired;
import org.kleber.app.model.usuario.UsuarioDao;
import org.kleber.app.model.credencial.CredencialDao;
import org.kleber.app.model.usuario.Usuario;
import org.kleber.app.model.credencial.Credencial;

@SpringBootApplication
@Controller
public class App extends SpringBootServletInitializer {
  @Autowired
  UsuarioDao usuarioDao;

  @Autowired
  CredencialDao credencialDao;

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(App.class);
  }

  @Bean
  public SpringSecurityDialect springSecurityDialect() {
    return new SpringSecurityDialect();
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .cors().disable()
        .authorizeHttpRequests()
        .antMatchers("/", "/login", "/logout", "/register", "/error", "/css/**", "/js/**", "/img/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .loginPage("/login")
        .loginProcessingUrl("/authenticate")
        .defaultSuccessUrl("/home")
        .failureUrl("/login?error=true")
        .and().authenticationProvider(authProvider());
    return http.build();
  }

  @Bean
  public DaoAuthenticationProvider authProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setPasswordEncoder(passwordEncoder());
    provider.setUserDetailsService(userDetailsService());
    return provider;
  }

  @Bean
  public UserDetailsService userDetailsService() {
    return new UserDetailsService() {
      @Override
      public UserDetails loadUserByUsername(String username) {
        System.out.println("loadUserByUsername: " + username);
        return usuarioDao.findByUsername(username);
      }
    };
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new PasswordEncoder() {
      @Override
      public String encode(CharSequence rawPassword) {
        try {
          MessageDigest md = MessageDigest.getInstance("MD5");
          md.update(rawPassword.toString().getBytes());
          byte[] digest = md.digest();

          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < digest.length; i++)
            sb.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1));
          return sb.toString();
        } catch (Exception e) {
          return null;
        }
      }

      @Override
      public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(encode(rawPassword));
      }
    };
  }

  @RequestMapping(value = "/")
  public String index(Model model) {
    return "index";
  }

  @RequestMapping(value = "/home")
  public String home(Model model) {
    return "home";
  }

  @RequestMapping(value = "/login")
  public String login(Model model) {
    return "login";
  }

  @RequestMapping(value = "/register", method = RequestMethod.GET)
  public String register(Model model) {
    model.addAttribute("obj", new Usuario());
    return "register";
  }

  @RequestMapping(value = "/register", method = RequestMethod.POST)
  public String register(@ModelAttribute Usuario usuario) {
    try {
      usuario.setPassword(passwordEncoder().encode(usuario.getPassword()));
      usuario.setCredenciais(new ArrayList<Credencial>());
      usuario.getCredenciais().add(credencialDao.findBy("nome", "USER").get(0));
      usuarioDao.insert(usuario);
      return "login";
    } catch (Exception e) {
      e.printStackTrace();
      return "register";
    }
  }
}

Probably it could be improved in different ways, but the suggested setup should allow you to successfully access your app:

welcome page

jccampanero
  • 50,989
  • 3
  • 20
  • 49
1

You can handle UserDetailsService and PasswordEncoder in a SecurityConfig class which allow us set their relationship on an authentication provider.

First

Create your own custom UserDetailsService by implementing that interface.

@Service
public class CustomUserDetailService implements UserDetailsService {

    @Autowired 
    private UserService service;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new CustomUserDetail(service.getByEmail(username));
    }
}

Second

Create a SecurityConfig class to manage the CustomUserDetailsService and configure the AuthenticationProvider to set the passwordEncoder and userDetailsService.

@Configuration
public class SecurityConfig {

    @Autowired
    private CustomUserDetailService userDetailService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable().cors().disable()
                .authorizeRequests()
                .antMatchers("/api/v1/entity", "/api/v1/auth")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .authenticationProvider(authenticationProvider())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailService);
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Jonathan JOhx
  • 5,784
  • 2
  • 17
  • 33
  • this class `WebSecurityConfigurerAdapter` is mark as deprecated when I try use it. Do you know what would be the new way to do that? – Kleber Mota Jul 02 '22 at 12:11
  • I see, okay I just updated my answer. Let me know if works, because on my local env works perfectly. – Jonathan JOhx Jul 02 '22 at 15:14
  • That solved an issue I posted here: https://stackoverflow.com/questions/72788862/secauthorize-not-working-in-spring-boot-application, but I am still unable login on the application (with any of the users inserted in the database). It does not appear the program is even trying acessing my custom userDetailsService Bean when I try to login. My current code is this: https://pastebin.com/3e551C1t – Kleber Mota Jul 02 '22 at 15:39
  • Can you share your login API? `/login` is returning login.html page but where does the request go when you actually login. – Pradyskumar Jul 07 '22 at 17:09
  • hey @KleberMota ping me my email is on my profile, I think you have a different issue, let me help you. – Jonathan JOhx Jul 20 '22 at 14:55
  • @JonathanJOhx can you give me a hint of what could be the problem? – Kleber Mota Jul 20 '22 at 15:53
  • I need to see your code, or better you can add the `stacktrace` when you can't log in, if not, contact me by sending a mail. – Jonathan JOhx Jul 20 '22 at 16:14
  • The full code for this project can be found here: https://github.com/klebermo/minimal_spring_boot – Kleber Mota Jul 23 '22 at 01:12