|
|
@@ -0,0 +1,135 @@
|
|
|
+package com.malk.apigw.config;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.malk.apigw.caller.CallerProfile;
|
|
|
+import com.malk.apigw.caller.CallerRateLimiter;
|
|
|
+import com.malk.apigw.caller.CallerRegistryService;
|
|
|
+import com.malk.core.NonceCache;
|
|
|
+import com.malk.server.common.McR;
|
|
|
+import com.malk.utils.UtilSignature;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.slf4j.MDC;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 调用方鉴权拦截器
|
|
|
+ *
|
|
|
+ * <p>复用基座 {@link UtilSignature} + {@link NonceCache};按 callerId 查 secret + 限流。</p>
|
|
|
+ *
|
|
|
+ * <p>校验顺序(capability: baas-gateway / REQ-GW-002):</p>
|
|
|
+ * <ol>
|
|
|
+ * <li>Header 齐全(X-Caller-Id / X-MJ-Timestamp / X-MJ-Nonce / X-MJ-Signature)</li>
|
|
|
+ * <li>时间窗 5 分钟</li>
|
|
|
+ * <li>callerId 查注册表 + enabled + 未过期</li>
|
|
|
+ * <li>Nonce 去重</li>
|
|
|
+ * <li>HMAC 签名比对</li>
|
|
|
+ * <li>限流</li>
|
|
|
+ * </ol>
|
|
|
+ */
|
|
|
+@Component
|
|
|
+public class CallerAuthInterceptor extends HandlerInterceptorAdapter {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger("point");
|
|
|
+ public static final String H_CALLER = "X-Caller-Id";
|
|
|
+ public static final String H_TS = "X-MJ-Timestamp";
|
|
|
+ public static final String H_NONCE = "X-MJ-Nonce";
|
|
|
+ public static final String H_SIG = "X-MJ-Signature";
|
|
|
+ public static final String ATTR_CALLER = "com.malk.apigw.caller";
|
|
|
+
|
|
|
+ private static final long WINDOW_MS = 300_000L;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private CallerRegistryService registry;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private NonceCache nonceCache;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private CallerRateLimiter rateLimiter;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
|
+ String callerId = request.getHeader(H_CALLER);
|
|
|
+ String ts = request.getHeader(H_TS);
|
|
|
+ String nonce = request.getHeader(H_NONCE);
|
|
|
+ String sig = request.getHeader(H_SIG);
|
|
|
+ if (empty(callerId) || empty(ts) || empty(nonce) || empty(sig)) {
|
|
|
+ return write(response, 401, "AUTH_FAILED");
|
|
|
+ }
|
|
|
+
|
|
|
+ long clientMs;
|
|
|
+ try {
|
|
|
+ clientMs = Long.parseLong(ts);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return write(response, 401, "AUTH_FAILED");
|
|
|
+ }
|
|
|
+ if (Math.abs(System.currentTimeMillis() - clientMs) > WINDOW_MS) {
|
|
|
+ return write(response, 401, "AUTH_FAILED");
|
|
|
+ }
|
|
|
+
|
|
|
+ CallerProfile caller = registry.get(callerId);
|
|
|
+ if (caller == null || !caller.isEnabled()) {
|
|
|
+ log.warn("[CallerAuth] disabled or not found callerId={}", callerId);
|
|
|
+ return write(response, 401, "AUTH_FAILED");
|
|
|
+ }
|
|
|
+ if (caller.getExpireAt() != null && caller.getExpireAt().getTime() < System.currentTimeMillis()) {
|
|
|
+ return write(response, 401, "AUTH_FAILED");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!nonceCache.putIfAbsent(nonce)) {
|
|
|
+ return write(response, 401, "AUTH_NONCE_REPLAYED");
|
|
|
+ }
|
|
|
+
|
|
|
+ String body = readBody(request);
|
|
|
+ String bodyHash = UtilSignature.sha256Hex(body.getBytes(StandardCharsets.UTF_8));
|
|
|
+ String expected = UtilSignature.sign(caller.getCallerSecret(), ts, nonce,
|
|
|
+ request.getMethod().toUpperCase(), request.getServletPath(), bodyHash);
|
|
|
+ if (!UtilSignature.safeEquals(sig, expected)) {
|
|
|
+ log.warn("[CallerAuth] signature mismatch callerId={} path={}", callerId, request.getServletPath());
|
|
|
+ return write(response, 403, "AUTH_SIGNATURE_INVALID");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!rateLimiter.tryAcquire(callerId, caller.getRateLimit())) {
|
|
|
+ return write(response, 429, "RATE_LIMITED");
|
|
|
+ }
|
|
|
+
|
|
|
+ request.setAttribute(ATTR_CALLER, caller);
|
|
|
+ MDC.put("callerId", callerId);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
|
|
+ MDC.remove("callerId");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean empty(String s) {
|
|
|
+ return s == null || s.isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String readBody(HttpServletRequest req) throws java.io.IOException {
|
|
|
+ BufferedReader reader = req.getReader();
|
|
|
+ if (reader == null) return "";
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) sb.append(line);
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean write(HttpServletResponse resp, int status, String code) throws java.io.IOException {
|
|
|
+ resp.setStatus(status);
|
|
|
+ resp.setCharacterEncoding("UTF-8");
|
|
|
+ resp.setContentType("application/json;charset=UTF-8");
|
|
|
+ McR r = McR.error(code, code);
|
|
|
+ resp.getOutputStream().write(JSON.toJSONString(r).getBytes(StandardCharsets.UTF_8));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|