使用AES加密方法,对Springboot+Vue项目进行前后端数据加密

一、后端代码

  1. 安装maven依赖
java 复制代码
   <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15to18</artifactId>
            <version>1.73</version>
        </dependency>
  1. 修复版AES加密工具类
java 复制代码
@Slf4j
public class AESUtil {

    private static final String ALGORITHM = "AES";
    private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";

    private static final String KEY = "0123456789abcdef0123456789abcdef";
    private static final String IV = "0123456789abcdef";

    // Base64清理正则
    private static final Pattern BASE64_CLEAN_PATTERN = Pattern.compile("[\\s\\r\\n\\t]");

    static {
//        log.info("🔑 AES配置信息:");
//        log.info("  算法: {}", ALGORITHM);
//        log.info("  模式: CBC");
//        log.info("  填充: PKCS5Padding");
//        log.info("  密钥: {}", KEY);
//        log.info("  IV: {}", IV);
    }

    /**
     * 清理Base64字符串
     */
    private static String cleanBase64(String base64) {
        if (base64 == null) {
            return null;
        }
        return BASE64_CLEAN_PATTERN.matcher(base64).replaceAll("");
    }

    /**
     * AES加密
     */
    public static String encrypt(String data) throws Exception {
        try {
            log.debug("🔐 AES加密 - 原文长度: {}", data.length());

            SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));

            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);

            byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            String result = Base64.getEncoder().encodeToString(encryptedBytes);

            log.debug("✅ AES加密成功 - 密文长度: {}", result.length());
            return result;
        } catch (Exception e) {
            log.error("❌ AES加密失败: {}", e.getMessage(), e);
            throw new Exception("AES加密失败: " + e.getMessage(), e);
        }
    }

    /**
     * AES解密  增强日志版本
     */
    public static String decrypt(String encryptedData) throws Exception {
        try {
            log.info("🔓 AES解密启动 - 密文长度: {}", encryptedData.length());

            // 记录密文样本
            String preview = encryptedData.length() > 100 ? encryptedData.substring(0, 100) + "..." : encryptedData;
            log.debug("🔓 密文预览: {}", preview);

            // 清理Base64数据
            String cleanedData = cleanBase64(encryptedData);
            log.debug("🔧 清理后密文长度: {}", cleanedData.length());

            SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));

            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);

            byte[] encryptedBytes;
            try {
                encryptedBytes = Base64.getDecoder().decode(cleanedData);
                log.debug("🔧 Base64解码成功,字节长度: {}", encryptedBytes.length);
            } catch (IllegalArgumentException e) {
                log.error("❌ Base64解码失败: {}", e.getMessage());
                throw new Exception("Base64数据格式错误: " + e.getMessage(), e);
            }

            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
            String result = new String(decryptedBytes, StandardCharsets.UTF_8);

            log.info("✅ AES解密成功 - 原文长度: {}", result.length());

            // 🎯 详细记录解密结果
            String resultPreview = result.length() > 200 ? result.substring(0, 200) + "..." : result;
            log.info("🔓 解密结果预览: {}", resultPreview);

            // 检查是否为JSON格式
            String trimmedResult = result.trim();
            boolean isJson = trimmedResult.startsWith("{") || trimmedResult.startsWith("[");
            log.debug("🔍 解密结果JSON检测: {}", isJson ? "是" : "否");

            if (isJson) {
                try {
                    // 验证JSON格式
                    ObjectMapper objectMapper = new ObjectMapper();
                    Object jsonObject = objectMapper.readValue(result, Object.class);
                    log.debug("✅ 解密结果为有效JSON格式");

                    if (jsonObject instanceof Map) {
                        log.debug("📋 JSON对象键: {}", ((Map) jsonObject).keySet());
                    } else if (jsonObject instanceof List) {
                        log.debug("📋 JSON数组长度: {}", ((List) jsonObject).size());
                    }
                } catch (Exception e) {
                    log.warn("⚠️ 解密结果看起来像JSON但解析失败: {}", e.getMessage());
                }
            }

            return result;
        } catch (Exception e) {
            log.error("❌ AES解密失败: {}", e.getMessage());
            log.error("❌ 解密失败详情 - 输入数据长度: {}", encryptedData != null ? encryptedData.length() : 0);
            if (encryptedData != null) {
                log.error("❌ 输入数据预览: {}", encryptedData.length() > 100 ?
                        encryptedData.substring(0, 100) + "..." : encryptedData);
            }
            throw new Exception("AES解密失败: " + e.getMessage(), e);
        }
    }

    /**
     * 测试复杂数据结构
     */
    public static void testComplexData() {
        log.info("🧪 测试AES复杂数据结构处理能力...");

        // 模拟复杂数据结构
        String complexJson = "{\"doctorType\":0,\"isCenterHos\":true,\"shareCheckTimeOutList\":[{\"name\":\"1天内有效\",\"value\":\"1440\"},{\"name\":\"30分钟内有效\",\"value\":\"30\"}],\"checkTypeList\":[{\"name\":\"SR\",\"value\":\"1\"},{\"name\":\"MR\",\"value\":\"2\"}]}";

        try {
            log.info("测试数据长度: {}", complexJson.length());

            String encrypted = encrypt(complexJson);
            log.info("加密成功,密文长度: {}", encrypted);

            String decrypted = decrypt(encrypted);
            log.info("解密成功,原文长度: {}", decrypted);

            boolean success = complexJson.equals(decrypted);
            log.info("复杂数据测试: {}", success ? "✅ 成功" : "❌ 失败");

        } catch (Exception e) {
            log.error("❌ 复杂数据测试失败: {}", e.getMessage(), e);
        }
    }
    public static void main(String[] args) {
        testComplexData();
    }
}
  1. 加密请求包装类
