PostgreSQL con un poco de SpringBoot

Otro post más, se verá algo de bases de datos, springboot, java, xml, h2, flyway para la migración de h2 a postgreSQL, securizar un poco en postgreSQL y listo, más de lo mismo.

Migrando Script de h2 a PostgreSQL

Migracion
  1. Paso 1 Habilitando flyway desde el pom.xml

  2. Paso 2 formateo del fichero para el versionado correcto de flyway

Propiedades para el h2

### spring config
spring:
  datasource:
    url: jdbc:h2:mem:db (1)
    username: test
    password: test
1 db: es el nombre de nuestra bases de datos.

Depedencias para nuestro pom.xml

Ya estoy esta con un parent de spring y por lo tanto la version la hereda, por eso no esta el tag <version>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.6</version>
</parent>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
En esta versión de springboot 3.3.6 debe estar habilitado en nuestra seguridad la consola de h2, para poder acceder a ella al menos en desarrollo.
@Bean
public WebSecurityCustomizer ignoringCustomizer() {
    return (web) -> web.ignoring().requestMatchers("/h2-console/**");
}

Desde la UI cuando nos conectamos podemos hacer esto

Tips
  1. Claramente antes de acceder a la UI, debemos tener todas nuestras entidades creadas en java para el h2, y con compatibilidad tal cual para que puedan crearse ahi.

  2. Luego al acceder, podremos visualizar todas las tablas etc.

SCRIPT TO 'dump.sql' (1)
1 Esto creará un fichero llamado dump.sql dentro el directorio del proyecto, no hace falta copiar y pegar 😚

h2 dump sql

Renombrado
  1. Renombrarlo a V1__init.sql

  2. Así lo reconocerá Flyway en el src/main/resources/db/migration

convension migration

El Docker compose

Necesitamos tener postgresql levantado al menos en local, o usar testcontainer.

services:
  postgres:
    image: 'postgres:latest'
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432:5432'

Reconfigurando properties de Springboot

Flyway debe encargarse de la estructura de la base de datos, por lo que es importante desactivar la auto-creación de Hibernate.

### spring config
spring:
  jpa:
    defer-datasource-initialization: true
    show-sql: true
    generate-ddl: false (1)
    hibernate
        ddl-auto: none (2)
    properties:
      hibernate:
        format_sql: true
  #Config Flyway
  flyway:
    enabled: true
    baseline-on-migrate: true
    out-of-order: true
  datasource:
    url: jdbc:postgresql://localhost:5432/mydatabase
    username: myuser
    password: secret
    driverClassName: org.postgresql.Driver (3)
1 desactivado.
2 desactivado.
3 Esta propiedad se supone que ya es opcional.

Esto evita que Hibernate intente modificar la base de datos antes de que Flyway haga su trabajo.

Actualizamos el pom.xml

Con estas dependencias del driver de postgresql y flyway

<!--<dependency>
    <groupId>com.h2database</groupId> (1)
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency> -->

<!-- https://mvnrepository.com/artifact/org.flywaydb/flyway-core -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>11.3.0</version> (2)
</dependency>

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-postgresql</artifactId> (3)
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
1 Dependencia de h2 comentada.
2 Importante tenerla para evitar errores tipo Caused by: org.flywaydb.core.api.FlywayException: Unsupported Database: PostgreSQL 17.2
3 Importante también.

Posible error en el arranque, con jpa y flyway

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Failed to initialize dependency 'flyway' of LoadTimeWeaverAware bean 'entityManagerFactory': Error creating bean with name 'flyway' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Circular depends-on relationship between 'flyway' and 'entityManagerFactory'

Lo más probable es tengamos pospuesta la inicialización del Datasource en nuestro fichero de propiedades, debemos tenerlo así, con false:

  • defer-datasource-initialization: false

Caso con db y datos existentes

La configuración cuando tenemos datos existentes en la db, y no queremos tocar nada de nuestros datos, debemos cambiar las propiedades por ejemplo:

flyway:
  schema: nuestro-schema
  baseline-on-migrate: true
  baseline-version: 0
  clean-disabled: true
  validate-on-migrate: false

Además crear un fichero, en db.migration por ejemplo: V1.1__baseline.sql con esto:

-- Baseline migration - estructura existente

Cuando arranquemos flyway el va a versionar este script, en el schema publico de nuestra db.

SELECT * FROM flyway_schema_history;
installed_rank|version|description|type|script            |checksum  |installed_by|installed_on           |execution_time|success|
--------------+-------+-----------+----+------------------+----------+------------+-----------------------+--------------+-------+
             1|1.1    |baseline   |SQL |V1.1__baseline.sql|-385248959|admin       |fecha                  |             6|true   |

Para un futuro podemos crear nuevas migraciones siguiendo la convención de nombres de Flyway:

