Crear el proyecto Spring Boot
Configurar el proyecto
Crea un proyecto Spring Boot desde Spring Initializr seleccionando los iniciadores (starters) de Spring Web, Validation, JDBC API, PostgreSQL Driver, Spring Security, OAuth2 Resource Server y Testcontainers.
Alternativamente, puedes clonar el repositorio de la guía.
Después de generar la aplicación, añade el módulo comunitario testcontainers-keycloak y REST Assured como dependencias de prueba.
Las dependencias clave en pom.xml son:
<properties>
<java.version>17</java.version>
<testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Crear el modelo de dominio
Crea un record Product que represente el objeto de dominio:
package com.testcontainers.products.domain;
import jakarta.validation.constraints.NotEmpty;
public record Product(Long id, @NotEmpty String title, String description) {}Crear el repositorio
Implementa ProductRepository utilizando JdbcClient de Spring para interactuar con una base de datos PostgreSQL:
package com.testcontainers.products.domain;
import java.util.List;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
@Repository
public class ProductRepository {
private final JdbcClient jdbcClient;
public ProductRepository(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
public List<Product> getAll() {
return jdbcClient.sql("SELECT * FROM products").query(Product.class).list();
}
public Product create(Product product) {
String sql =
"INSERT INTO products(title, description) VALUES (:title,:description) RETURNING id";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient
.sql(sql)
.param("title", product.title())
.param("description", product.description())
.update(keyHolder);
Long id = keyHolder.getKeyAs(Long.class);
return new Product(id, product.title(), product.description());
}
}Añadir un script de creación de esquema
Crea src/main/resources/schema.sql para inicializar la tabla products:
CREATE TABLE products (
id bigserial primary key,
title varchar not null,
description text
);Habilita la inicialización del esquema en src/main/resources/application.properties:
spring.sql.init.mode=alwaysPara aplicaciones en producción, utiliza en su lugar una herramienta de migración de base de datos como Flyway o Liquibase.
Implementar los endpoints de la API
Crea ProductController con los endpoints para obtener todos los productos y crear un producto:
package com.testcontainers.products.api;
import com.testcontainers.products.domain.Product;
import com.testcontainers.products.domain.ProductRepository;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductRepository productRepository;
ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping
List<Product> getAll() {
return productRepository.getAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Product createProduct(@RequestBody @Valid Product product) {
return productRepository.create(product);
}
}Configurar la seguridad OAuth 2.0
Crea una clase SecurityConfig que proteja los endpoints de la API mediante autenticación basada en tokens JWT:
package com.testcontainers.products.config;
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(c ->
c
.requestMatchers(HttpMethod.GET, "/api/products")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/products")
.authenticated()
.anyRequest()
.authenticated()
)
.sessionManagement(c ->
c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(CorsConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
return http.build();
}
}Esta configuración:
- Permite el acceso no autenticado a
GET /api/products. - Requiere autenticación para
POST /api/productsy todos los demás endpoints. - Configura el servidor de recursos (Resource Server) OAuth 2.0 con autenticación basada en tokens JWT.
- Deshabilita CORS y CSRF debido a que esta es una API sin estado (stateless).
Añade la URI del emisor de JWT a application.properties:
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/keycloaktcdemoExportar la configuración del reino (realm) de Keycloak
Antes de escribir las pruebas, exporta una configuración del reino (realm) de Keycloak para que el entorno de prueba pueda importarla automáticamente. Inicia una instancia temporal de Keycloak:
$ docker run -p 9090:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:25 start-dev
Abre http://localhost:9090 e inicia sesión en la Consola de Administración con admin/admin. Luego, configura el reino (realm):
- En la esquina superior izquierda, selecciona el menú desplegable de reinos y crea un reino llamado
keycloaktcdemo. - Bajo el reino
keycloaktcdemo, crea un cliente con la siguiente configuración:- Client ID:
product-service - Client Authentication: On
- Authentication flow: selecciona únicamente Service accounts roles
- Client ID:
- En la pantalla Client details, ve a la pestaña Credentials y copia el valor de Client secret.
Exporta la configuración del reino (realm):
$ docker ps
# copia el ID del contenedor de keycloak
$ docker exec -it <container-id> /bin/bash
$ /opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --realm keycloaktcdemo
$ exit
$ docker cp <container-id>:/opt/keycloak/data/import/keycloaktcdemo-realm.json keycloaktcdemo-realm.json
Copia el archivo keycloaktcdemo-realm.json exportado en src/test/resources.