java 复制代码
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class EncryptedRequest implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 加密后的数据
     */
    private String data;

    /**
     * 时间戳
     */
    private Long timestamp;

    public EncryptedRequest() {
    }

    public EncryptedRequest(String data) {
        this.data = data;
        this.timestamp = System.currentTimeMillis();
    }
}
  1. 加密响应包装类
java 复制代码
@Data
public class EncryptedResponse<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 加密后的数据
     */
    private String data;

    /**
     * 时间戳
     */
    private Long timestamp;

    public EncryptedResponse() {
        this.timestamp = System.currentTimeMillis();
    }

    public EncryptedResponse(String data) {
        this();
        this.data = data;
    }

    public static <T> EncryptedResponse<T> success(String encryptedData) {
        return new EncryptedResponse<>(encryptedData);
    }
}
  1. 加密请求包装器
java 复制代码
@Slf4j
public class EncryptHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private String requestBody;
    private String decryptedBody;
    private boolean isEncryptedRequest = false;
    private boolean isDecrypted = false;

    public EncryptHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);

        String isEncrypted = request.getHeader("X-Request-Encrypted");
        this.isEncryptedRequest = "true".equals(isEncrypted);

        logRequestInfo(request);
    }

    /**
     * 记录请求详细信息
     */
    private void logRequestInfo(HttpServletRequest request) {
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String encryptedHeader = request.getHeader("X-Request-Encrypted");
        String contentType = request.getContentType();

        log.info("📥 请求包装器初始化 - {} {} [加密头: {}, Content-Type: {}]",
                method, uri, encryptedHeader, contentType);
        log.debug("📥 原始请求体长度: {}", requestBody.length());

        if (isEncryptedRequest && requestBody != null && !requestBody.trim().isEmpty()) {
            log.debug("📥 加密请求体预览: {}",
                    requestBody.length() > 200 ? requestBody.substring(0, 200) + "..." : requestBody);

            try {
                decryptAndLogRequestBody();
            } catch (Exception e) {
                log.error("❌ 请求解密失败: {}", e.getMessage());
            }
        } else {
            log.debug("📥 普通请求体: {}", requestBody);
            this.decryptedBody = this.requestBody;
        }
    }

    /**
     * 解密并记录请求体
     */
    private void decryptAndLogRequestBody() throws Exception {
        if (isDecrypted) {
            return;
        }

        ObjectMapper objectMapper = new ObjectMapper();

        try {
            // 解析加密请求结构
            Map<String, Object> requestMap = objectMapper.readValue(requestBody, Map.class);
            String encryptedData = (String) requestMap.get("data");

            if (encryptedData != null && !encryptedData.trim().isEmpty()) {
                log.info("🔓 开始解密请求数据 - 密文长度: {}", encryptedData.length());

                // 执行解密
                this.decryptedBody = AESUtil.decrypt(encryptedData);
                this.isDecrypted = true;

                log.info("✅ 请求解密成功 - 原文长度: {}", decryptedBody.length());
                log.info("🔓 解密后请求数据: {}", decryptedBody);

            } else {
                log.warn("⚠️ 加密请求但data字段为空");
                this.decryptedBody = this.requestBody;
            }
        } catch (Exception e) {
            log.error("❌ 解析加密请求结构失败: {}", e.getMessage());
            this.decryptedBody = this.requestBody;
            throw e;
        }
    }

    // 🎯 关键修复:重写getContentType方法
    @Override
    public String getContentType() {
        // 如果是加密请求,解密后的数据是JSON,强制返回application/json
        if (isEncryptedRequest && isDecrypted) {
            log.debug("🔧 强制设置Content-Type为application/json");
            return "application/json";
        }
        return super.getContentType();
    }

    // 🎯 关键修复:重写getHeader方法,确保Content-Type正确
    @Override
    public String getHeader(String name) {
        if ("content-type".equalsIgnoreCase(name) && isEncryptedRequest && isDecrypted) {
            return "application/json";
        }
        return super.getHeader(name);
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream;

        if (decryptedBody != null) {
            byteArrayInputStream = new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8));
            log.debug("🔓 返回解密后的输入流,长度: {}", decryptedBody.length());
        } else {
            byteArrayInputStream = new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8));
            log.debug("📝 返回原始输入流,长度: {}", requestBody.length());
        }

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                // 不需要实现
            }

            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
    }
}
  1. 加密响应包装器
java 复制代码
public class EncryptHttpServletResponseWrapper extends HttpServletResponseWrapper {

    private final ByteArrayOutputStream buffer;
    private final ServletOutputStream outputStream;
    private final PrintWriter writer;

    public EncryptHttpServletResponseWrapper(HttpServletResponse response) {
        super(response);
        buffer = new ByteArrayOutputStream();
        outputStream = new WrapperOutputStream(buffer);
        writer = new PrintWriter(outputStream);
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return outputStream;
    }

    @Override
    public PrintWriter getWriter() {
        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        }
        if (outputStream != null) {
            outputStream.flush();
        }
    }

    public byte[] getResponseData() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    private static class WrapperOutputStream extends ServletOutputStream {
        private final ByteArrayOutputStream buffer;

        public WrapperOutputStream(ByteArrayOutputStream buffer) {
            this.buffer = buffer;
        }

        @Override
        public void write(int b) {
            buffer.write(b);
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {
            // 不需要实现
        }
    }
}
  1. 全局加解密过滤器
java 复制代码
@Slf4j
public class GlobalEncryptDecryptFilter implements Filter {

    private final ObjectMapper objectMapper = new ObjectMapper();

    // 添加请求标记,避免重复处理
    private static final String FILTER_APPLIED_ATTRIBUTE = GlobalEncryptDecryptFilter.class.getName() + ".APPLIED";
    // 排除加密的接口
    private static final String[] EXCLUDE_ENCRYPT_URIS = {
            "/static",
            "/error",
    };

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String requestURI = httpRequest.getRequestURI();
        String method = httpRequest.getMethod();

