Rev 36510 | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.spice.profitmandi.web.interceptor;import com.spice.profitmandi.web.filter.RequestCachingFilter;import com.spice.profitmandi.web.model.LoginDetails;import com.spice.profitmandi.web.util.CookiesProcessor;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.security.MessageDigest;import java.util.concurrent.TimeUnit;@Componentpublic class PostInterceptor implements HandlerInterceptor {private static final Logger LOGGER = LogManager.getLogger(PostInterceptor.class);private static final String IDEM_PREFIX = "idem:";private static final long IDEM_TTL_SECONDS = 300;private static final String REQUEST_ATTR_IDEM_KEY = "postInterceptor.idemKey";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate CookiesProcessor cookiesProcessor;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// Idempotency applies only to state-changing methods. GET/HEAD/OPTIONS are// safe/repeatable and must never be deduped (a stale read must not be 400'd).if (!isMutatingMethod(request.getMethod())) {return true;}String idemKey = buildIdempotencyKey(request);if (idemKey == null) {// Nothing reliable to key on (e.g. multipart upload with no client key).return true;}String redisKey = IDEM_PREFIX + idemKey;Boolean claimed = redisTemplate.opsForValue().setIfAbsent(redisKey, "pending", IDEM_TTL_SECONDS, TimeUnit.SECONDS);if (Boolean.TRUE.equals(claimed)) {request.setAttribute(REQUEST_ATTR_IDEM_KEY, idemKey);return true;}LOGGER.info("Duplicate request detected: idemKey={}, uri={}", idemKey, request.getRequestURI());response.setStatus(HttpServletResponse.SC_BAD_REQUEST);response.getWriter().write("Duplicate request.");return false;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {String idemKey = (String) request.getAttribute(REQUEST_ATTR_IDEM_KEY);if (idemKey == null) {return;}String redisKey = IDEM_PREFIX + idemKey;if (ex == null && response.getStatus() >= 200 && response.getStatus() < 300) {redisTemplate.opsForValue().set(redisKey, "done", IDEM_TTL_SECONDS, TimeUnit.SECONDS);} else {redisTemplate.delete(redisKey);}}private boolean isMutatingMethod(String method) {return "POST".equalsIgnoreCase(method)|| "PUT".equalsIgnoreCase(method)|| "PATCH".equalsIgnoreCase(method)|| "DELETE".equalsIgnoreCase(method);}/*** Server-authoritative idempotency key. Dedupes on the request CONTENT* (a hash of the body) scoped to the authenticated partner + method + uri, so* that a client which rotates or otherwise mismanages its IdempotencyKey* header (the jQuery SPA, or an old mobile build that cannot be force-updated)* can no longer produce distinct keys for identical submissions. The client* header is used only as a fallback when the body is not cached (multipart /* form-encoded requests).*/private String buildIdempotencyKey(HttpServletRequest request) {String base = null;if (request instanceof RequestCachingFilter.CachedBodyRequest) {byte[] body = ((RequestCachingFilter.CachedBodyRequest) request).getCachedBody();if (body != null && body.length > 0) {base = "body:" + sha256(body);}}if (base == null) {String header = request.getHeader("IdempotencyKey");if (header != null && !header.isEmpty()) {base = "hdr:" + header;}}if (base == null) {return null;}String principal = resolvePrincipal(request);String query = request.getQueryString();String scope = principal + "|" + request.getMethod() + "|" + request.getRequestURI()+ (query != null ? "?" + query : "") + "|" + base;return sha256(scope.getBytes(java.nio.charset.StandardCharsets.UTF_8));}/*** Scope the key to the logged-in partner so two different partners submitting* an identical payload are not collapsed into a single claim. Falls back to* "anon" for unauthenticated / public mutating endpoints.*/private String resolvePrincipal(HttpServletRequest request) {try {LoginDetails login = cookiesProcessor.getCookiesObject(request);if (login != null && login.getFofoId() > 0) {return "fofo:" + login.getFofoId();}} catch (Exception ignore) {// session/cookies absent on public endpoints -> fall through to anon}return "anon";}private static String sha256(byte[] data) {try {MessageDigest digest = MessageDigest.getInstance("SHA-256");byte[] hash = digest.digest(data);StringBuilder hex = new StringBuilder(64);for (byte b : hash) {hex.append(String.format("%02x", b));}return hex.toString();} catch (Exception e) {throw new RuntimeException("SHA-256 not available", e);}}}