Spring Security
Anem a integrar Spring Security en una aplicació Spring Boot amb H2 com a base de dades.
Farem dues implementacions:
- Sense DTO (la forma més simple).
- Amb DTO (més estructurat i segur).
Què és Spring Security i per què usar-lo?
Quan desenvolupem una aplicació web, una de les principals preocupacions és la seguretat. No volem que qualsevol persona puga accedir a dades privades, manipular informació o realitzar accions sense permís. Ací és on Spring Security entra en joc.
Spring Security és un mòdul de seguretat per a aplicacions Spring Boot que ens ajuda a protegir la nostra aplicació de manera senzilla i flexible. Amb ell podem:
- Restringir l’accés a determinades pàgines o funcionalitats.
- Gestionar autenticació i permisos d’usuaris (qui pot accedir i què pot fer).
- Protegir l’aplicació contra atacs comuns com Cross-Site Request Forgery (CSRF) o injeccions de codi.
Configuració bàsica de Spring Security
Afegir la dependència de Spring Security
Afegim la següent línia al nostre build.gradle
:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Aquesta dependència afegeix Spring Security a l’aplicació. Quan iniciem l’aplicació, Spring Security s’encarregarà de la gestió d’usuaris i autenticació. La dependència validation és per a les anotacions de validació.
Provar la configuració per defecte
Si accedim a http://localhost:8080/login
, Spring Security ens mostrarà automàticament una pàgina d’autenticació.
🔹 Usuari per defecte: user
🔹 Contrasenya: Apareixerà a la consola en iniciar l’aplicació, en una línia com aquesta:
Using generated security password: 3d1a2b3c4d5e
Definir un usuari personalitzat en application.properties
Podem establir un usuari personalitzat modificant src/main/resources/application.properties
:
spring.security.user.name=jaume
spring.security.user.password=1234
spring.security.user.roles=USER
Amb això, podrem accedir a http://localhost:8080/login
amb:
- Usuari:
jaume
- Contrasenya:
1234
Creació d’Usuaris en Spring Security amb JDBC Authentication
Ara implementarem un sistema complet de gestió d’usuaris utilitzant Spring Security 6.0 i JDBC Authentication, usant la base de dades H2.
Creació de la base de dades per a gestió d’usuaris
Spring Security requereix dues taules per a gestionar els usuaris. Si no estan creades, cal executar aquest SQL:
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(500) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);
Aquestes taules emmagatzemen els usuaris i els seus rols.
Creació de les entitats User i Authority
Aquestes entitats són les que s’encarregaran de mapejar les dades a la base de dades H2.
Classe User
package com.jaume.Ciutats.model.Entitats;
import jakarta.persistence.*;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
private String username;
private String password;
private boolean enabled;
@OneToMany(mappedBy = "username")
private Set<Authority> authorities;
// Getters i setters
}
Classe Authority
package com.jaume.Ciutats.model.Entitats;
import jakarta.persistence.*;
@Entity
@Table(name = "authorities")
public class Authority {
@Id
private String username;
private String authority;
// Getters i setters
}
Aquestes classes mapegen les taules users
i authorities
que s’han creat a la base de dades.
Implementació de SecurityConfig
Ara crearem la classe SecurityConfig
per a configurar Spring Security.
package com.jaume.Ciutats.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // Desactiva CSRF per a evitar bloquejos en Postman
.authorizeHttpRequests(auth -> auth
.requestMatchers("/users/register/").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.httpBasic(withDefaults())
.build();
}
}
Creació del UserController
per a registre d’usuaris
Aquest controlador permet registrar nous usuaris.
package com.jaume.Ciutats.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserDetailsManager userDetailsManager;
@PostMapping("/register/")
public String register(@RequestParam String username, @RequestParam String password) {
if (userDetailsManager.userExists(username)) {
return "ERROR: l'usuari ja existeix";
}
userDetailsManager.createUser(User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.roles("USER")
.build());
return "OK";
}
}
Provar el registre d’usuaris
Hem de crear un usuari per a poder iniciar sessió: Jaume/1234.
INSERT INTO users (username, password, enabled)
VALUES ('jaume', '$2a$10$E0NCN2K5VbX0JPUOJ91QnupFIRFkOrzjs11yThPboWv/CcGMfZl6W', TRUE);
INSERT INTO authorities (username, authority)
VALUES ('jaume', 'ROLE_USER');
La encriptació de la contrasenya es fa amb BCryptPasswordEncoder
, per a usar
Es pot registrar un nou usuari amb una petició POST a http://localhost:8080/users/register/
, enviant els paràmetres directament en la URL.
POST http://localhost:8080/users/register/?username=jaume&password=1234
Després d’aquesta petició:
- L’usuari “jaume” estarà registrat a la base de dades.
- Podrà iniciar sessió a
http://localhost:8080/login
.
Implementació amb DTO: Per què i com fer-ho
Quan treballem amb APIs en Spring Boot, és recomanable utilitzar DTOs (Data Transfer Objects) per a gestionar les dades que s’envien i es reben en les peticions HTTP. En aquest cas, farem servir un DTO per al registre d’usuaris, encapsulant la informació de l’usuari en una classe específica en lloc de passar els paràmetres directament en la petició.
- Utilitzar un DTO en el registre d’usuaris:
- Millora l’organització del codi: El DTO separa la lògica de validació de les dades de la lògica del controlador.
- Evita problemes de seguretat: Es poden controlar millor quins camps es permeten modificar o llegir.
- Facilita la validació de dades: Podem utilitzar anotacions com
@NotBlank
per assegurar que els camps requerits no siguen buits. - Escalabilitat: Si en el futur necessitem afegir més camps al registre d’usuaris, només cal modificar el DTO sense afectar altres parts del codi.
caldra importar
1. Creació del DTO UserRegisterRequest
Creem un nou paquet per a DTOs en la nostra aplicació i afegim la classe UserRegisterRequest
.
📁 Estructura del projecte:
src/main/java/com/jaume/Ciutats/
├── controller/
│ ├── UserController.java
├── model/
│ ├── entitats/
│ │ ├── User.java
│ │ ├── Authority.java
│ ├── dto/
│ │ ├── UserRegisterRequest.java
├── config/
│ ├── SecurityConfig.java
Fitxer: src/main/java/com/jaume/Ciutats/model/dto/UserRegisterRequest.java
package com.jaume.Ciutats.model.dto;
import jakarta.validation.constraints.NotBlank;
public class UserRegisterRequest {
@NotBlank(message = "El nom d'usuari és obligatori")
private String username;
@NotBlank(message = "La contrasenya és obligatòria")
private String password;
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;
}
}
@NotBlank
evita que el nom d’usuari i la contrasenya siguen buits o només espais.- Es proporciona getters i setters per a accedir a les propietats de l’usuari.
2. Modificació del UserController
per a utilitzar el DTO
package com.jaume.Ciutats.controller;
import com.jaume.Ciutats.model.dto.UserRegisterRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserDetailsManager userDetailsManager;
@PostMapping("/register/")
public String register(@Valid @RequestBody UserRegisterRequest userRegisterRequest) {
if (userDetailsManager.userExists(userRegisterRequest.getUsername())) {
return "ERROR: l'usuari ja existeix";
}
userDetailsManager.createUser(User.builder()
.username(userRegisterRequest.getUsername())
.password(passwordEncoder.encode(userRegisterRequest.getPassword()))
.roles("USER")
.build()
);
return "OK";
}
}
- El
@RequestBody
permet que Spring mapegue el JSON de la petició a un objecteUserRegisterRequest
. @Valid
assegura que les validacions definides en el DTO es comproven abans d’executar la lògica del registre.- Ara el controlador ja no rep
username
ipassword
com a paràmetres directes, sinó que els obté del DTO.