V1.2__descripcion.sql
V1.3__descripcion.sql
Pendiente con esto:
  • Siempre un backup antes.

  • Prueba las migraciones en un entorno de desarrollo/dockerizado primero

  • ALTER TABLE en lugar de DROP/CREATE

  • Documentemos los cambios al menos, no deberiamos ser tan vagos.

Algunas configuraciones para el IDE

Si actualizamos el fichero .bashrc del usuario con variables de entorno, debemos reiniciar el IDE para que las variables se refresquen correctamente, sino apuntaran a versiones incorrectas.

El profile de spring

Si tenemos varios profile debemos estar atentos a cargar el profile correcto, local y/o algun otro.

  • Para local desde el IDE, podemos usar --spring.profiles.active=local aquí:

images spring profile local

Por lo tanto en la consola debemos ver lo siguient:

2025-02-10T20:30:14.347+01:00  INFO 1228306 --- [  restartedMain] c.x.x.Application   : The following 1 profile is active: "local" (1)
1 El profile de local, application-local.yml cargado correctamente.

Cargando correctamente el .bashrc del IDE

Es importante para asegurar nos que nuestro ide carge el .bashrc correctamente donde tenemos las variables de entorno establecidas como en la imagen, es bastante util.

/bin/bash --login

load bashrc from intellIIdea

/home/rubn/.local/share/applications/IntelliJ.desktop
#!/usr/bin/env xdg-open
[Desktop Entry]
Type=Application
Name=IntelliJ
StartupWMClass=jetbrains-idea-ce
Exec=/bin/bash -i -c "/home/rubn/Downloads/jdk-ide/idea/bin/idea" %f (1)
Icon=/home/rubn/Downloads/jdk-ide/idea/bin/idea.svg
Categories=Network;Application;
1 Aquí forzamos la carga de modo interactivo con el parametro -i

Cada cambio en dichero fichero le toca un reinicio con source .bashrc o el fichero donde tengamos dichas propiedades.

Instalando PostgreSQL FreeBSD

Postgresql elephant

La wiki en nuestra amiga PostgreSQL/Setup

Un poco de postgres con springboot para securizar

La seguridad suele ser importante siempre se usar para la prueba la version de postgres 15, para ellos habilitamos rapidamente un jail con bastille usando la template por ejemplo esta

aplicandolo al jail existente llamado postgresql

bastille template TARGET yaazkal/bastille-postgres (1)
1 TARGET es el nombre de nuestro jail

Solo con acceso desde localhost

Si, tengo solo habilitado usar el psql desde el propio server con esta linea en el fichero pg_hba.conf

# "local" is for Unix domain socket connections only
local   all             all                                     trust (1)
1 las otra lineas por recomendación mejor que esten con scram-sha-256 por trust o MD5

Migrando password a SCRAM

Revisar esto

El fichero postgresql.conf toca actualizar la línea

# - Authentication -

#authentication_timeout = 1min          # 1s-600s
password_encryption = scram-sha-256     # scram-sha-256 or md5 (1)
1 Aqui descomentar y usar scram-sha-256
postgres=#
SELECT rolname, rolpassword ~ '^SCRAM-SHA-256\$' AS has_upgraded
FROM pg_authid
WHERE rolcanlogin;
 rolname  | has_upgraded
----------+--------------
 postgres |
 admin    | t (1)
(2 rows)
1 Indica true, que ya esta actualizada

Posiblemente para autenticarnos desde nuestro cliente java con spring tengas ciertos problemas como

Caused by: org.postgresql.util.PSQLException: FATAL: password authentication failed for user "admin"

Actualización recomentada

$ psql
psql (14.15)
Type "help" for help.

postgres=# \password admin (1)
Enter new password for user "admin":
1 Usando \password se hace la actualizacion correcta de las password 🔥
ALTER ROLE admin WITH PASSWORD 'tu_contraseña'; (1)
1 Esto no es recomendado para la actualización, ademas de dejar rastro en el histórico.

Para reiniciar db

service postgresql reload

El comando \du

Nos permite ver los roles con sus atributos

postgres=# \du
                                   List of roles
 Role name |                         Attributes                         | Member of
-----------+------------------------------------------------------------+-----------
 admin     | Create DB                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Importando Schemas

Para hacerlo podemos usar el comando

psql -U \ (1)
admin -d \ (2)
ott -a -f /var/db/postgres/ott-schema.sql (3)
1 PostgreSQL en modo interactivo, con el parametro de -U de usuario.
2 El comando -d para la database ott.
3 El comando -a imprime todas la lineas, y -f es la ruta donde el usuario postgres tiene permisos de lectura, necesario para import dicho .sql.

Encoding para la password

Si usamos por defecto el bean JdbcUserDetailsManager por el InMemoryUserDetailsManager

@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
    return new JdbcUserDetailsManager(dataSource);
}

Debemos añadir un encoder por defecto al menos

java.lang.IllegalArgumentException: Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.

En el hash por defecto de spring security es un SHA-256