        // 🎯 只使用请求属性检查
        if (httpRequest.getAttribute(FILTER_APPLIED_ATTRIBUTE) != null) {
//            log.debug("⏩ 跳过已处理的请求: {} {}", method, requestURI);
            chain.doFilter(request, response);
            return;
        }

        log.info("🔐 全局加解密过滤器处理请求: {} {}", method, requestURI);

        // 标记请求已处理
        httpRequest.setAttribute(FILTER_APPLIED_ATTRIBUTE, Boolean.TRUE);

        try {

            // 🎯 新增:检查是否是Multipart请求(文件上传)
            if (isMultipartRequest(httpRequest)) {
                log.info("📎 跳过Multipart文件上传请求: {}", requestURI);
                chain.doFilter(request, response);
                return;
            }

            // 检查是否在排除列表中
            if (shouldExcludeEncrypt(requestURI)) {
                log.info("✅ 跳过加解密: {}", requestURI);
                chain.doFilter(request, response);
                return;
            }

            // 🎯 重构:根据请求类型选择处理方式
            boolean isEncryptedRequest = "true".equals(httpRequest.getHeader("X-Request-Encrypted"));
            String contentType = httpRequest.getContentType();
            boolean isPostMethod = "POST".equalsIgnoreCase(method);

            log.debug("🔍 请求分析 - 加密头: {}, Content-Type: {}, 方法: {}", isEncryptedRequest, contentType, method);

            if (isEncryptedRequest && isPostMethod) {
                // 🎯 加密的POST请求:使用专门的加密请求处理
                handleEncryptedRequest(httpRequest, httpResponse, chain);
            } else {
                // 🎯 非加密请求或GET请求:使用普通处理
                EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(httpResponse);
                chain.doFilter(request, responseWrapper);
                processResponseEncryption(requestURI, responseWrapper, httpResponse);
            }

        } catch (Exception e) {
            log.error("❌ 加解密处理异常, URI: {}", requestURI, e);
            handleEncryptError(response, e);
        }
    }

    /**
     * 处理加密请求 - 执行解密并继续过滤器链
     */
    private void handleEncryptedRequest(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws Exception {

        log.info("🔐 处理加密请求: {} {}", request.getMethod(), request.getRequestURI());

        // 使用深度集成包装器
//        SpringMvcCompatibleRequestWrapper requestWrapper = new SpringMvcCompatibleRequestWrapper(request);
        UniversalRequestWrapper requestWrapper = new UniversalRequestWrapper(request);
        EncryptHttpServletResponseWrapper responseWrapper = new EncryptHttpServletResponseWrapper(response);

        // 🎯 关键:将解密内容存储为请求属性,供后续参数绑定使用
        if (requestWrapper.isEncryptedRequest()) {
            String decryptedContent = requestWrapper.getDecryptedContent();
            // 存储解密内容到请求属性,确保后续流程可以获取
            request.setAttribute("DECRYPTED_REQUEST_BODY", decryptedContent);
            log.info("🎯 解密内容已存储到请求属性,长度: {}", decryptedContent.length());
            log.debug("🔓 解密内容预览: {}",
                    decryptedContent.length() > 200 ? decryptedContent.substring(0, 200) + "..." : decryptedContent);
        }

        // 继续过滤器链
        chain.doFilter(requestWrapper, responseWrapper);

        // 处理响应加密
        processResponseEncryption(request.getRequestURI(), responseWrapper, response);
    }

    /**
     * 🎯 新增:检查是否是Multipart请求(文件上传)
     */
    private boolean isMultipartRequest(HttpServletRequest request) {
        String contentType = request.getContentType();
        boolean isMultipart = contentType != null && contentType.toLowerCase().startsWith("multipart/");

        if (isMultipart) {
            log.debug("📎 检测到Multipart请求,Content-Type: {}", contentType);
        }

        return isMultipart;
    }


    /**
     * 检查是否应该排除加密 - 支持通配符匹配
     */
    private boolean shouldExcludeEncrypt(String uri) {
        for (String excludeUri : EXCLUDE_ENCRYPT_URIS) {
            // 如果排除项以 * 结尾,使用前缀匹配
            if (excludeUri.endsWith("/*")) {
                String prefix = excludeUri.substring(0, excludeUri.length() - 1);
                if (uri.startsWith(prefix)) {
                    log.debug("✅ 前缀匹配排除: {} -> {}", excludeUri, uri);
                    return true;
                }
            }
            // 精确匹配
            else if (uri.equals(excludeUri)) {
                log.debug("✅ 精确匹配排除: {}", uri);
                return true;
            }
            // 路径前缀匹配(原有逻辑保持)
            else if (uri.startsWith(excludeUri)) {
                log.debug("✅ 路径匹配排除: {} -> {}", excludeUri, uri);
                return true;
            }
        }
        return false;
    }

    /**
     * 处理响应加密
     */
    private void processResponseEncryption(String requestURI,
                                           EncryptHttpServletResponseWrapper responseWrapper,
                                           HttpServletResponse originalResponse) throws Exception {

        byte[] responseData = responseWrapper.getResponseData();
        if (responseData.length > 0) {
            String responseBody = new String(responseData, StandardCharsets.UTF_8);

            log.info("🔐 处理响应加密, URI: {}", requestURI);

            if (isJsonResponse(responseWrapper) && !responseBody.trim().isEmpty()) {
                try {
                    // 解析原始响应
                    Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
                    Integer code = (Integer) responseMap.get("code");
                    Object data = responseMap.get("data");

                    // 🎯 关键修复:检查数据是否已经是加密字符串
                    boolean isAlreadyEncrypted = isEncryptedData(data);

                    // 只对成功的业务响应且未加密的数据进行加密
                    if (code != null && code == 200 && data != null && !isAlreadyEncrypted) {
                        log.info("✅ 开始AES加密响应数据");

                        // 加密数据
                        String dataJson = objectMapper.writeValueAsString(data);
                        String encryptedData = AESUtil.encrypt(dataJson);

                        // 构建加密响应
                        Map<String, Object> encryptedResponse = new HashMap<>();
                        encryptedResponse.put("code", 200);
                        encryptedResponse.put("msg", responseMap.get("msg"));
                        encryptedResponse.put("data", encryptedData);
                        encryptedResponse.put("timestamp", System.currentTimeMillis());

                        String finalResponse = objectMapper.writeValueAsString(encryptedResponse);
                        byte[] finalResponseBytes = finalResponse.getBytes(StandardCharsets.UTF_8);

                        // 设置响应头
                        originalResponse.setHeader("X-Response-Encrypted", "true");
                        originalResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
                        originalResponse.setContentLength(finalResponseBytes.length);

                        // 清空可能已写入的数据
                        originalResponse.resetBuffer();

                        // 写入加密后的数据
                        originalResponse.getOutputStream().write(finalResponseBytes);
                        originalResponse.getOutputStream().flush();

                        log.info("✅ 响应AES加密成功, URI: {}, 数据长度: {}", requestURI, encryptedData.length());
                        return;
                    } else if (isAlreadyEncrypted) {
                        log.info("⚠️ 数据已经是加密状态,跳过重复加密, URI: {}", requestURI);
                    } else {
                        log.debug("响应不符合加密条件, code: {}, data: {}", code, data != null);
                    }
                } catch (Exception e) {
                    log.error("❌ 响应AES加密失败,返回原始数据: {}", e.getMessage(), e);
                }
            }

            // 返回原始数据
            originalResponse.resetBuffer();
            originalResponse.getOutputStream().write(responseData);
            log.info("📝 返回原始响应数据, URI: {}", requestURI);
        }
    }

    /**
     * 判断数据是否已经是加密字符串
     */
    private boolean isEncryptedData(Object data) {
        if (!(data instanceof String)) {
            return false;
        }

        String dataStr = (String) data;

        // 加密数据通常的特征:
        // 1. 长度较长(至少100字符以上)
        // 2. 只包含Base64字符(A-Z, a-z, 0-9, +, /, =)
        // 3. 不包含JSON特征字符({, }, :, "等)
        if (dataStr.length() < 100) {
            return false;
        }

        // 检查是否是Base64格式
        String base64Pattern = "^[A-Za-z0-9+/]*={0,2}$";
        boolean isBase64 = dataStr.matches(base64Pattern);

        // 检查是否包含JSON特征
        boolean hasJsonChars = dataStr.contains("{") || dataStr.contains("}") ||
                dataStr.contains("\"") || dataStr.contains(":");

        log.debug("🔍 加密数据检测 - 长度: {}, Base64格式: {}, 包含JSON字符: {}",
                dataStr.length(), isBase64, hasJsonChars);

        return isBase64 && !hasJsonChars;
    }

    private boolean isJsonResponse(HttpServletResponse response) {
        String contentType = response.getContentType();
        return contentType != null && contentType.contains(MediaType.APPLICATION_JSON_VALUE);
    }

    private void handleEncryptError(ServletResponse response, Exception e) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

        Map<String, Object> errorResult = new HashMap<>();
        errorResult.put("code", 500);
        errorResult.put("msg", "系统加解密异常: " + e.getMessage());
        String errorResponse = objectMapper.writeValueAsString(errorResult);

        // 确保错误响应不设置加密头
        response.getWriter().write(errorResponse);

        log.error("💥 加解密异常处理完成");
    }
}
  1. 通用请求包装器 - 自动处理加密请求的参数绑定
java 复制代码
@Slf4j
public class UniversalRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] originalBodyBytes;
    private byte[] decryptedBodyBytes;
    private final boolean isEncryptedRequest;
    private boolean isDecrypted = false;

    // 🎯 关键:参数映射,用于支持参数绑定
    private Map<String, String[]> parameterMap;
    private boolean parametersExtracted = false;
    private final ObjectMapper objectMapper = new ObjectMapper();

    // 🎯 关键:使用CaseInsensitiveMap确保头部不区分大小写
    private final Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    public UniversalRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        // 🎯 保存所有原始头部信息
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            headers.put(headerName, headerValue);
        }

        this.originalBodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
        this.isEncryptedRequest = "true".equals(request.getHeader("X-Request-Encrypted"));

        log.info("🔄 通用请求包装器 - {} {} [加密: {}]",
                request.getMethod(), request.getRequestURI(), isEncryptedRequest);

        if (this.isEncryptedRequest) {
            try {
                decryptRequestBody();
                // 🎯 关键:解密后自动提取参数
                extractParametersFromJson();
            } catch (Exception e) {
                log.error("❌ 请求解密失败: {}", e.getMessage());
                this.decryptedBodyBytes = this.originalBodyBytes;
            }
        } else {
            this.decryptedBodyBytes = this.originalBodyBytes;
        }
    }

    /**
     * 解密请求体
     */
    private void decryptRequestBody() throws Exception {
        if (isDecrypted) {
            return;
        }

        try {
            String originalRequestBody = new String(originalBodyBytes, StandardCharsets.UTF_8);
            log.debug("📥 加密请求体字符串长度: {}", originalRequestBody.length());

            // 解析加密请求结构
            Map<String, Object> requestMap = objectMapper.readValue(originalRequestBody, Map.class);
            String encryptedData = (String) requestMap.get("data");

            if (encryptedData != null && !encryptedData.trim().isEmpty()) {
                log.info("🔓 开始解密请求数据 - 密文长度: {}", encryptedData.length());

                // 执行解密
                String decryptedBody = AESUtil.decrypt(encryptedData);
                this.decryptedBodyBytes = decryptedBody.getBytes(StandardCharsets.UTF_8);
                this.isDecrypted = true;

                log.info("✅ 请求解密成功 - 原文字节长度: {}", decryptedBodyBytes.length);
                log.debug("🔓 解密后请求数据: {}", decryptedBody);

                // 验证JSON格式
                try {
                    objectMapper.readValue(decryptedBody, Object.class);
                    log.debug("✅ 解密数据是有效的JSON格式");
                } catch (Exception e) {
                    log.error("❌ 解密后的数据不是有效的JSON: {}", e.getMessage());
                    throw new RuntimeException("解密后的数据不是有效的JSON格式");
                }
            } else {
                log.warn("⚠️ 加密请求但data字段为空");
                this.decryptedBodyBytes = this.originalBodyBytes;
            }
        } catch (Exception e) {
            log.error("❌ 解析加密请求结构失败: {}", e.getMessage());
            this.decryptedBodyBytes = this.originalBodyBytes;
            throw e;
        }
    }

    /**
     * 🎯 关键方法:从JSON中提取参数
     */
    private void extractParametersFromJson() {
        if (parametersExtracted) {
            return;
        }

        try {
            String decryptedContent = getDecryptedContent();
            if (decryptedContent != null && !decryptedContent.trim().isEmpty()) {
                Map<String, Object> jsonMap = objectMapper.readValue(decryptedContent, Map.class);
                this.parameterMap = new HashMap<>();

                for (Map.Entry<String, Object> entry : jsonMap.entrySet()) {
                    String key = entry.getKey();
                    Object value = entry.getValue();

                    if (value == null) {
                        parameterMap.put(key, new String[]{null});
                    } else if (value instanceof List) {
                        // 处理数组
                        List<?> list = (List<?>) value;
                        String[] array = list.stream()
                                .map(item -> item != null ? item.toString() : null)
                                .toArray(String[]::new);
                        parameterMap.put(key, array);
                    } else {
                        // 处理单个值
                        parameterMap.put(key, new String[]{value.toString()});
                    }
                }

                log.info("🎯 从JSON提取参数: {}", parameterMap.keySet());
                log.debug("📋 参数详情: {}", parameterMap);
            }
        } catch (Exception e) {
            log.warn("⚠️ JSON参数提取失败: {}", e.getMessage());
        } finally {
            parametersExtracted = true;
        }
    }

    // 🎯 关键:重写参数相关方法,支持参数绑定
    @Override
    public String getParameter(String name) {
        // 优先返回从JSON提取的参数
        if (parameterMap != null && parameterMap.containsKey(name)) {
            String[] values = parameterMap.get(name);
            String result = values != null && values.length > 0 ? values[0] : null;
            log.debug("🔍 获取参数 {}: {}", name, result);
            return result;
        }
        return super.getParameter(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        // 合并原始参数和JSON参数(JSON参数优先)
        if (parameterMap != null && !parameterMap.isEmpty()) {
            Map<String, String[]> merged = new HashMap<>(super.getParameterMap());
            merged.putAll(parameterMap);
            return merged;
        }
        return super.getParameterMap();
    }

    @Override
    public Enumeration<String> getParameterNames() {
        if (parameterMap != null && !parameterMap.isEmpty()) {
            Set<String> names = new HashSet<>(Collections.list(super.getParameterNames()));
            names.addAll(parameterMap.keySet());
            return Collections.enumeration(names);
        }
        return super.getParameterNames();
    }

    @Override
    public String[] getParameterValues(String name) {
        if (parameterMap != null && parameterMap.containsKey(name)) {
            String[] values = parameterMap.get(name);
            log.debug("🔍 获取参数值 {}: {}", name, Arrays.toString(values));
            return values;
        }
        return super.getParameterValues(name);
    }

    // 🎯 关键:确保Authorization头正确返回
    @Override
    public String getHeader(String name) {
        if ("content-type".equalsIgnoreCase(name)) {
            return "application/json;charset=UTF-8";
        }

        if ("authorization".equalsIgnoreCase(name)) {
            String authHeader = headers.get(name);
            if (authHeader != null) {
                return authHeader;
            }
        }

        String headerValue = headers.get(name);
        if (headerValue != null) {
            return headerValue;
        }

        return super.getHeader(name);
    }

    @Override
    public Enumeration<String> getHeaderNames() {
        Set<String> headerNames = new HashSet<>(headers.keySet());
        String authHeader = super.getHeader("Authorization");
        if (authHeader != null && !headers.containsKey("Authorization")) {
            headers.put("Authorization", authHeader);
            headerNames.add("Authorization");
        }
        return Collections.enumeration(headerNames);
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        if ("content-type".equalsIgnoreCase(name)) {
            return Collections.enumeration(Collections.singletonList("application/json;charset=UTF-8"));
        }

        String headerValue = headers.get(name);
        if (headerValue != null) {
            return Collections.enumeration(Collections.singletonList(headerValue));
        }

        return super.getHeaders(name);
    }

    // 🎯 关键:重写输入流方法,确保返回解密后的数据
    @Override
    public ServletInputStream getInputStream() throws IOException {
        log.debug("🔓 返回解密后的输入流,字节长度: {}", decryptedBodyBytes.length);

        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decryptedBodyBytes);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                // 不需要实现
            }

            @Override
            public int read() {
                return byteArrayInputStream.read();
            }

            @Override
            public int read(byte[] b, int off, int len) throws IOException {
                return byteArrayInputStream.read(b, off, len);
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
    }

    // 🎯 关键:重写内容长度相关方法
    @Override
    public int getContentLength() {
        return decryptedBodyBytes.length;
    }

    @Override
    public long getContentLengthLong() {
        return decryptedBodyBytes.length;
    }

    @Override
    public String getCharacterEncoding() {
        return "UTF-8";
    }

    /**
     * 获取解密后的请求体内容(用于调试)
     */
    public String getDecryptedContent() {
        return new String(decryptedBodyBytes, StandardCharsets.UTF_8);
    }

    public boolean isEncryptedRequest() {
        return isEncryptedRequest;
    }
}
  1. 注意,全局过滤器需要配置自己框架所有过滤器的最前面,比如Security配置类configure方法中
  2. 代码只是把data数据加密了,所有全部数据加密,需要根据需要调整响应的响应返回参数,需要根据实际业务操作进行调整,代码仅供参考,可以根据自己的想法和思路进行相应调整

二、前端代码

  1. 安装依赖 npm install crypto-js
  2. 创建AES加密工具类 aes.js
java 复制代码
// src/utils/aes.js
import CryptoJS from 'crypto-js'

/**
 * AES加密工具类 - 解决数据类型问题
 */
class AESUtil {
    constructor() {
        this.key = CryptoJS.enc.Utf8.parse('0123456789abcdef0123456789abcdef')
        this.iv = CryptoJS.enc.Utf8.parse('0123456789abcdef')
        this.mode = CryptoJS.mode.CBC
        this.padding = CryptoJS.pad.Pkcs7
    }

    /**
     * 智能数据类型处理
     */
    processDataForEncryption(data) {
        // 如果是字符串,直接返回(不进行JSON.stringify)
        if (typeof data === 'string') {
            return {
                data: data,
                type: 'string'
            }
        }
        // 如果是对象或数组,进行JSON序列化
        else if (typeof data === 'object' && data !== null) {
            return {
                data: JSON.stringify(data),
                type: 'json'
            }
        }
        // 其他类型转换为字符串
        else {
            return {
                data: String(data),
                type: 'string'
            }
        }
    }

    /**
     * 根据类型处理解密结果
     */
    processDecryptedData(decryptedStr, originalType) {
        if (originalType === 'json') {
            try {
                return JSON.parse(decryptedStr)
            } catch (e) {
                console.warn('⚠️ JSON解析失败,返回原始字符串:', e.message)
                return decryptedStr
            }
        } else {
            return decryptedStr
        }
    }

    /**
     * AES加密
     */
    encrypt(data) {
        try {
            // 智能处理数据类型
            const processed = this.processDataForEncryption(data)
            // console.log('🔐 AES加密 - 数据类型:', processed.type, '原始长度:', data.length, '处理后长度:', processed.data.length)

            const encrypted = CryptoJS.AES.encrypt(processed.data, this.key, {
                iv: this.iv,
                mode: this.mode,
                padding: this.padding
            })

            const result = encrypted.toString()

            // 返回包含类型信息的加密结果
            return {
                encryptedData: result,
                dataType: processed.type
            }
        } catch (error) {
            console.error('❌ AES加密失败:', error)
            throw new Error(`AES加密失败: ${error.message}`)
        }
    }

    /**
     * AES解密 
     */
    decrypt(encryptedData, dataType = 'auto') {
        try {
            if (!encryptedData || typeof encryptedData !== 'string') {
                throw new Error('解密数据必须是字符串')
            }

            // console.log('🔓 AES开始解密,密文长度:', encryptedData.length, '数据类型:', dataType)

            const decrypted = CryptoJS.AES.decrypt(encryptedData, this.key, {
                iv: this.iv,
                mode: this.mode,
                padding: this.padding
            })

            const resultStr = decrypted.toString(CryptoJS.enc.Utf8)

            if (!resultStr) {
                throw new Error('解密结果为空,可能是密钥或数据格式错误')
            }

            // console.log('✅ AES解密成功,原始字符串长度:', resultStr.length)
            // console.log('🔍 解密后的原始内容:', resultStr)

            // 🎯 关键修复:强制尝试JSON解析
            let finalResult
            if (dataType === 'json' || dataType === 'auto') {
                // 对于接口响应数据,强制尝试JSON解析
                try {
                    finalResult = JSON.parse(resultStr)
                    // console.log('✅ JSON解析成功')
                } catch (e) {
                    // JSON解析失败,检查是否真的是JSON格式但有小问题
                    const trimmed = resultStr.trim()
                    if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
                        (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
                        console.warn('⚠️ JSON格式有误,但看起来是JSON,尝试修复...')
                        try {
                            // 尝试修复常见的JSON格式问题
                            const fixedJson = trimmed
                                .replace(/(\w+):/g, '"$1":') // 修复无引号的key
                                .replace(/'/g, '"') // 单引号转双引号
                            finalResult = JSON.parse(fixedJson)
                            // console.log('✅ 修复后JSON解析成功')
                        } catch (fixError) {
                            // console.error('❌ JSON修复失败,返回原始字符串')
                            finalResult = resultStr
                        }
                    } else {
                        // console.log('📝 不是JSON格式,返回字符串')
                        finalResult = resultStr
                    }
                }
            } else {
                finalResult = resultStr
                // console.log('📝 作为字符串返回')
            }

            // console.log('🎯 最终解密结果类型:', typeof finalResult)
            if (typeof finalResult === 'object') {
                // console.log('🎯 解密对象键名:', Object.keys(finalResult))
            }

            return finalResult
        } catch (error) {
            console.error('❌ AES解密失败:', error)
            throw new Error(`AES解密失败: ${error.message}`)
        }
    }
    /**
     * 兼容性封装函数 - 用于HTTP拦截器
     */
    encryptForRequest(data) {
        const result = this.encrypt(data)
        // 对于请求,我们只返回加密字符串,类型信息通过其他方式传递
        return result.encryptedData
    }

    /**
     * 修复的测试用例
     */
    testComplexData() {
        // console.group('🧪 修复版AES兼容性测试')

        const testCases = [
            {
                name: '简单对象',
                data: { test: "简单测试", number: 123 },
                expectedType: 'json'
            },
            {
                name: '复杂对象',
                data: {
                    list: Array(10).fill().map((_, i) => ({ id: i, name: `项目${i}` })),
                    config: { enabled: true, timeout: 5000 }
                },
                expectedType: 'json'
            },
            {
                name: '纯字符串',
                data: "这是一个纯文本字符串,没有JSON格式",
                expectedType: 'string'
            },
            {
                name: 'JSON格式字符串',
                data: '{"name": "test", "value": 123}',
                expectedType: 'string' // 注意:这是字符串,不是对象
            },
            {
                name: '超长字符串',
                data: "这是一个很长的测试字符串".repeat(100),
                expectedType: 'string'
            }
        ]

        let allPassed = true
        const results = []

        for (const testCase of testCases) {
            try {
                // console.log(`\n📋 测试用例: ${testCase.name}`)
                // console.log('输入数据类型:', typeof testCase.data)
                // console.log('输入数据长度:', typeof testCase.data === 'string' ? testCase.data.length : JSON.stringify(testCase.data).length)

                // 加密
                const encryptedResult = this.encrypt(testCase.data)
                // console.log('加密结果长度:', encryptedResult.encryptedData.length)
                // console.log('加密数据类型:', encryptedResult.dataType)

                // 解密 - 根据加密时的类型信息
                const decrypted = this.decrypt(encryptedResult.encryptedData, encryptedResult.dataType)
                // console.log('解密结果类型:', decrypted)

                // 验证
                let expected
                if (testCase.expectedType === 'json') {
                    expected = testCase.data
                } else {
                    expected = testCase.data
                }

                const success = JSON.stringify(decrypted) === JSON.stringify(expected)


                // console.log(`测试结果: ${success ? '✅ 通过' : '❌ 失败'}`)
                // console.log('解密结果类型:', typeof decrypted)

                if (!success) {
                    // console.error('数据不匹配:')
                    // console.error('  预期:', JSON.stringify(expected).substring(0, 100))
                    // console.error('  实际:', JSON.stringify(decrypted).substring(0, 100))
                }

                results.push({
                    name: testCase.name,
                    success: success,
                    inputType: typeof testCase.data,
                    encryptedType: encryptedResult.dataType,
                    decryptedType: typeof decrypted,
                    inputLength: typeof testCase.data === 'string' ? testCase.data.length : JSON.stringify(testCase.data).length,
                    encryptedLength: encryptedResult.encryptedData.length,
                    decryptedLength: typeof decrypted === 'string' ? decrypted.length : JSON.stringify(decrypted).length
                })

                allPassed = allPassed && success

            } catch (error) {
                // console.error(`❌ 测试用例"${testCase.name}"执行失败:`, error.message)
                results.push({
                    name: testCase.name,
                    success: false,
                    error: error.message
                })
                allPassed = false
            }
        }

        // console.log('\n📊 测试总结:')
        results.forEach(result => {
            const status = result.success ? '✅' : '❌'
            const text = result.success ? '通过' : '失败'
            // console.log(`  ${result.name}: ${status} ${text}`)
            // console.log(`    输入: ${result.inputType}, 加密: ${result.encryptedType}, 解密: ${result.decryptedType}`)
            if (result.error) {
                // console.log(`    错误: ${result.error}`)
            }
        })

        // console.log(`\n🎯 总体结果: ${allPassed ? '✅ 所有测试通过' : '❌ 存在失败测试'}`)
        console.groupEnd()

        return allPassed
    }
}

// 创建单例实例
const aesUtil = new AESUtil()

// 导出工具函数 - 保持向后兼容
export const encrypt = (data) => {
    const result = aesUtil.encrypt(data)
    return result.encryptedData
}

export const decrypt = (encryptedData) => aesUtil.decrypt(encryptedData, 'auto')
export const testComplexData = () => aesUtil.testComplexData()

export default aesUtil
  1. 在前端vue统一请求响应方法中添加加密和解密过滤
java 复制代码
//添加aes方法
import { encrypt, decrypt} from '../utils/aes'
  1. 在request 拦截器添加方法
java 复制代码
// request 拦截器
axios.interceptors.request.use(
    (req) => {
        console.log('🔐 请求拦截器 - URL:', req.url, '方法:', req.method)
  // 加密处理逻辑
        const isPostMethod = req.method && req.method.toLowerCase() === 'post'
        const isNotFormData = !(req.data instanceof FormData)
        const shouldEncryptThisRequest = shouldEncrypt(url)

        // console.log('🔐 加密检查 - POST:', isPostMethod, '非FormData:', isNotFormData, '需要加密:', shouldEncryptThisRequest)

        if (isPostMethod && shouldEncryptThisRequest && req.data && isNotFormData) {
            try {
                // 使用修复版的加密
                const encryptedData = encrypt(req.data)

                const encryptedRequest = {
                    data: encryptedData,
                    timestamp: Date.now()
                }
                req.data = encryptedRequest
                req.headers['X-Request-Encrypted'] = 'true'
                req.headers['Content-Type'] = 'application/json'
                // console.log('✅ 请求AES加密成功')
            } catch (error) {
                console.error('❌ 请求AES加密失败:', error)
                req.data = qs.stringify(req.data)
                req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
            }
        } else if (isPostMethod && !shouldEncryptThisRequest && isNotFormData) {
            req.data = qs.stringify(req.data)
            req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
            // console.log('📝 使用表单格式,不加密')
        }

        return req
    },
    (error) => {
        console.error('❌ 请求拦截器错误:', error)
        return Promise.reject(error)
    }
)
 /**
 * 检查URL是否需要加密 - 支持动态参数和通配符
 */
function shouldEncrypt(url) {
    if (!url) return false

    return !EXCLUDE_ENCRYPT_URLS.some(excludeUrl => {
        // 🎯 新增:如果排除项以 /* 结尾,使用前缀匹配
        if (excludeUrl.endsWith('/*')) {
            const prefix = excludeUrl.substring(0, excludeUrl.length - 2); // 去掉 /*
            if (url.startsWith(prefix)) {
                // console.log(`🔕 通配符匹配排除加密: ${url} -> ${excludeUrl}`);
                return true;
            }
        }
        // 如果排除项以 / 结尾,使用前缀匹配(处理动态参数)
        else if (excludeUrl.endsWith('/')) {
            if (url.startsWith(excludeUrl)) {
                // console.log(`🔕 前缀匹配排除加密: ${url} -> ${excludeUrl}`);
                return true;
            }
        }
        // 精确匹配
        else if (url === excludeUrl) {
            // console.log(`🔕 精确匹配排除加密: ${url}`);
            return true;
        }
        // 包含匹配(原有逻辑)
        else if (url.includes(excludeUrl)) {
            // console.log(`🔕 包含匹配排除加密: ${url} -> ${excludeUrl}`);
            return true;
        }
        return false;
    });
}
// response 拦截器 - 使用AES解密
axios.interceptors.response.use(
    (response) => {
      // 如果AES测试失败,禁用解密
        if (window.AES_DISABLED) {
            console.warn('⚠️ AES解密已禁用,直接返回原始数据')
            return handleBusinessLogic(response.data)
        }
                const url = response.config.url
        const isEncryptedResponse = response.headers['x-response-encrypted'] === 'true'
        const shouldEncryptThisResponse = shouldEncrypt(url)
    let finalData = response.data

        // 处理加密响应
        if ((isEncryptedResponse || shouldEncryptThisResponse) && response.data) {
            try {
                console.group('🔐 加密响应处理流程 - URL: ' + url)

                let encryptedData = null
                let responseStructure = null

                if (response.data.data && typeof response.data.data === 'string') {
                    encryptedData = response.data.data
                    responseStructure = 'standard'
                    // console.log('🔓 标准加密格式,加密数据长度:', encryptedData.length)
                } else if (typeof response.data === 'string') {
                    encryptedData = response.data
                    responseStructure = 'direct'
                    // console.log('🔓 直接加密格式,加密数据长度:', encryptedData.length)
                } else {
                    // console.warn('⚠️ 无法识别的加密格式,跳过解密')
                    console.groupEnd()
                }

                if (encryptedData) {
                    // console.log('🔐 原始加密数据预览:', encryptedData.substring(0, 100) + '...')

                    try {
                        // 使用修复版的解密(自动检测类型)
                        const decryptedResult = decrypt(encryptedData)
                        // console.log('✅ AES解密成功!')
                        // console.log('📊 解密结果类型:', typeof decryptedResult)
                        // console.log('📏 解密结果长度:', typeof decryptedResult === 'string' ? decryptedResult.length : JSON.stringify(decryptedResult).length)

                        // 🎯 关键位置:打印每个接口的完整解密数据
                        // console.log('🔓 完整解密数据:')
                        if (typeof decryptedResult === 'string') {
                            // 如果是字符串,直接输出
                            // console.log('  字符串内容:', decryptedResult)
                        } else {
                            // 如果是对象,格式化输出
                            // console.log('  JSON对象:', JSON.stringify(decryptedResult, null, 2))
                        }

                        // 检查数据结构
                        if (typeof decryptedResult === 'object' && decryptedResult !== null) {
                            // console.log('📋 对象键名:', Object.keys(decryptedResult))
                            if (Array.isArray(decryptedResult)) {
                                // console.log('📋 数组长度:', decryptedResult.length)
                                if (decryptedResult.length > 0) {
                                    // console.log('📋 数组第一项:', decryptedResult[0])
                                }
                            }
                        }

                        // 根据响应结构构建最终数据
                        if (responseStructure === 'standard') {
                            finalData = {
                                ...response.data,
                                data: decryptedResult
                            }
                        } else {
                            finalData = decryptedResult
                        }

                        // console.log('🎯 最终返回给业务的数据结构:', typeof finalData)
                        if (typeof finalData === 'object' && finalData !== null) {
                            // console.log('🎯 最终数据包含的字段:', Object.keys(finalData))
                        }

                    } catch (decryptError) {
                        console.error('❌ AES解密失败:', decryptError.message)
                        console.error('❌ 解密失败详情:', {
                            url: url,
                            encryptedDataLength: encryptedData.length,
                            error: decryptError.message,
                            stack: decryptError.stack
                        })
                        // 解密失败时保持原始数据
                    }
                }
                console.groupEnd()
            } catch (error) {
                console.groupEnd()
                console.error('❌ 响应解密处理异常 - URL: ' + url, error)
                console.error('❌ 异常详情:', {
                    url: url,
                    error: error.message,
                    stack: error.stack
                })
            }
        }
    }
)
// 代码仅供参考、需要根据实际业务场景进行判断和使用AES加密方法
  1. 前端效果,可以看到前后端data的请求响应参数就加密完成

相关推荐
码事漫谈3 小时前
单链表与双链表专题详解
后端
momo061173 小时前
用一篇文章带你手写Vue中的reactive响应式
javascript·vue.js
Lear3 小时前
【JavaSE】NIO技术与应用:高并发网络编程的利器
后端
expect7g3 小时前
Paimon源码解读 -- Compaction-3.MergeSorter
大数据·后端·flink
狗哥哥3 小时前
前端基础数据中心:从混乱到统一的架构演进
前端·vue.js·架构
码事漫谈3 小时前
C++链表环检测算法完全解析
后端
ShaneD7713 小时前
Spring Boot 实战:基于拦截器与 ThreadLocal 的用户登录校验
后端
JosieBook3 小时前
【Vue】02 Vue技术——搭建 Vue 开发框架:在VS Code中创建一个Vue项目
前端·javascript·vue.js