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 objecte UserRegisterRequest.
  • @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 i password com a paràmetres directes, sinó que els obté del DTO.

Table of contents