username|password                                                                        |enabled|
--------+--------------------------------------------------------------------------------+-------+
boby    |843b103f6c09de407d366f8ff6553691767a35dbaf870cad47e86945c4103be85ee8ea49509b9502|true   |
chispa    |a94f80ed1a6677dedb489a6912e6c83a7c2a7ada2c4d739dd4a4ab9e454d3d875134fa4480b19c55|true   |

Actualizamos añadiendo un template del hash

UPDATE users SET password = '{sha256}' || password;
username|password                                                                                |enabled|
--------+----------------------------------------------------------------------------------------+-------+
boby    |{sha256}843b103f6c09de407d366f8ff6553691767a35dbaf870cad47e86945c4103be85ee8ea49509b9502|true   |
chispa  |{sha256}a94f80ed1a6677dedb489a6912e6c83a7c2a7ada2c4d739dd4a4ab9e454d3d875134fa4480b19c55|true   |

Si aplicamos este otro bean y nos logueamos en el login de spring security el password encoder se cambiara a bcrypt bajo fondo.

@Bean
public UserDetailsPasswordService userDetailsPasswordService(UserDetailsManager userDetailsManager) {
    return (user, newPassword) -> {
        var updated = User.withUserDetails(user)
                .password(newPassword)
                .build();
        userDetailsManager.updateUser(updated);
        return updated;
    };
}

Migración bajo fondo a bcrypt

username|password                                                                                |enabled|
--------+----------------------------------------------------------------------------------------+-------+
boby    |{sha256}843b103f6c09de407d366f8ff6553691767a35dbaf870cad47e86945c4103be85ee8ea49509b9502|true   |
chispa  |{bcrypt}$2a$10$dMqSFw875ZLvpsADb24XDezNMacIYqC4bIUkbb24QOLYE3pmlZInS                    |true   |

Ya ese bcrypt nos dice que fue un update existoso

Secretos desde el vault con TLS

Como ya tenemos configurado nuestro vault con SSL/TLS podemos traernos los secretos usando certificado para autenticarnos.

De momento al intentar hacerlo desde spring tenemos lo siguiente, en nuestro fichero de propiedad

### spring config
spring:

### Vault
  config:
    import: vault://
  cloud:
    vault:
      uri: ${URL_VAULT} (1)
      token: ${INITIAL_ROOT_TOKEN} (2)
      kv:
        enabled: true
        backend: app
        application-name: nombre-app
      ssl:
        key-store: classpath:/cert/vault-cert.p12  # O file:/ruta/completa/mi-certificado.p12
        key-store-password: pass # La passphrase de nuestro certificado
        key-store-type: PKCS12
1 Url del vault, en https
2 El initial root token
org.springframework.vault.VaultException: Status 400 Bad Request [path/secreto]: Client sent an HTTP request to an HTTPS server. (1)
1 Intento de conexion de secretos via http normal, sin certificado, el server nos rechaza a la hora de traernos los secretos.

Como ya tenemos nuestro certificado de cliente .p12 debemos usarlo para autenticarnos por ejemplo con keytool para importar el ca:

keytool -import -trustcacerts -alias vault-ca -file ca-cert.crt -keystore vault-truststore.jks
...
...
Trust this certificate? [no]:  yes (1)
Certificate was added to keystore
1 Escribimos yes

Deberíamos tener generado el vault-truststore.jks entonces, ahora para probar la conexión

Ahora añadimos propiedades trust-store al src/main/resources/cert para prueba rápida.

FYI Sobre file
  1. file, classpath actúan distinto, file es para la ruta absoluta puede leer ficheros del sistema si la colocamos, el classpath no.

  2. Por lo visto es más flexible permite a la app hacer cambios sin recompilarlar.

  3. Los certificados o configuraciones pueden cambiar en producción

  4. Cuando los archivos son grandes y no quieres aumentar el tamaño del JAR

  5. Cuando necesitamos diferentes archivos para diferentes entornos (desarrollo, producción, etc.)

ssl:
  (1)
  key-store: file:src/main/resources/cert/vault-cert.p12  # O file:/ruta/completa/mi-certificado.p12
  key-store-password: pass # La passphrase de nuestro certificado
  key-store-type: PKCS12
  # Trust store para validar el certificado del servidor
  trust-store: file:src/main/resources/cert/vault-truststore.jks
  trust-store-password: pass (2)
  trust-store-type: JKS
1 Por la brevedad/seguridad usaremos file, también esta classpath cuando usaremos el .jar final de PROD.
2 La password introducida al ejecutar el comando keytool.

La config de spring para mapping de los secretos

Ya los secretos los tenemos disponibles en nuestro vault, ahora los podemos mappear a nuestro objeto Java, via @ConfigurationProperties

Explicitamente en el vault, tengo un secret seguido de un punto . para usarlo con el bean de spring y hacer mapping más facil.

mapping secret vault to java configuration properties

@Data
@Configuration
@ConfigurationProperties("secret") (1)
public class SecretsConfiguration {

    private String chispita;

}
1 Indicando que, en el vault estaría secret.chispita con el valor del secreto.