SpringBoot 快速实现 api 加密

思路

   客户端发送请求时,先把业务 JSON 做 AES 加密,再用 appId、timestamp、nonce、cipherText 拼一个待签名串,用 HmacSHA256 算签名。服务端收到请求后,先验签,再看时间戳,再解密。响应出去时反过来,把响应体再加密一层。

@Data
public class SecureApiRequest {
    private String appId;
    private Long timestamp;
    private String nonce;
    private String data;
    private String sign;
}

 

public final class SignTextBuilder {

    public static String build(String appId, Long timestamp, String nonce, String data) {
        return "appId=" + appId
                + "&timestamp=" + timestamp
                + "&nonce=" + nonce
                + "&data=" + data;
    }
}

加解密工具==========

public final class AesCipher {

    public static String encrypt(String plainText, String key, String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
            byte[] bytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(bytes);
        } catch (Exception e) {
            throw new IllegalStateException("aes encrypt fail", e);
        }
    }

    public static String decrypt(String cipherText, String key, String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            byte[] bytes = Base64.getDecoder().decode(cipherText);
            return new String(cipher.doFinal(bytes), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new IllegalStateException("aes decrypt fail", e);
        }
    }
}

 

 

====签名工具======

public final class HmacSigner {

    public static String sign(String text, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] bytes = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new IllegalStateException("sign fail", e);
        }
    }
}

 

==================HandlerInterceptor======

HandlerInterceptor拦截器接口

HandlerInterceptor是 Spring 框架提供的一个拦截器接口,用于在请求处理过程中拦截和处理请求。

当一个请求到达 Spring MVC 的控制器之前或之后,拦截器可以对请求进行预处理、后处理或者进行拦截操作。

通过实现HandlerInterceptor接口,可以自定义拦截器并注册到 Spring MVC 的拦截器链中。

拦截器可以用于实现一些通用的功能,如权限验证、日志记录、跨域处理等,以及对请求进行修改、重定向或拦截等操作。

HandlerInterceptor接口定义了三个方法,分别是:

  1. preHandle:在请求处理之前被调用。可以进行一些预处理操作,如身份验证、参数校验等。如果返回true,则继续执行后续的拦截器或请求处理器;如果返回false,则中断请求处理流程。
  2. postHandle:在请求处理之后、视图渲染之前被调用。可以对请求的结果进行后处理,如添加额外的模型数据或修改视图等。
  3. afterCompletion:在整个请求处理完成后被调用,即在视图渲染完成后调用。可以进行一些资源清理操作或日志记录等。

通过实现HandlerInterceptor接口,并在配置文件中进行注册,可以将自定义的拦截器应用到 Spring MVC 的请求处理流程中。

拦截器按照配置的顺序依次执行,并可以对请求进行拦截、修改或处理,实现特定的业务逻辑或功能需求。

 

=====

 

@Component
public class ApiSecurityInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String body = request.getReader().lines().collect(Collectors.joining());
        SecureApiRequest req = new ObjectMapper().readValue(body, SecureApiRequest.class);

        ApiClient client = loadClient(req.getAppId());
        String plainSignText = SignTextBuilder.build(req.getAppId(), req.getTimestamp(), req.getNonce(), req.getData());
        String localSign = HmacSigner.sign(plainSignText, client.getSignSecret());

        if (!localSign.equals(req.getSign())) {
            throw new RuntimeException("sign invalid");
        }

        long now = System.currentTimeMillis();
        if (Math.abs(now - req.getTimestamp()) > 5 * 60 * 1000) {
            throw new RuntimeException("request expired");
        }

        String cacheKey = "api:nonce:" + req.getAppId() + ":" + req.getNonce();
        if (!saveNonce(cacheKey, 5 * 60)) {
            throw new RuntimeException("duplicate request");
        }

        String json = AesCipher.decrypt(req.getData(), client.getAesKey(), client.getIv());
        request.setAttribute("plainBody", json);
        return true;
    }
}

posted @ 2026-05-27 22:26  KLAPT  阅读(5)  评论(0)    收藏  举报