Questo è un articolo divulgativo e non ha la pretesa di essere esaustivo. E’ parte di un più ampio insieme di articoli che trattano la programmazione di Web Application con Spring. E’ un articolo di appoggio per le lezioni che tengo fuori da web. Buona lettura
Oggi impariamo a mettere in sicurezza un’applicazione Spring. Avremo bisogno di:
- Una nuova dipendenza
- Un insieme di Entity che rappresentino l’utente e i suoi ruoli
- Una classe di configurazione di tipo WebSecurityConfigurerAdapter
- Nuove route per gestire il login nel Controller
Iniziamo dalla dipendenza. Aggiungiamo nel file pom.xml la seguente dipendenza:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
Quindi creiamo una nuova classe all’interno del nostro progetto di tipo WebSecurityConfigurerAdapter.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
}
La classe sopra esposta presenta l’annotation @EnableWebSecurity che, come si può immaginare, comunica a Spring di inizializzare il contesto di sicurezza. Quindi viene definito un metodo configure, che appunto configura una serie di route all’interno del sistema permettendo a tutti di arrivare sulle route / e /login, mentre richiede l’autenticazione per ogni altra route.
Passiamo alle Entity e creiamo due nuove Entity così fatte:
@Entity @Getter @Setter public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String username; private String password; @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER) private List<Role> roles; public User() { roles = new ArrayList<>(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles .stream() .map(r->new SimpleGrantedAuthority(r.getName())) .collect(Collectors.toList()); } public void addRole(String role) { roles.add(new Role(role)); } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public boolean equals(Object o) { if (o instanceof User) { User other = (User) o; return other.id == this.id; } return false; } @Override public int hashCode() { return (int) id; } }
E la classe Role:
@Entity @NoArgsConstructor @Getter @Setter public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column private String name; public Role(String name) { this.name = name; } }
Proseguiamo col definire una route GET per il login:
@GetMapping("/login")
public String login() {
return "login";
}
Ed un apposito template:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
Infine il vero Deus ex machina, la classe UserService, che implementa un UserDetailsService, che è richiesto dall’autenticazione per poter caricare un utente da un database:
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepositoryInterface userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent()) return user.get();
else throw new UsernameNotFoundException("Credentials not valid");
}
}
Tutto questo è sufficiente per poter autenticare i vostri utenti attraverso un database gestito da Hibernate, sfruttando la dipendenza Security introdotta all’inizio.
Spring sfrutta una serie di convenzioni e di classi predefinite per raggiungere il suo obiettivo, semplificandoci moltissimo il lavoro.
Con questo è tutto. Mi si perdoni la sintesi ma spero che questo breve articolo possa essere d’aiuto come promemoria.