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";
}
}
}
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: