trabajando en el notify

This commit is contained in:
2025-11-03 23:32:31 +01:00
parent 725cff9b51
commit f528809c07
5 changed files with 171 additions and 139 deletions

View File

@ -90,7 +90,8 @@ public class SecurityConfig {
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers) // Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
.csrf(csrf -> csrf .csrf(csrf -> csrf
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"))) .ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"),
pathStartsWith("/pagos/redsys/")))
// ====== RequestCache: sólo navegaciones HTML reales ====== // ====== RequestCache: sólo navegaciones HTML reales ======
.requestCache(rc -> { .requestCache(rc -> {
HttpSessionRequestCache cache = new HttpSessionRequestCache(); HttpSessionRequestCache cache = new HttpSessionRequestCache();
@ -103,21 +104,26 @@ public class SecurityConfig {
// No AJAX // No AJAX
RequestMatcher nonAjax = new NegatedRequestMatcher( RequestMatcher nonAjax = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); new RequestHeaderRequestMatcher("X-Requested-With",
"XMLHttpRequest"));
// Excluir sondas .well-known // Excluir sondas .well-known
RequestMatcher notWellKnown = new NegatedRequestMatcher(pathStartsWith("/.well-known/")); RequestMatcher notWellKnown = new NegatedRequestMatcher(
pathStartsWith("/.well-known/"));
// Excluir estáticos: comunes + tu /assets/** // Excluir estáticos: comunes + tu /assets/**
RequestMatcher notStatic = new AndRequestMatcher( RequestMatcher notStatic = new AndRequestMatcher(
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()), new NegatedRequestMatcher(PathRequest.toStaticResources()
.atCommonLocations()),
new NegatedRequestMatcher(pathStartsWith("/assets/"))); new NegatedRequestMatcher(pathStartsWith("/assets/")));
RequestMatcher cartCount = new AndRequestMatcher( RequestMatcher cartCount = new AndRequestMatcher(
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()), new NegatedRequestMatcher(PathRequest.toStaticResources()
.atCommonLocations()),
new NegatedRequestMatcher(pathStartsWith("/cart/count"))); new NegatedRequestMatcher(pathStartsWith("/cart/count")));
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown, cartCount)); cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic,
notWellKnown, cartCount));
rc.requestCache(cache); rc.requestCache(cache);
}) })
// ======================================================== // ========================================================
@ -139,8 +145,10 @@ public class SecurityConfig {
"/error", "/error",
"/favicon.ico", "/favicon.ico",
"/.well-known/**", // opcional "/.well-known/**", // opcional
"/api/pdf/presupuesto/**" "/api/pdf/presupuesto/**",
).permitAll() "/pagos/redsys/**"
)
.permitAll()
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN") .requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
.anyRequest().authenticated()) .anyRequest().authenticated())

View File

