Skip to main content

Estratègia de Validació

Tipus: Document d'Arquitectura
Versió: 1.0
Actualitzat: 2024-12
Estat: Normatiu


Resum

Aquest document defineix l'estratègia de validació d'entrada per a LDP. Cobreix:

  1. On validar (capes)
  2. Què validar (tipus de validacions)
  3. Com validar (patrons i eines)
  4. Gestió d'errors de validació

1. Principi Fonamental

Mai Confiar en el Client

Tota entrada de l'usuari és potencialment maliciosa fins que es validi.

El frontend pot validar per UX, però el servidor ha de validar per seguretat.


2. Capes de Validació

┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (UX) │
│ - Feedback immediat a l'usuari │
│ - Redueix peticions innecessàries │
│ - NO és seguretat │
└─────────────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ CONTROLLER (Input) │
│ - Validació de format i tipus │
│ - @Valid + Jakarta Validation │
│ - Retorna 400 Bad Request si falla │
└─────────────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ SERVICE (Business) │
│ - Validació de regles de negoci │
│ - Consistència entre entitats │
│ - Permisos i estat │
└─────────────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ DATABASE (Integrity) │
│ - Última línia de defensa │
│ - Constraints, FKs, CHECKs │
│ - Veure: db-integrity.md │
└─────────────────────────────────────────────────────────────┘

3. Validació a Controller (Jakarta Validation)

3.1. DTOs de Request

Tots els DTOs de request han de tenir anotacions de validació:

public record CreateDanceRequest(
@NotBlank(message = "El nom és obligatori")
@Size(min = 2, max = 200, message = "El nom ha de tenir entre 2 i 200 caràcters")
String name,

@NotBlank(message = "El nivell és obligatori")
@Pattern(regexp = "BEGINNER|INTERMEDIATE|ADVANCED|EXPERT",
message = "Nivell invàlid")
String levelCode,

@Positive(message = "Els counts han de ser positius")
Integer counts,

@Min(value = 1, message = "Mínim 1 wall")
@Max(value = 4, message = "Màxim 4 walls")
Integer walls,

@Size(max = 5000, message = "Descripció massa llarga")
String description,

@Size(max = 10, message = "Màxim 10 coreògrafs")
List<@Positive Long> choreographerIds,

@Valid // Validar objectes niats
List<LinkRequest> links
) {}

3.2. Controlador amb @Valid

@PostMapping
@PreAuthorize("hasAnyRole('TEACHER', 'ADMIN')")
public DanceDto create(@Valid @RequestBody CreateDanceRequest request) {
// Si @Valid falla, Spring retorna 400 automàticament
return service.create(request);
}

3.3. Anotacions Disponibles

AnotacióÚsExemple
@NotNullNo pot ser nullCamps obligatoris
@NotBlankNo null, no buit, no només espaisStrings obligatoris
@NotEmptyNo null ni buitCol·leccions obligatòries
@SizeLongitud entre min i max@Size(min=2, max=100)
@Min / @MaxValor mínim/màximNombres
@Positive / @PositiveOrZero> 0 o >= 0IDs, quantitats
@EmailFormat emailCamps d'email
@PatternRegexCodis, formats especials
@Past / @FutureDates passades/futuresDates de naixement, events
@ValidValidar objecte niatDTOs compostos

3.4. Validacions Personalitzades

Per validacions complexes, crear anotació custom:

// Definició
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeHtmlValidator.class)
public @interface SafeHtml {
String message() default "Conté HTML no permès";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

// Implementació
public class SafeHtmlValidator implements ConstraintValidator<SafeHtml, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
// Usar biblioteca com OWASP Java HTML Sanitizer
return !containsDangerousHtml(value);
}
}

// Ús
public record UpdateBioRequest(
@SafeHtml
@Size(max = 2000)
String bio
) {}

4. Validació a Service (Regles de Negoci)

4.1. Validacions que NO es poden fer amb anotacions

@Service
public class ChoreographerService {

@Transactional
public ChoreographerDto create(ChoreographerDto dto) {
User user = authUtil.getCurrentUser();

// Regla de negoci: 1 user = 1 coreògraf SOLO
if (!dto.isGroup() && repo.existsByOwnerIdAndIsGroupFalse(user.getId())) {
throw new ClaimException(
"Ja ets propietari d'una fitxa de coreògraf",
ErrorCode.USER_ALREADY_OWNS_SOLO_CHOREOGRAPHER
);
}

// Regla: nom únic dins del país (si aplica)
if (repo.existsByNameAndCountry(dto.name(), dto.country())) {
throw new BadRequestException("Ja existeix un coreògraf amb aquest nom al país");
}

// Continuar amb creació...
}
}

4.2. Patró de Validació Centralitzada

Per validacions repetides, crear mètodes dedicats:

@Component
public class DanceValidator {

private final LevelRepository levelRepo;
private final ChoreographerRepository choreoRepo;

public void validateCreate(CreateDanceRequest request) {
validateLevelExists(request.levelCode());
validateChoreographersExist(request.choreographerIds());
validateOriginExclusivity(request.originLocationId(), request.originVenueId());
}

private void validateLevelExists(String code) {
if (!levelRepo.existsByCode(code)) {
throw new BadRequestException("Nivell no vàlid: " + code);
}
}

private void validateChoreographersExist(List<Long> ids) {
if (ids == null || ids.isEmpty()) return;

long found = choreoRepo.countByIdIn(ids);
if (found != ids.size()) {
throw new BadRequestException("Un o més coreògrafs no existeixen");
}
}

private void validateOriginExclusivity(Long locationId, Long venueId) {
if (locationId != null && venueId != null) {
throw new BadRequestException(
"L'origen pot ser location O venue, no ambdós"
);
}
}
}

5. Validació de Path/Query Parameters

5.1. IDs Positius

@GetMapping("/{id}")
public DanceDto get(
@PathVariable
@Positive(message = "L'ID ha de ser positiu")
Long id
) {
return service.get(id);
}

5.2. Paràmetres de Paginació

@GetMapping
public PageResponse<DanceDto> list(
@RequestParam(defaultValue = "0")
@Min(0)
int page,

@RequestParam(defaultValue = "20")
@Min(1)
@Max(100) // Límit dur
int size
) {
return service.search(PageRequest.of(page, size));
}

5.3. Enums

@GetMapping
public List<EventDto> listByType(
@RequestParam
@Pattern(regexp = "FESTIVAL|WORKSHOP|CLASS|SOCIAL", message = "Tipus invàlid")
String type
) {
return service.findByType(EventType.valueOf(type));
}

6. Sanitització

6.1. Quan Sanititzar vs Rebutjar

SituacióAcció
Input clarament maliciósRebutjar (400)
Espais extraTrim
HTML en text plaSanititzar o rebutjar
SQL injection attemptRebutjar + loguejar

6.2. Sanitització de Strings

public record UpdateNameRequest(
@NotBlank
@Size(max = 200)
String name
) {
// Compact constructor per sanititzar
public UpdateNameRequest {
if (name != null) {
name = name.trim();
// Eliminar caràcters de control
name = name.replaceAll("[\\p{Cntrl}]", "");
}
}
}

6.3. Protecció contra Mass Assignment

Mai fer bind directe de request a entitat:

// ❌ VULNERABLE: l'usuari podria enviar "role": "ADMIN"
@PutMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody User user) {
return repo.save(user);
}

// ✅ SEGUR: DTO explícit amb camps controlats
@PutMapping("/{id}")
public UserDto update(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) {
User entity = repo.findById(id).orElseThrow();
// Només actualitzar camps permesos
entity.setDisplayName(request.displayName());
entity.setBio(request.bio());
// NO: entity.setRole(request.role());
return mapper.toDto(repo.save(entity));
}

7. Gestió d'Errors de Validació

7.1. Resposta Estàndard

@RestControllerAdvice
public class ValidationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {

List<FieldError> errors = ex.getBindingResult().getFieldErrors()
.stream()
.map(err -> new FieldError(err.getField(), err.getDefaultMessage()))
.toList();

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ValidationErrorResponse("VALIDATION_ERROR", errors));
}
}

public record ValidationErrorResponse(
String code,
List<FieldError> errors
) {}

public record FieldError(
String field,
String message
) {}

7.2. Exemple de Resposta

{
"code": "VALIDATION_ERROR",
"errors": [
{ "field": "name", "message": "El nom és obligatori" },
{ "field": "counts", "message": "Els counts han de ser positius" }
]
}

8. Validació de Respostes Externes

8.1. APIs de Tercers

Quan consumim APIs externes (Spotify, etc.):

public SpotifyTrack fetchTrack(String trackId) {
ResponseEntity<SpotifyTrackResponse> response = restTemplate.getForEntity(
spotifyApiUrl + "/tracks/" + trackId,
SpotifyTrackResponse.class
);

if (!response.getStatusCode().is2xxSuccessful()) {
throw new ExternalServiceException("Spotify API error");
}

SpotifyTrackResponse body = response.getBody();

// Validar resposta abans d'usar
if (body == null || body.id() == null || body.name() == null) {
throw new ExternalServiceException("Invalid Spotify response");
}

// Sanititzar abans de guardar
return new SpotifyTrack(
sanitize(body.id()),
sanitize(body.name()),
sanitizeUrl(body.previewUrl())
);
}

9. Checklist de Validació

Per Request DTO

- [ ] Camps obligatoris tenen @NotNull/@NotBlank
- [ ] Strings tenen @Size amb màxim raonable
- [ ] Nombres tenen @Min/@Max si aplica
- [ ] IDs tenen @Positive
- [ ] Enums validats amb @Pattern o tipus enum
- [ ] Objectes niats tenen @Valid
- [ ] Col·leccions tenen @Size màxim

Per Controller

- [ ] @Valid als @RequestBody
- [ ] Path variables validades
- [ ] Query params amb defaults segurs
- [ ] Paginació amb límit dur

Per Service

- [ ] Regles de negoci validades
- [ ] Existència d'entitats referenciades
- [ ] Consistència entre camps
- [ ] Permisos i estat validats

10. Referències


Historial de Canvis

DataVersióCanvis
2024-121.0Document inicial