@ -77,21 +77,34 @@ public class PaymentService {
* decodeMerchantParameters * decodeMerchantParameters
*/ */
@Transactional @Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParametersB64) throws Exception { public void handleRedsysNotification(String dsSignature, String dsMerchantParameters) throws Exception {
RedsysNotification notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParametersB64); RedsysNotification notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParameters);
// Log útil para depurar
System.out.println(">> Redsys notify: order=" + notif.order +
" amountCents=" + notif.amountCents +
" currency=" + notif.currency +
" response=" + notif.response);
Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order) Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order)
.orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order)); .orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order));
if (!Objects.equals(p.getCurrency(), notif.currency)) { // 🔹 Opción sencilla: sólo comprobar el importe
throw new IllegalStateException("Divisa inesperada");
}
if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) { if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) {
throw new IllegalStateException("Importe inesperado"); throw new IllegalStateException("Importe inesperado: esperado=" +
p.getAmountTotalCents() + " recibido=" + notif.amountCents);
} }
// Idempotencia sencilla: si ya está capturado o reembolsado, no creamos otra // Si quieres, puedes hacer un check mínimamente decente de divisa numérica:
// transacción // (si usas siempre EUR)
/*
* if (!"978".equals(notif.currency)) {
* throw new IllegalStateException("Divisa Redsys inesperada: " +
* notif.currency);
* }
*/
// Idempotencia simple: si ya está capturado o reembolsado, no hacemos nada
if (p.getStatus() == PaymentStatus.CAPTURED if (p.getStatus() == PaymentStatus.CAPTURED
|| p.getStatus() == PaymentStatus.PARTIALLY_REFUNDED || p.getStatus() == PaymentStatus.PARTIALLY_REFUNDED
|| p.getStatus() == PaymentStatus.REFUNDED) { || p.getStatus() == PaymentStatus.REFUNDED) {
@ -101,9 +114,10 @@ public class PaymentService {
PaymentTransaction tx = new PaymentTransaction(); PaymentTransaction tx = new PaymentTransaction();
tx.setPayment(p); tx.setPayment(p);
tx.setType(PaymentTransactionType.CAPTURE); tx.setType(PaymentTransactionType.CAPTURE);
tx.setCurrency(p.getCurrency()); tx.setCurrency(p.getCurrency()); // "EUR"
tx.setAmountCents(notif.amountCents); tx.setAmountCents(notif.amountCents);
tx.setStatus(notif.authorized() ? PaymentTransactionStatus.SUCCEEDED tx.setStatus(notif.authorized()
? PaymentTransactionStatus.SUCCEEDED
: PaymentTransactionStatus.FAILED); : PaymentTransactionStatus.FAILED);
Object authCode = notif.raw.get("Ds_AuthorisationCode"); Object authCode = notif.raw.get("Ds_AuthorisationCode");

View File

@ -116,25 +116,26 @@ public class RedsysController {
@GetMapping("/ko") @GetMapping("/ko")
@ResponseBody @ResponseBody
public ResponseEntity<String> koGet() { public ResponseEntity<String> koGet() {
return ResponseEntity.ok("<h2>Pago cancelado o rechazado</h2><a href=\"/cart\">Volver</a>"); return ResponseEntity.ok("<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>");
} }
@PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody @ResponseBody
public ResponseEntity<String> koPost(@RequestParam Map<String, String> form) { public ResponseEntity<String> koPost(@RequestParam Map<String, String> form) {
// Podrías loguear 'form' si quieres ver qué manda Redsys // Podrías loguear 'form' si quieres ver qué manda Redsys
return ResponseEntity.ok("<h2>Pago cancelado o rechazado</h2><a href=\"/cart\">Volver</a>"); return ResponseEntity.ok("<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>");
} }
@PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody @ResponseBody
@Transactional @jakarta.transaction.Transactional
public String notifyRedsys(@RequestParam("Ds_Signature") String signature, public String notifyRedsys(@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) { @RequestParam("Ds_MerchantParameters") String merchantParameters) {
try { try {
paymentService.handleRedsysNotification(signature, merchantParameters); paymentService.handleRedsysNotification(signature, merchantParameters);
return "OK"; return "OK";
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); // 👈 para ver el motivo del 500 en logs
return "ERROR"; return "ERROR";
} }
} }

View File

@ -38,10 +38,12 @@ public class RedsysService {
// ---------- RECORDS ---------- // ---------- RECORDS ----------
// Pedido a Redsys // Pedido a Redsys
public record PaymentRequest(String order, long amountCents, String description) {} public record PaymentRequest(String order, long amountCents, String description) {
}
// Payload para el formulario // Payload para el formulario
public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {} public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {
}
// ---------- MÉTODO PRINCIPAL (TARJETA) ---------- // ---------- MÉTODO PRINCIPAL (TARJETA) ----------
public FormPayload buildRedirectForm(PaymentRequest req) throws Exception { public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
@ -103,6 +105,7 @@ public class RedsysService {
// ---------- STEP 4: Validar notificación ---------- // ---------- STEP 4: Validar notificación ----------
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64) public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
throws Exception { throws Exception {
// 1) Decodificamos a mapa solo para leer campos
Map<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64); Map<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
RedsysNotification notif = new RedsysNotification(mp); RedsysNotification notif = new RedsysNotification(mp);
@ -110,15 +113,21 @@ public class RedsysService {
throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters"); throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters");
} }
// 2) Calculamos la firma esperada usando el B64 tal cual
ApiMacSha256 api = new ApiMacSha256(); ApiMacSha256 api = new ApiMacSha256();
// Esta línea es opcional para createMerchantSignatureNotif, pero no molesta:
api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64); api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64);
String expected = api.createMerchantSignatureNotif( String expected = api.createMerchantSignatureNotif(
secretKeyBase64, secretKeyBase64,
api.decodeMerchantParameters(dsMerchantParametersB64) dsMerchantParametersB64 // 👈 AQUÍ va el B64, NO el JSON
); );
// 3) Comparamos en constante time, normalizando Base64 URL-safe
if (!safeEqualsB64(dsSignature, expected)) { if (!safeEqualsB64(dsSignature, expected)) {
System.out.println("Firma Redsys no válida");
System.out.println("Ds_Signature (Redsys) = " + dsSignature);
System.out.println("Expected (local) = " + expected);
throw new SecurityException("Firma Redsys no válida"); throw new SecurityException("Firma Redsys no válida");
} }

View File

@ -22,4 +22,4 @@ safekat.api.password=Safekat2024
redsys.environment=test redsys.environment=test
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
redsys.urls.notify=https://hns2jx2x-8080.uks1.devtunnels.ms/pagos/redsys/notify redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify