Spring Cloud Gateway 安全验证完整解决方案

1. 架构设计

请求处理流程-4ec98476

2. Maven 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>gateway-security</artifactId>
    <version>1.0.0</version>
    <name>Gateway Security Module</name>
    <description>
        Spring Cloud Gateway 安全验证模块
        - 支持 RSA 签名验证
        - 支持 RSA 数据加解密
        - 支持 V1(仅签名) 和 V2(签名+加密) 两种模式
    </description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
        <fastjson2.version>2.0.40</fastjson2.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- ==================== 核心依赖 ==================== -->
        
        <!-- Spring Cloud Gateway - 响应式网关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- Redis Reactive - 响应式Redis客户端,用于密钥缓存 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

        <!-- ==================== 工具依赖 ==================== -->
        
        <!-- FastJson2 - 高性能JSON处理 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>

        <!-- Commons Codec - Base64编解码 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <!-- Lombok - 减少样板代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- ==================== 测试依赖 ==================== -->
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3. 配置文件

3.1 application.yml

server:
  port: 8080

spring:
  application:
    name: gateway-security
  
  # Redis 配置 - 用于缓存渠道密钥信息
  data:
    redis:
      host: localhost
      port: 6379
      password: 
      database: 0
      timeout: 3000ms
      lettuce:
        pool:
          max-active: 8      # 最大连接数
          max-idle: 8        # 最大空闲连接
          min-idle: 2        # 最小空闲连接
          max-wait: -1ms     # 获取连接最大等待时间

  cloud:
    gateway:
      # 路由配置
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**

# 安全配置
security:
  # 白名单路径 - 这些路径不进行安全校验
  skip-urls:
    - /api/public/**
    - /api/health/**
    - /api/doc/**
    - /swagger-ui/**
    - /v3/api-docs/**
  
  # 密钥缓存配置
  cache:
    # 缓存前缀
    key-prefix: "gateway:channel:"
    # 缓存过期时间(秒)
    expire-seconds: 3600

# 日志配置
logging:
  level:
    com.example.gateway: DEBUG
    org.springframework.cloud.gateway: INFO

3.2 SecurityProperties.java - 配置属性类

package com.example.gateway.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 安全配置属性类
 * 
 * <p>从配置文件中读取安全相关的配置项,包括:
 * <ul>
 *   <li>白名单URL列表</li>
 *   <li>缓存配置</li>
 * </ul>
 * 
 * <p>使用示例:
 * <pre>
 * security:
 *   skip-urls:
 *     - /api/public/**
 *   cache:
 *     key-prefix: "gateway:channel:"
 *     expire-seconds: 3600
 * </pre>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {

    /**
     * 白名单URL列表
     * <p>匹配这些路径的请求将跳过安全验证,直接放行
     * <p>支持 Ant 风格的路径匹配:
     * <ul>
     *   <li>? - 匹配单个字符</li>
     *   <li>* - 匹配0个或多个字符(不包含路径分隔符)</li>
     *   <li>** - 匹配0个或多个目录</li>
     * </ul>
     */
    private List<String> skipUrls = new ArrayList<>();

    /**
     * 缓存配置
     */
    private CacheConfig cache = new CacheConfig();

    /**
     * 缓存配置内部类
     */
    @Data
    public static class CacheConfig {
        
        /**
         * Redis 缓存键前缀
         * <p>完整的缓存键格式:{keyPrefix}{channelCode}
         */
        private String keyPrefix = "gateway:channel:";

        /**
         * 缓存过期时间(秒)
         * <p>建议设置为 1 小时(3600秒),
         * 在密钥更新时需要主动清除缓存
         */
        private long expireSeconds = 3600;
    }
}

4. 数据模型

4.1 ChannelDataVo.java - 渠道数据模型

package com.example.gateway.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.io.Serializable;

/**
 * 渠道数据模型
 * 
 * <p>存储每个接入渠道(如:APP、H5、小程序等)的密钥信息。
 * 每个渠道拥有独立的密钥对,实现渠道隔离和权限控制。
 * 
 * <h3>密钥体系说明:</h3>
 * <pre>
 * ┌─────────────────────────────────────────────────────────────┐
 * │                      签名密钥对                              │
 * │  signPublicKey  - 后端持有,用于验证前端签名                   │
 * │  (signPrivateKey - 前端持有,用于对请求签名,后端不存储)        │
 * ├─────────────────────────────────────────────────────────────┤
 * │                      数据加密密钥对                           │
 * │  dataPublicKey  - 后端持有,用于加密响应数据给前端              │
 * │  dataPrivateKey - 后端持有,用于解密前端发送的加密数据          │
 * └─────────────────────────────────────────────────────────────┘
 * </pre>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChannelDataVo implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 渠道编码
     * <p>唯一标识一个接入渠道,如:
     * <ul>
     *   <li>APP_IOS - iOS客户端</li>
     *   <li>APP_ANDROID - Android客户端</li>
     *   <li>H5 - H5页面</li>
     *   <li>MINI_PROGRAM - 微信小程序</li>
     * </ul>
     */
    private String code;

    /**
     * 渠道名称
     * <p>渠道的可读名称,用于日志和监控展示
     */
    private String name;

    /**
     * 签名公钥(Base64编码)
     * 
     * <p><b>用途:</b>验证前端请求的签名
     * <p><b>算法:</b>SHA256withRSA
     * <p><b>流程:</b>
     * <ol>
     *   <li>前端使用对应的私钥对请求参数签名</li>
     *   <li>后端使用此公钥验证签名的有效性</li>
     *   <li>验签通过则继续处理,否则拒绝请求</li>
     * </ol>
     * 
     * <p><b>安全说明:</b>
     * <ul>
     *   <li>公钥可以安全地存储在数据库/配置中心</li>
     *   <li>对应的私钥由前端安全保管,后端不应存储</li>
     * </ul>
     */
    private String signPublicKey;

    /**
     * 数据加密公钥(Base64编码)
     * 
     * <p><b>用途:</b>加密响应数据返回给前端
     * <p><b>算法:</b>RSA/ECB/PKCS1Padding
     * <p><b>流程:</b>
     * <ol>
     *   <li>后端处理完业务逻辑,得到响应数据</li>
     *   <li>使用此公钥加密响应数据</li>
     *   <li>前端使用对应的私钥解密获取明文</li>
     * </ol>
     * 
     * <p><b>注意:</b>这是前端数据密钥对的公钥,
     * 对应的私钥由前端保管用于解密响应
     */
    private String dataPublicKey;

    /**
     * 数据解密私钥(Base64编码)
     * 
     * <p><b>用途:</b>解密前端发送的加密业务数据
     * <p><b>算法:</b>RSA/ECB/PKCS1Padding
     * <p><b>流程:</b>
     * <ol>
     *   <li>前端使用后端数据公钥加密 businessBody</li>
     *   <li>后端使用此私钥解密得到明文</li>
     *   <li>将明文传递给下游微服务处理</li>
     * </ol>
     * 
     * <p><b>安全说明:</b>
     * <ul>
     *   <li>私钥必须安全存储,建议使用加密存储或密钥管理服务</li>
     *   <li>定期轮换密钥,并同步更新前端配置</li>
     * </ul>
     */
    private String dataPrivateKey;

    /**
     * 渠道状态
     * <p>1-启用,0-禁用
     * <p>禁用的渠道将拒绝所有请求
     */
    private Integer status;

    /**
     * 判断渠道是否启用
     * @return true-启用,false-禁用
     */
    public boolean isEnabled() {
        return status != null && status == 1;
    }
}

4.2 GatewayRequest.java - 统一请求模型

package com.example.gateway.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一网关请求模型
 * 
 * <p>所有经过网关的请求都必须遵循此格式。
 * 这种统一格式便于安全校验和日志追踪。
 * 
 * <h3>请求体示例(V1模式-仅签名):</h3>
 * <pre>
 * {
 *   "code": "APP_IOS",
 *   "timestamp": 1703318400000,
 *   "nonce": "abc123xyz",
 *   "businessBody": "{\"userId\":\"10001\",\"action\":\"query\"}",
 *   "sign": "Base64编码的签名值"
 * }
 * </pre>
 * 
 * <h3>请求体示例(V2模式-签名+加密):</h3>
 * <pre>
 * {
 *   "code": "APP_IOS",
 *   "timestamp": 1703318400000,
 *   "nonce": "abc123xyz",
 *   "businessBody": "RSA加密后的Base64字符串",
 *   "sign": "Base64编码的签名值"
 * }
 * </pre>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GatewayRequest {

    /**
     * 渠道编码
     * <p>标识请求来源,用于获取对应的密钥配置
     * <p><b>必填</b>
     */
    private String code;

    /**
     * 时间戳(毫秒)
     * <p>请求发起时的时间戳,用于防止重放攻击
     * <p>后端会校验时间戳与当前时间的差值,超过阈值(如5分钟)则拒绝
     * <p><b>必填</b>
     */
    private Long timestamp;

    /**
     * 随机字符串
     * <p>每次请求生成的唯一随机串,配合时间戳防止重放攻击
     * <p>后端可将 nonce 存入 Redis(设置过期时间),检测重复请求
     * <p><b>必填</b>,建议长度 16-32 字符
     */
    private String nonce;

    /**
     * 业务数据
     * <p>实际的业务请求参数,格式为 JSON 字符串
     * <p><b>V1模式:</b>明文 JSON
     * <p><b>V2模式:</b>RSA 加密后的 Base64 字符串
     * <p><b>必填</b>
     */
    private String businessBody;

    /**
     * 签名
     * <p>对请求参数的数字签名,Base64 编码
     * 
     * <p><b>签名规则:</b>
     * <ol>
     *   <li>将参与签名的字段按 key 字典序排序</li>
     *   <li>拼接成 key1=value1&key2=value2 格式</li>
     *   <li>使用 SHA256withRSA 算法和前端私钥签名</li>
     *   <li>将签名结果 Base64 编码</li>
     * </ol>
     * 
     * <p><b>参与签名的字段:</b>businessBody, code, nonce, timestamp
     * <p><b>必填</b>
     */
    private String sign;
}

4.3 GatewayResponse.java - 统一响应模型

package com.example.gateway.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一网关响应模型
 * 
 * <p>网关返回给前端的统一响应格式。
 * 
 * <h3>响应示例(V1模式-明文):</h3>
 * <pre>
 * {
 *   "code": 200,
 *   "message": "success",
 *   "data": {
 *     "userId": "10001",
 *     "userName": "张三"
 *   }
 * }
 * </pre>
 * 
 * <h3>响应示例(V2模式-加密):</h3>
 * <pre>
 * {
 *   "code": 200,
 *   "message": "success",
 *   "data": "RSA加密后的Base64字符串"
 * }
 * </pre>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GatewayResponse<T> {

    /**
     * 响应码
     * <ul>
     *   <li>200 - 成功</li>
     *   <li>400 - 请求参数错误</li>
     *   <li>401 - 未授权(签名验证失败)</li>
     *   <li>403 - 禁止访问(渠道被禁用)</li>
     *   <li>500 - 服务器内部错误</li>
     * </ul>
     */
    private Integer code;

    /**
     * 响应消息
     */
    private String message;

    /**
     * 响应数据
     * <p>V1模式:明文对象
     * <p>V2模式:加密后的字符串
     */
    private T data;

    /**
     * 构建成功响应
     */
    public static <T> GatewayResponse<T> success(T data) {
        return GatewayResponse.<T>builder()
                .code(200)
                .message("success")
                .data(data)
                .build();
    }

    /**
     * 构建错误响应
     */
    public static <T> GatewayResponse<T> error(Integer code, String message) {
        return GatewayResponse.<T>builder()
                .code(code)
                .message(message)
                .build();
    }
}

5. RSA 工具类

package com.example.gateway.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.TreeMap;

/**
 * RSA 加解密工具类
 * 
 * <p>提供完整的 RSA 签名验证和数据加解密功能,支持:
 * <ul>
 *   <li>SHA256withRSA 签名与验签</li>
 *   <li>RSA 分段加解密(支持任意长度数据)</li>
 *   <li>签名内容的标准化构建</li>
 * </ul>
 * 
 * <h3>RSA 密钥长度与加解密限制:</h3>
 * <table border="1">
 *   <tr><th>密钥长度</th><th>最大加密长度</th><th>解密块大小</th></tr>
 *   <tr><td>1024 bit</td><td>117 bytes</td><td>128 bytes</td></tr>
 *   <tr><td>2048 bit</td><td>245 bytes</td><td>256 bytes</td></tr>
 * </table>
 * <p>本工具类使用分段加解密,突破长度限制。
 * 
 * <h3>使用示例:</h3>
 * <pre>{@code
 * // 验签
 * Map<String, Object> params = new HashMap<>();
 * params.put("code", "APP_IOS");
 * params.put("timestamp", 1703318400000L);
 * params.put("businessBody", "...");
 * boolean valid = RsaUtils.verify(params, publicKey, sign);
 * 
 * // 解密
 * String plainText = RsaUtils.decrypt(cipherText, privateKey);
 * 
 * // 加密
 * String cipherText = RsaUtils.encrypt(plainText, publicKey);
 * }</pre>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Slf4j
public final class RsaUtils {

    // ==================== 常量定义 ====================

    /**
     * 签名算法:SHA256withRSA
     * <p>使用 SHA-256 计算摘要,再用 RSA 私钥加密
     */
    public static final String SIGN_ALGORITHM = "SHA256withRSA";

    /**
     * 密钥算法:RSA
     */
    public static final String KEY_ALGORITHM = "RSA";

    /**
     * 加密转换方式
     * <p>ECB 模式 + PKCS1 填充,这是 RSA 的默认且最常用的组合
     */
    public static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";

    /**
     * RSA 密钥长度(bit)
     * <p>2048 位是目前推荐的安全长度
     */
    public static final int KEY_SIZE = 2048;

    /**
     * RSA 最大加密明文长度(2048位密钥)
     * <p>计算公式:密钥长度/8 - 11(PKCS1填充占用11字节)
     * <p>2048/8 - 11 = 245 字节
     */
    private static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11;

    /**
     * RSA 最大解密密文长度(2048位密钥)
     * <p>等于密钥长度/8 = 256 字节
     */
    private static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8;

    // ==================== 私有构造函数 ====================

    /**
     * 私有构造函数,防止实例化
     */
    private RsaUtils() {
        throw new UnsupportedOperationException("Utility class cannot be instantiated");
    }

    // ==================== 签名相关方法 ====================

    /**
     * 构建签名内容
     * 
     * <p>将请求参数按照 key 的字典序排序后,拼接成标准格式的签名原文。
     * 这是保证前后端签名一致的关键步骤。
     * 
     * <h3>处理规则:</h3>
     * <ol>
     *   <li>使用 TreeMap 自动按 key 字典序排序</li>
     *   <li>排除 sign 字段(签名字段本身不参与签名)</li>
     *   <li>排除 null 和空字符串值</li>
     *   <li>拼接格式:key1=value1&key2=value2</li>
     * </ol>
     * 
     * <h3>示例:</h3>
     * <pre>
     * 输入:{code=APP, timestamp=123, businessBody=data, sign=xxx}
     * 排序:businessBody, code, timestamp(字典序)
     * 输出:businessBody=data&code=APP&timestamp=123
     * </pre>
     *
     * @param params 请求参数Map
     * @return 签名原文字符串
     */
    public static String buildSignContent(Map<String, Object> params) {
        if (params == null || params.isEmpty()) {
            return "";
        }

        // 使用 TreeMap 自动按 key 字典序排序
        Map<String, Object> sortedMap = new TreeMap<>(params);
        
        StringBuilder content = new StringBuilder();
        
        for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            
            // 排除 sign 字段和空值
            if (!"sign".equals(key) && value != null && !"".equals(value.toString())) {
                if (content.length() > 0) {
                    content.append("&");
                }
                content.append(key).append("=").append(value);
            }
        }
        
        return content.toString();
    }

    /**
     * 验证签名
     * 
     * <p>使用公钥验证请求的签名是否有效。这是安全校验的第一道防线。
     * 
     * <h3>验签流程:</h3>
     * <ol>
     *   <li>按照 {@link #buildSignContent} 规则构建签名原文</li>
     *   <li>将公钥字符串转换为 PublicKey 对象</li>
     *   <li>使用 SHA256withRSA 算法验证签名</li>
     * </ol>
     * 
     * <h3>为什么要先验签再解密?(Fail-Fast 原则)</h3>
     * <p>验签的计算成本远低于解密。如果先解密,攻击者可以发送大量
     * 无效请求来消耗服务器的 CPU 资源(DDoS攻击)。先验签可以快速
     * 过滤掉无效请求。
     *
     * @param params       请求参数Map(包含待验证的数据)
     * @param publicKeyStr Base64编码的公钥字符串
     * @param sign         Base64编码的签名字符串
     * @return true-验签成功,false-验签失败
     */
    public static boolean verify(Map<String, Object> params, String publicKeyStr, String sign) {
        if (params == null || publicKeyStr == null || sign == null) {
            log.warn("验签参数不完整: params={}, publicKey={}, sign={}", 
                    params != null, publicKeyStr != null, sign != null);
            return false;
        }

        try {
            // 1. 构建签名原文
            String content = buildSignContent(params);
            log.debug("验签原文: {}", content);

            // 2. 解码公钥
            byte[] keyBytes = Base64.decodeBase64(publicKeyStr);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            PublicKey publicKey = keyFactory.generatePublic(keySpec);

            // 3. 验证签名
            Signature signature = Signature.getInstance(SIGN_ALGORITHM);
            signature.initVerify(publicKey);
            signature.update(content.getBytes(StandardCharsets.UTF_8));

            boolean result = signature.verify(Base64.decodeBase64(sign));
            log.debug("验签结果: {}", result);
            
            return result;

        } catch (Exception e) {
            log.error("验签异常: {}", e.getMessage(), e);
            return false;
        }
    }

    /**
     * 使用私钥对数据签名
     * 
     * <p>生成请求的数字签名,通常由前端调用。后端也可用于测试。
     *
     * @param params        待签名的参数Map
     * @param privateKeyStr Base64编码的私钥字符串
     * @return Base64编码的签名字符串
     * @throws RuntimeException 签名失败时抛出
     */
    public static String sign(Map<String, Object> params, String privateKeyStr) {
        try {
            // 1. 构建签名原文
            String content = buildSignContent(params);
            log.debug("签名原文: {}", content);

            // 2. 解码私钥
            byte[] keyBytes = Base64.decodeBase64(privateKeyStr);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

            // 3. 执行签名
            Signature signature = Signature.getInstance(SIGN_ALGORITHM);
            signature.initSign(privateKey);
            signature.update(content.getBytes(StandardCharsets.UTF_8));

            byte[] signBytes = signature.sign();
            return Base64.encodeBase64String(signBytes);

        } catch (Exception e) {
            throw new RuntimeException("RSA签名失败", e);
        }
    }

    // ==================== 加解密相关方法 ====================

    /**
     * 公钥加密(支持分段加密)
     * 
     * <p>使用公钥对明文进行加密。本方法支持任意长度的数据,
     * 内部会自动进行分段处理。
     * 
     * <h3>使用场景:</h3>
     * <ul>
     *   <li>后端使用前端数据公钥加密响应数据</li>
     *   <li>前端使用后端数据公钥加密请求数据</li>
     * </ul>
     * 
     * <h3>分段加密原理:</h3>
     * <p>RSA 加密的数据长度有限制(2048位密钥最多加密245字节)。
     * 对于超长数据,需要分段加密后拼接。
     *
     * @param content      待加密的明文字符串
     * @param publicKeyStr Base64编码的公钥字符串
     * @return Base64编码的密文字符串
     * @throws RuntimeException 加密失败时抛出
     */
    public static String encrypt(String content, String publicKeyStr) {
        if (content == null || content.isEmpty()) {
            return content;
        }

        try {
            // 1. 解码公钥
            byte[] keyBytes = Base64.decodeBase64(publicKeyStr);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            PublicKey publicKey = keyFactory.generatePublic(keySpec);

            // 2. 初始化加密器
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            // 3. 分段加密
            byte[] data = content.getBytes(StandardCharsets.UTF_8);
            byte[] encryptedData = doFinalWithBlock(cipher, data, MAX_ENCRYPT_BLOCK);

            // 4. Base64 编码
            return Base64.encodeBase64String(encryptedData);

        } catch (Exception e) {
            throw new RuntimeException("RSA加密失败", e);
        }
    }

    /**
     * 私钥解密(支持分段解密)
     * 
     * <p>使用私钥对密文进行解密。本方法支持任意长度的加密数据,
     * 内部会自动进行分段处理。
     * 
     * <h3>使用场景:</h3>
     * <ul>
     *   <li>后端使用后端数据私钥解密请求中的 businessBody</li>
     *   <li>前端使用前端数据私钥解密响应中的 data</li>
     * </ul>
     *
     * @param content       Base64编码的密文字符串
     * @param privateKeyStr Base64编码的私钥字符串
     * @return 解密后的明文字符串
     * @throws RuntimeException 解密失败时抛出
     */
    public static String decrypt(String content, String privateKeyStr) {
        if (content == null || content.isEmpty()) {
            return content;
        }

        try {
            // 1. 解码私钥
            byte[] keyBytes = Base64.decodeBase64(privateKeyStr);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

            // 2. 初始化解密器
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);

            // 3. 分段解密
            byte[] encryptedData = Base64.decodeBase64(content);
            byte[] decryptedData = doFinalWithBlock(cipher, encryptedData, MAX_DECRYPT_BLOCK);

            // 4. 转换为字符串
            return new String(decryptedData, StandardCharsets.UTF_8);

        } catch (Exception e) {
            throw new RuntimeException("RSA解密失败: " + e.getMessage(), e);
        }
    }

    /**
     * 分段处理加解密
     * 
     * <p>将数据按指定块大小分段处理,解决 RSA 长度限制问题。
     *
     * @param cipher    加密/解密器
     * @param data      待处理的数据
     * @param blockSize 每块的最大长度
     * @return 处理后的数据
     * @throws Exception 处理异常
     */
    private static byte[] doFinalWithBlock(Cipher cipher, byte[] data, int blockSize) 
            throws Exception {
        
        int inputLen = data.length;
        int offset = 0;
        
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            while (inputLen > offset) {
                int len = Math.min(inputLen - offset, blockSize);
                byte[] block = cipher.doFinal(data, offset, len);
                out.write(block);
                offset += len;
            }
            return out.toByteArray();
        }
    }

    // ==================== 密钥生成方法 ====================

    /**
     * 生成 RSA 密钥对
     * 
     * <p>生成用于签名/加密的 RSA 密钥对。建议使用 2048 位密钥长度。
     * 
     * <h3>使用示例:</h3>
     * <pre>{@code
     * KeyPair keyPair = RsaUtils.generateKeyPair();
     * String publicKey = Base64.encodeBase64String(keyPair.getPublic().getEncoded());
     * String privateKey = Base64.encodeBase64String(keyPair.getPrivate().getEncoded());
     * }</pre>
     *
     * @return RSA密钥对
     * @throws RuntimeException 生成失败时抛出
     */
    public static KeyPair generateKeyPair() {
        try {
            KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
            generator.initialize(KEY_SIZE, new SecureRandom());
            return generator.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("生成RSA密钥对失败", e);
        }
    }

    /**
     * 生成 Base64 编码的密钥对
     *
     * @return 包含公钥和私钥的字符串数组 [publicKey, privateKey]
     */
    public static String[] generateKeyPairBase64() {
        KeyPair keyPair = generateKeyPair();
        return new String[] {
            Base64.encodeBase64String(keyPair.getPublic().getEncoded()),
            Base64.encodeBase64String(keyPair.getPrivate().getEncoded())
        };
    }
}

6. 渠道数据服务

6.1 ChannelDataService.java - 服务接口

package com.example.gateway.service;

import com.example.gateway.model.ChannelDataVo;
import reactor.core.publisher.Mono;

/**
 * 渠道数据服务接口
 * 
 * <p>提供渠道密钥数据的查询功能,支持多级缓存策略。
 * 
 * <h3>缓存策略:</h3>
 * <ol>
 *   <li>一级缓存:本地缓存(可选,使用 Caffeine)</li>
 *   <li>二级缓存:Redis 分布式缓存</li>
 *   <li>三级存储:数据库持久化存储</li>
 * </ol>
 * 
 * <h3>缓存更新策略:</h3>
 * <ul>
 *   <li>密钥更新时,主动清除相关缓存</li>
 *   <li>设置合理的过期时间,自动失效</li>
 *   <li>支持手动刷新指定渠道的缓存</li>
 * </ul>
 *
 * @author Your Name
 * @since 1.0.0
 */
public interface ChannelDataService {

    /**
     * 根据渠道编码获取渠道数据
     * 
     * <p>优先从缓存获取,缓存未命中则查询数据库并更新缓存。
     * 
     * <h3>查询流程:</h3>
     * <pre>
     * Redis 缓存 ──未命中──▶ 数据库查询 ──▶ 写入缓存 ──▶ 返回结果
     *     │
     *     └──命中──▶ 直接返回
     * </pre>
     *
     * @param code 渠道编码,如 "APP_IOS"
     * @return 渠道数据的 Mono,如果渠道不存在则返回 Mono.empty()
     */
    Mono<ChannelDataVo> getChannelDataByCode(String code);

    /**
     * 刷新指定渠道的缓存
     * 
     * <p>当密钥更新时,调用此方法清除旧缓存。
     * 下次查询时会自动加载新的密钥数据。
     *
     * @param code 渠道编码
     * @return 操作结果的 Mono
     */
    Mono<Boolean> refreshCache(String code);

    /**
     * 验证渠道是否有效
     * 
     * <p>检查渠道是否存在且状态为启用。
     *
     * @param code 渠道编码
     * @return true-有效,false-无效或不存在
     */
    Mono<Boolean> isValidChannel(String code);
}

6.2 ChannelDataServiceImpl.java - 服务实现

package com.example.gateway.service.impl;

import com.alibaba.fastjson2.JSON;
import com.example.gateway.config.SecurityProperties;
import com.example.gateway.model.ChannelDataVo;
import com.example.gateway.service.ChannelDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 渠道数据服务实现类
 * 
 * <p>实现了基于 Redis 的分布式缓存策略,保证集群环境下的一致性。
 * 
 * <h3>实现特点:</h3>
 * <ul>
 *   <li>使用响应式 Redis 客户端,与 WebFlux 无缝集成</li>
 *   <li>缓存未命中时,加载数据并自动填充缓存</li>
 *   <li>支持缓存穿透防护(空值缓存)</li>
 * </ul>
 * 
 * <h3>注意事项:</h3>
 * <ul>
 *   <li>生产环境应替换 loadFromDatabase 方法,接入真实数据库</li>
 *   <li>私钥等敏感数据建议加密存储</li>
 * </ul>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ChannelDataServiceImpl implements ChannelDataService {

    /**
     * 响应式 Redis 模板
     */
    private final ReactiveStringRedisTemplate redisTemplate;

    /**
     * 安全配置属性
     */
    private final SecurityProperties securityProperties;

    /**
     * 模拟数据库存储(生产环境请替换为真实数据库访问)
     * 
     * <p>这里使用 Map 模拟数据库,实际项目中应该:
     * <ul>
     *   <li>使用 R2DBC 进行响应式数据库访问</li>
     *   <li>或使用传统 JPA/MyBatis 配合 subscribeOn(Schedulers.boundedElastic())</li>
     * </ul>
     */
    private static final Map<String, ChannelDataVo> DATABASE = new ConcurrentHashMap<>();

    // 静态初始化模拟数据(仅用于演示)
    static {
        // 注意:以下密钥仅为示例,生产环境请使用真实密钥
        DATABASE.put("APP_IOS", ChannelDataVo.builder()
                .code("APP_IOS")
                .name("iOS客户端")
                .signPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...")  // 替换为真实公钥
                .dataPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...")   // 替换为真实公钥
                .dataPrivateKey("MIIEvgIBADANBgkqhkiG9w0BAQEFAAOCAQ8...")           // 替换为真实私钥
                .status(1)
                .build());

        DATABASE.put("APP_ANDROID", ChannelDataVo.builder()
                .code("APP_ANDROID")
                .name("Android客户端")
                .signPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...")
                .dataPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...")
                .dataPrivateKey("MIIEvgIBADANBgkqhkiG9w0BAQEFAAOCAQ8...")
                .status(1)
                .build());
    }

    /**
     * {@inheritDoc}
     * 
     * <p>实现逻辑:
     * <ol>
     *   <li>生成 Redis 缓存键</li>
     *   <li>尝试从 Redis 获取缓存数据</li>
     *   <li>如果缓存命中,反序列化后返回</li>
     *   <li>如果缓存未命中,从数据库加载并填充缓存</li>
     * </ol>
     */
    @Override
    public Mono<ChannelDataVo> getChannelDataByCode(String code) {
        if (code == null || code.trim().isEmpty()) {
            return Mono.empty();
        }

        String cacheKey = buildCacheKey(code);
        
        return redisTemplate.opsForValue().get(cacheKey)
                // 缓存命中:反序列化
                .map(json -> {
                    log.debug("缓存命中: key={}", cacheKey);
                    return JSON.parseObject(json, ChannelDataVo.class);
                })
                // 缓存未命中:从数据库加载
                .switchIfEmpty(Mono.defer(() -> loadAndCacheChannelData(code, cacheKey)))
                // 验证渠道状态
                .filter(ChannelDataVo::isEnabled)
                .doOnNext(data -> log.debug("获取渠道数据成功: code={}", code))
                .doOnError(e -> log.error("获取渠道数据失败: code={}, error={}", code, e.getMessage()));
    }

    /**
     * 从数据库加载数据并填充缓存
     *
     * @param code     渠道编码
     * @param cacheKey Redis缓存键
     * @return 渠道数据
     */
    private Mono<ChannelDataVo> loadAndCacheChannelData(String code, String cacheKey) {
        return loadFromDatabase(code)
                .flatMap(data -> {
                    // 序列化并写入缓存
                    String json = JSON.toJSONString(data);
                    Duration expireDuration = Duration.ofSeconds(
                            securityProperties.getCache().getExpireSeconds()
                    );
                    
                    return redisTemplate.opsForValue()
                            .set(cacheKey, json, expireDuration)
                            .doOnSuccess(success -> log.debug("缓存写入成功: key={}", cacheKey))
                            .thenReturn(data);
                })
                // 处理空值(防止缓存穿透)
                .switchIfEmpty(Mono.defer(() -> {
                    log.warn("渠道不存在: code={}", code);
                    // 可选:缓存空值,设置较短的过期时间
                    return Mono.empty();
                }));
    }

    /**
     * 从数据库加载渠道数据
     * 
     * <p><b>生产环境请替换此方法的实现!</b>
     * 
     * <p>推荐实现方式:
     * <pre>{@code
     * // 使用 R2DBC
     * return channelRepository.findByCode(code);
     * 
     * // 或使用传统 JPA
     * return Mono.fromCallable(() -> channelRepository.findByCode(code))
     *     .subscribeOn(Schedulers.boundedElastic());
     * }</pre>
     *
     * @param code 渠道编码
     * @return 渠道数据
     */
    private Mono<ChannelDataVo> loadFromDatabase(String code) {
        log.debug("从数据库加载渠道数据: code={}", code);
        
        // 模拟数据库查询(生产环境替换为真实数据库访问)
        ChannelDataVo data = DATABASE.get(code);
        
        if (data != null) {
            return Mono.just(data);
        } else {
            return Mono.empty();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<Boolean> refreshCache(String code) {
        String cacheKey = buildCacheKey(code);
        
        return redisTemplate.delete(cacheKey)
                .map(count -> count > 0)
                .doOnSuccess(deleted -> {
                    if (deleted) {
                        log.info("缓存刷新成功: code={}", code);
                    } else {
                        log.debug("缓存不存在,无需刷新: code={}", code);
                    }
                })
                .doOnError(e -> log.error("缓存刷新失败: code={}, error={}", code, e.getMessage()));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<Boolean> isValidChannel(String code) {
        return getChannelDataByCode(code)
                .map(data -> data != null && data.isEnabled())
                .defaultIfEmpty(false);
    }

    /**
     * 构建 Redis 缓存键
     *
     * @param code 渠道编码
     * @return 完整的缓存键
     */
    private String buildCacheKey(String code) {
        return securityProperties.getCache().getKeyPrefix() + code;
    }
}

7. 安全验证过滤器

package com.example.gateway.filter;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.gateway.config.SecurityProperties;
import com.example.gateway.exception.SecurityException;
import com.example.gateway.model.ChannelDataVo;
import com.example.gateway.model.GatewayResponse;
import com.example.gateway.service.ChannelDataService;
import com.example.gateway.utils.RsaUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * 安全验证全局过滤器
 * 
 * <p>这是网关安全体系的核心组件,负责对所有进入的请求进行安全校验。
 * 
 * <h3>处理流程:</h3>
 * <pre>
 * 请求进入
 *     │
 *     ▼
 * ┌─────────────────┐
 * │  1. 白名单检查   │ ── 匹配 ──▶ 直接放行
 * └─────────────────┘
 *     │ 不匹配
 *     ▼
 * ┌─────────────────┐
 * │  2. 读取请求体   │
 * └─────────────────┘
 *     │
 *     ▼
 * ┌─────────────────┐
 * │  3. 验证签名     │ ── 失败 ──▶ 返回 401
 * │  (Fail-Fast)    │
 * └─────────────────┘
 *     │ 成功
 *     ▼
 * ┌─────────────────┐
 * │  4. 解密数据     │ ── 失败 ──▶ 返回 400
 * │  (仅V2模式)     │
 * └─────────────────┘
 *     │ 成功
 *     ▼
 * ┌─────────────────┐
 * │  5. 重建请求     │
 * │  传递给下游服务  │
 * └─────────────────┘
 * </pre>
 * 
 * <h3>Fail-Fast 原则:</h3>
 * <p>验签的计算成本(仅计算哈希+公钥验证)远低于解密(私钥运算)。
 * 通过先验签,可以快速过滤无效请求,防止攻击者利用大量无效加密数据
 * 消耗服务器资源(DDoS攻击)。
 * 
 * <h3>请求体重建:</h3>
 * <p>在 Spring WebFlux 中,请求体(Request Body)是一个响应式流,
 * 只能被消费一次。为了让下游服务能够正常读取请求体,我们需要:
 * <ol>
 *   <li>读取原始请求体</li>
 *   <li>进行验签和解密处理</li>
 *   <li>将处理后的数据重新封装为新的请求体</li>
 *   <li>使用 ServerHttpRequestDecorator 装饰原请求</li>
 * </ol>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SecurityFilter implements GlobalFilter, Ordered {

    /**
     * 渠道数据服务,用于获取密钥信息
     */
    private final ChannelDataService channelDataService;

    /**
     * 安全配置属性
     */
    private final SecurityProperties securityProperties;

    /**
     * 路径匹配器,支持 Ant 风格的路径表达式
     */
    private final PathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 加密模式请求头名称
     * <p>值为 "v1" 时仅验签,值为 "v2" 时验签并解密
     */
    private static final String ENCRYPT_HEADER = "encrypt";

    /**
     * V2 加密模式标识
     */
    private static final String ENCRYPT_MODE_V2 = "v2";

    /**
     * 渠道编码在 Exchange Attributes 中的存储键
     * <p>用于在响应过滤器中获取渠道信息
     */
    public static final String CHANNEL_DATA_ATTR = "gateway.channel.data";

    /**
     * 加密模式在 Exchange Attributes 中的存储键
     */
    public static final String ENCRYPT_MODE_ATTR = "gateway.encrypt.mode";

    /**
     * 过滤器主逻辑
     * 
     * <p>处理流程:
     * <ol>
     *   <li>白名单检查:匹配则直接放行</li>
     *   <li>读取并缓存请求体</li>
     *   <li>执行安全校验(验签+解密)</li>
     *   <li>重建请求传递给下游</li>
     * </ol>
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // 1. 白名单检查
        if (isSkipUrl(path)) {
            log.debug("白名单放行: path={}", path);
            return chain.filter(exchange);
        }

        // 2. 读取请求体并处理
        return DataBufferUtils.join(request.getBody())
                .flatMap(dataBuffer -> {
                    // 读取请求体内容
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    // 释放 DataBuffer,防止内存泄漏
                    DataBufferUtils.release(dataBuffer);
                    String rawBody = new String(bytes, StandardCharsets.UTF_8);

                    log.debug("收到请求: path={}, body={}", path, rawBody);

                    // 3. 执行安全校验
                    return doSecurityCheck(rawBody, request.getHeaders(), exchange)
                            .flatMap(newBody -> {
                                // 4. 重建请求并传递给下游
                                return chain.filter(buildNewExchange(exchange, newBody));
                            });
                })
                // 处理空请求体的情况
                .switchIfEmpty(Mono.defer(() -> {
                    log.warn("请求体为空: path={}", path);
                    return buildErrorResponse(exchange, HttpStatus.BAD_REQUEST, "请求体不能为空");
                }))
                // 统一异常处理
                .onErrorResume(e -> handleException(exchange, e));
    }

    /**
     * 执行安全校验
     * 
     * <p>核心校验逻辑,包括:
     * <ol>
     *   <li>解析请求参数</li>
     *   <li>获取渠道密钥</li>
     *   <li>验证签名(Fail-Fast)</li>
     *   <li>解密数据(V2模式)</li>
     * </ol>
     *
     * @param rawBody  原始请求体
     * @param headers  请求头
     * @param exchange Web交换对象
     * @return 处理后的请求体(明文)
     */
    private Mono<String> doSecurityCheck(String rawBody, HttpHeaders headers, 
                                          ServerWebExchange exchange) {
        try {
            // 1. 解析请求体
            JSONObject bodyJson = JSON.parseObject(rawBody);
            if (bodyJson == null) {
                return Mono.error(new SecurityException("请求体格式错误", HttpStatus.BAD_REQUEST));
            }

            // 2. 提取核心参数
            String code = bodyJson.getString("code");
            String sign = bodyJson.getString("sign");
            String businessBody = bodyJson.getString("businessBody");
            Long timestamp = bodyJson.getLong("timestamp");
            String nonce = bodyJson.getString("nonce");

            // 3. 参数校验
            if (code == null || sign == null || businessBody == null) {
                return Mono.error(new SecurityException("缺少必要参数", HttpStatus.BAD_REQUEST));
            }

            // 4. 时间戳校验(防重放攻击)
            if (!isValidTimestamp(timestamp)) {
                return Mono.error(new SecurityException("请求已过期", HttpStatus.BAD_REQUEST));
            }

            // 5. 获取加密模式
            String encryptMode = headers.getFirst(ENCRYPT_HEADER);
            
            // 6. 获取渠道密钥并验证
            return channelDataService.getChannelDataByCode(code)
                    .switchIfEmpty(Mono.error(new SecurityException("非法渠道: " + code, HttpStatus.FORBIDDEN)))
                    .flatMap(channelData -> {
                        // 保存渠道信息到 Exchange,供响应过滤器使用
                        exchange.getAttributes().put(CHANNEL_DATA_ATTR, channelData);
                        exchange.getAttributes().put(ENCRYPT_MODE_ATTR, encryptMode);

                        // 7. 验证签名(Fail-Fast)
                        @SuppressWarnings("unchecked")
                        Map<String, Object> paramsMap = bodyJson.toJavaObject(Map.class);
                        boolean verifyResult = RsaUtils.verify(
                                paramsMap,
                                channelData.getSignPublicKey(),
                                sign
                        );

                        if (!verifyResult) {
                            log.warn("签名验证失败: code={}", code);
                            return Mono.error(new SecurityException("签名验证失败", HttpStatus.UNAUTHORIZED));
                        }

                        log.debug("签名验证成功: code={}", code);

                        // 8. 解密数据(仅V2模式)
                        String finalBusinessBody = businessBody;
                        if (ENCRYPT_MODE_V2.equalsIgnoreCase(encryptMode)) {
                            try {
                                finalBusinessBody = RsaUtils.decrypt(
                                        businessBody,
                                        channelData.getDataPrivateKey()
                                );
                                log.debug("数据解密成功: code={}", code);
                            } catch (Exception e) {
                                log.error("数据解密失败: code={}, error={}", code, e.getMessage());
                                return Mono.error(new SecurityException("数据解密失败", HttpStatus.BAD_REQUEST));
                            }
                        }

                        // 9. 替换 businessBody 为明文
                        bodyJson.put("businessBody", finalBusinessBody);
                        
                        return Mono.just(bodyJson.toJSONString());
                    });

        } catch (Exception e) {
            log.error("安全校验异常: {}", e.getMessage(), e);
            return Mono.error(new SecurityException("请求处理失败: " + e.getMessage(), HttpStatus.BAD_REQUEST));
        }
    }

    /**
     * 重建 ServerWebExchange
     * 
     * <p>将处理后的请求体封装到新的请求对象中。
     * 使用 Decorator 模式包装原始请求,只替换 Body 部分。
     *
     * @param exchange 原始Exchange
     * @param newBody  新的请求体内容
     * @return 新的Exchange对象
     */
    private ServerWebExchange buildNewExchange(ServerWebExchange exchange, String newBody) {
        byte[] bodyBytes = newBody.getBytes(StandardCharsets.UTF_8);
        
        // 创建新的 DataBuffer
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bodyBytes);

        // 使用 Decorator 模式重建请求
        ServerHttpRequest decoratedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
            
            /**
             * 重写 getBody 方法,返回新的请求体
             */
            @Override
            public Flux<DataBuffer> getBody() {
                return Flux.just(buffer);
            }

            /**
             * 重写 getHeaders 方法,更新 Content-Length
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.putAll(super.getHeaders());
                // 更新 Content-Length,否则下游服务可能读取不完整
                headers.setContentLength(bodyBytes.length);
                // 移除原有的 Content-Length 头(如果有多个值)
                headers.remove(HttpHeaders.CONTENT_LENGTH);
                headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(bodyBytes.length));
                return headers;
            }
        };

        // 构建新的 Exchange
        return exchange.mutate()
                .request(decoratedRequest)
                .build();
    }

    /**
     * 检查是否为白名单URL
     *
     * @param path 请求路径
     * @return true-在白名单中,false-不在
     */
    private boolean isSkipUrl(String path) {
        return securityProperties.getSkipUrls().stream()
                .anyMatch(pattern -> pathMatcher.match(pattern, path));
    }

    /**
     * 验证时间戳有效性
     * 
     * <p>时间戳与当前时间相差超过5分钟的请求将被拒绝,
     * 这是防止重放攻击的重要手段。
     *
     * @param timestamp 请求时间戳(毫秒)
     * @return true-有效,false-无效
     */
    private boolean isValidTimestamp(Long timestamp) {
        if (timestamp == null) {
            return false;
        }
        
        long currentTime = System.currentTimeMillis();
        long diff = Math.abs(currentTime - timestamp);
        
        // 允许的时间差:5分钟
        long maxDiff = 5 * 60 * 1000;
        
        return diff <= maxDiff;
    }

    /**
     * 统一异常处理
     *
     * @param exchange Web交换对象
     * @param e        异常对象
     * @return Void Mono
     */
    private Mono<Void> handleException(ServerWebExchange exchange, Throwable e) {
        log.error("请求处理异常: path={}, error={}", 
                exchange.getRequest().getURI().getPath(), e.getMessage());

        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        String message = "服务器内部错误";

        if (e instanceof SecurityException se) {
            status = se.getStatus();
            message = se.getMessage();
        }

        return buildErrorResponse(exchange, status, message);
    }

    /**
     * 构建错误响应
     *
     * @param exchange Web交换对象
     * @param status   HTTP状态码
     * @param message  错误消息
     * @return Void Mono
     */
    private Mono<Void> buildErrorResponse(ServerWebExchange exchange, 
                                           HttpStatus status, String message) {
        exchange.getResponse().setStatusCode(status);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        GatewayResponse<Void> response = GatewayResponse.error(status.value(), message);
        String responseBody = JSON.toJSONString(response);

        DataBuffer buffer = exchange.getResponse().bufferFactory()
                .wrap(responseBody.getBytes(StandardCharsets.UTF_8));

        return exchange.getResponse().writeWith(Mono.just(buffer));
    }

    /**
     * 过滤器执行顺序
     * 
     * <p>返回较小的值表示优先执行。
     * 设置为 -100 确保此过滤器在大多数其他过滤器之前执行。
     *
     * @return 顺序值
     */
    @Override
    public int getOrder() {
        return -100;
    }
}

8. 响应加密过滤器

package com.example.gateway.filter;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.gateway.model.ChannelDataVo;
import com.example.gateway.utils.RsaUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * 响应加密全局过滤器
 * 
 * <p>对返回给前端的响应数据进行 RSA 加密,仅在 V2 加密模式下生效。
 * 
 * <h3>处理流程:</h3>
 * <pre>
 * 下游服务响应
 *     │
 *     ▼
 * ┌─────────────────────┐
 * │  检查加密模式        │ ── 非V2 ──▶ 原样返回
 * └─────────────────────┘
 *     │ V2模式
 *     ▼
 * ┌─────────────────────┐
 * │  读取响应体          │
 * └─────────────────────┘
 *     │
 *     ▼
 * ┌─────────────────────┐
 * │  提取 data 字段      │
 * └─────────────────────┘
 *     │
 *     ▼
 * ┌─────────────────────┐
 * │  使用前端公钥加密    │
 * └─────────────────────┘
 *     │
 *     ▼
 * ┌─────────────────────┐
 * │  替换 data 为密文    │
 * └─────────────────────┘
 *     │
 *     ▼
 *   返回加密响应
 * </pre>
 * 
 * <h3>密钥使用说明:</h3>
 * <p>使用前端的数据公钥加密响应数据,前端使用对应的私钥解密。
 * 这样即使响应被中间人截获,没有前端私钥也无法解密。
 * 
 * <h3>注意事项:</h3>
 * <ul>
 *   <li>只加密 data 字段,code 和 message 保持明文</li>
 *   <li>如果 data 为 null,不进行加密处理</li>
 *   <li>加密失败时返回原始响应,保证服务可用性</li>
 * </ul>
 *
 * @author Your Name
 * @since 1.0.0
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ResponseEncryptFilter implements GlobalFilter, Ordered {

    /**
     * V2 加密模式标识
     */
    private static final String ENCRYPT_MODE_V2 = "v2";

    /**
     * 过滤器主逻辑
     * 
     * <p>使用 Decorator 模式包装响应对象,在写入响应时进行加密处理。
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取加密模式
        String encryptMode = exchange.getAttribute(SecurityFilter.ENCRYPT_MODE_ATTR);
        
        // 非V2模式,直接放行
        if (!ENCRYPT_MODE_V2.equalsIgnoreCase(encryptMode)) {
            return chain.filter(exchange);
        }

        // 获取渠道数据
        ChannelDataVo channelData = exchange.getAttribute(SecurityFilter.CHANNEL_DATA_ATTR);
        if (channelData == null) {
            log.warn("渠道数据为空,跳过响应加密");
            return chain.filter(exchange);
        }

        // 使用 Decorator 包装响应
        ServerHttpResponseDecorator decoratedResponse = new EncryptingResponseDecorator(
                exchange.getResponse(),
                channelData
        );

        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }

    /**
     * 加密响应装饰器
     * 
     * <p>继承 ServerHttpResponseDecorator,重写 writeWith 方法实现响应加密。
     */
    private class EncryptingResponseDecorator extends ServerHttpResponseDecorator {

        private final ChannelDataVo channelData;

        public EncryptingResponseDecorator(ServerHttpResponse delegate, 
                                           ChannelDataVo channelData) {
            super(delegate);
            this.channelData = channelData;
        }

        /**
         * 重写响应写入方法
         * 
         * <p>拦截响应数据,进行加密处理后再写入。
         */
        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            // 只处理 Flux 类型的响应体
            if (body instanceof Flux) {
                Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                
                return super.writeWith(
                        // 缓冲所有数据块
                        DataBufferUtils.join(fluxBody)
                                .map(dataBuffer -> {
                                    // 读取原始响应内容
                                    byte[] content = new byte[dataBuffer.readableByteCount()];
                                    dataBuffer.read(content);
                                    DataBufferUtils.release(dataBuffer);
                                    
                                    String originalResponse = new String(content, StandardCharsets.UTF_8);
                                    log.debug("原始响应: {}", originalResponse);

                                    // 加密处理
                                    String encryptedResponse = encryptResponseData(originalResponse);
                                    log.debug("加密响应: {}", encryptedResponse);

                                    // 更新 Content-Length
                                    byte[] newContent = encryptedResponse.getBytes(StandardCharsets.UTF_8);
                                    HttpHeaders headers = getDelegate().getHeaders();
                                    headers.setContentLength(newContent.length);

                                    // 返回新的 DataBuffer
                                    DataBufferFactory bufferFactory = getDelegate().bufferFactory();
                                    return bufferFactory.wrap(newContent);
                                })
                );
            }
            
            return super.writeWith(body);
        }

        /**
         * 加密响应数据
         * 
         * <p>只加密 data 字段,保持响应结构不变。
         *
         * @param jsonBody 原始响应JSON
         * @return 加密后的响应JSON
         */
        private String encryptResponseData(String jsonBody) {
            try {
                JSONObject json = JSON.parseObject(jsonBody);
                if (json == null) {
                    return jsonBody;
                }

                // 获取 data 字段
                Object data = json.get("data");
                if (data == null) {
                    log.debug("响应 data 为空,跳过加密");
                    return jsonBody;
                }

                // 将 data 序列化为字符串
                String dataStr;
                if (data instanceof String) {
                    dataStr = (String) data;
                } else {
                    dataStr = JSON.toJSONString(data);
                }

                // 使用前端数据公钥加密
                String encryptedData = RsaUtils.encrypt(dataStr, channelData.getDataPublicKey());

                // 替换 data 字段
                json.put("data", encryptedData);

                log.debug("响应加密成功: code={}", channelData.getCode());
                return json.toJSONString();

            } catch (Exception e) {
                log.error("响应加密失败,返回原始数据: error={}", e.getMessage(), e);
                return jsonBody;
            }
        }
    }

    /**
     * 过滤器执行顺序
     * 
     * <p>设置为 -2,确保在 NettyWriteResponseFilter(-1) 之前执行,
     * 这样才能正确拦截和修改响应。
     *
     * @return 顺序值
     */
    @Override
    public int getOrder() {
        return -2;
    }
}

9. 异常处理

9.1 SecurityException.java - 安全异常

package com.example.gateway.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;

/**
 * 安全校验异常
 * 
 * <p>在安全校验过程中抛出的异常,携带 HTTP 状态码和错误消息。
 *
 * @author Your Name
 * @since 1.0.0
 */
@Getter
public class SecurityException extends RuntimeException {

    /**
     * HTTP 状态码
     */
    private final HttpStatus status;

    /**
     * 构造函数
     *
     * @param message 错误消息
     * @param status  HTTP状态码
     */
    public SecurityException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }

    /**
     * 构造函数(带原因)
     *
     * @param message 错误消息
     * @param status  HTTP状态码
     * @param cause   原始异常
     */
    public SecurityException(String message, HttpStatus status, Throwable cause) {
        super(message, cause);
        this.status = status;
    }
}

9.2 GlobalExceptionHandler.java - 全局异常处理

package com.example.gateway.exception;

import com.alibaba.fastjson2.JSON;
import com.example.gateway.model.GatewayResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * 全局异常处理器
 * 
 * <p>捕获网关中所有未处理的异常,统一返回标准格式的错误响应。
 *
 * @author Your Name
 * @since 1.0.0
 */
@Slf4j
@Order(-1)
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();

        // 如果响应已经提交,无法再修改
        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        // 确定HTTP状态码和错误消息
        HttpStatus status;
        String message;

        if (ex instanceof SecurityException se) {
            status = se.getStatus();
            message = se.getMessage();
        } else if (ex instanceof ResponseStatusException rse) {
            status = HttpStatus.valueOf(rse.getStatusCode().value());
            message = rse.getReason();
        } else {
            status = HttpStatus.INTERNAL_SERVER_ERROR;
            message = "服务器内部错误";
            log.error("未处理异常: ", ex);
        }

        log.warn("请求异常: path={}, status={}, message={}", 
                exchange.getRequest().getURI().getPath(), status, message);

        // 构建错误响应
        response.setStatusCode(status);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        GatewayResponse<Void> errorResponse = GatewayResponse.error(status.value(), message);
        String responseBody = JSON.toJSONString(errorResponse);

        DataBuffer buffer = response.bufferFactory()
                .wrap(responseBody.getBytes(StandardCharsets.UTF_8));

        return response.writeWith(Mono.just(buffer));
    }
}

10. 测试示例

10.1 RsaUtilsTest.java - 工具类测试

package com.example.gateway.utils;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

/**
 * RSA 工具类测试
 *
 * @author Your Name
 * @since 1.0.0
 */
class RsaUtilsTest {

    private String publicKey;
    private String privateKey;

    @BeforeEach
    void setUp() {
        // 生成测试用密钥对
        String[] keyPair = RsaUtils.generateKeyPairBase64();
        publicKey = keyPair[0];
        privateKey = keyPair[1];
    }

    @Test
    @DisplayName("测试签名原文构建 - 应按字典序排序")
    void testBuildSignContent() {
        Map<String, Object> params = new HashMap<>();
        params.put("code", "APP_IOS");
        params.put("timestamp", 1703318400000L);
        params.put("businessBody", "{\"userId\":\"10001\"}");
        params.put("nonce", "abc123");
        params.put("sign", "xxxxx");  // sign 应被排除

        String content = RsaUtils.buildSignContent(params);

        // 验证排序:businessBody < code < nonce < timestamp
        assertTrue(content.startsWith("businessBody="));
        assertTrue(content.contains("&code="));
        assertTrue(content.contains("&nonce="));
        assertTrue(content.endsWith("&timestamp=1703318400000"));
        assertFalse(content.contains("sign="));
    }

    @Test
    @DisplayName("测试签名与验签")
    void testSignAndVerify() {
        Map<String, Object> params = new HashMap<>();
        params.put("code", "APP_IOS");
        params.put("timestamp", System.currentTimeMillis());
        params.put("businessBody", "{\"userId\":\"10001\"}");

        // 签名
        String sign = RsaUtils.sign(params, privateKey);
        assertNotNull(sign);

        // 验签
        boolean result = RsaUtils.verify(params, publicKey, sign);
        assertTrue(result);
    }

    @Test
    @DisplayName("测试加密与解密 - 短文本")
    void testEncryptAndDecryptShortText() {
        String plainText = "Hello, RSA!";

        // 加密
        String cipherText = RsaUtils.encrypt(plainText, publicKey);
        assertNotNull(cipherText);
        assertNotEquals(plainText, cipherText);

        // 解密
        String decryptedText = RsaUtils.decrypt(cipherText, privateKey);
        assertEquals(plainText, decryptedText);
    }

    @Test
    @DisplayName("测试加密与解密 - 长文本(测试分段加密)")
    void testEncryptAndDecryptLongText() {
        // 构建超过 245 字节的长文本
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100; i++) {
            sb.append("这是一段测试文本,用于测试RSA分段加密功能。");
        }
        String plainText = sb.toString();

        // 加密
        String cipherText = RsaUtils.encrypt(plainText, publicKey);
        assertNotNull(cipherText);

        // 解密
        String decryptedText = RsaUtils.decrypt(cipherText, privateKey);
        assertEquals(plainText, decryptedText);
    }

    @Test
    @DisplayName("测试验签失败 - 数据被篡改")
    void testVerifyFailOnTamperedData() {
        Map<String, Object> params = new HashMap<>();
        params.put("code", "APP_IOS");
        params.put("timestamp", System.currentTimeMillis());
        params.put("businessBody", "{\"userId\":\"10001\"}");

        // 签名
        String sign = RsaUtils.sign(params, privateKey);

        // 篡改数据
        params.put("businessBody", "{\"userId\":\"10002\"}");

        // 验签应该失败
        boolean result = RsaUtils.verify(params, publicKey, sign);
        assertFalse(result);
    }
}

10.2 SecurityFilterTest.java - 过滤器测试

package com.example.gateway.filter;

import com.alibaba.fastjson2.JSON;
import com.example.gateway.config.SecurityProperties;
import com.example.gateway.model.ChannelDataVo;
import com.example.gateway.model.GatewayRequest;
import com.example.gateway.service.ChannelDataService;
import com.example.gateway.utils.RsaUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

/**
 * 安全过滤器测试类
 * 
 * <p>测试覆盖场景:</p>
 * <ul>
 *   <li>白名单路径放行</li>
 *   <li>签名验证成功/失败</li>
 *   <li>V2 模式数据解密</li>
 *   <li>请求时效性校验</li>
 *   <li>渠道有效性校验</li>
 *   <li>异常情况处理</li>
 * </ul>
 *
 * @author Gateway Team
 * @version 1.0
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("SecurityFilter 安全过滤器测试")
class SecurityFilterTest {

    @Mock
    private ChannelDataService channelDataService;

    @Mock
    private GatewayFilterChain filterChain;

    private SecurityFilter securityFilter;
    private SecurityProperties securityProperties;

    // 测试用密钥对
    private KeyPair signKeyPair;
    private KeyPair dataKeyPair;
    private ChannelDataVo testChannelData;

    @BeforeEach
    void setUp() {
        // 初始化配置
        securityProperties = new SecurityProperties();
        securityProperties.setEnabled(true);
        securityProperties.setSignTimeout(300000L);
        securityProperties.setSkipPaths(Arrays.asList("/api/health", "/api/public/**"));

        // 创建过滤器实例
        securityFilter = new SecurityFilter(securityProperties, channelDataService);

        // 生成测试密钥对
        signKeyPair = RsaUtils.generateKeyPair();
        dataKeyPair = RsaUtils.generateKeyPair();

        // 构建测试渠道数据
        testChannelData = ChannelDataVo.builder()
                .code("TEST_CHANNEL")
                .name("测试渠道")
                .signPublicKey(RsaUtils.getPublicKeyString(signKeyPair))
                .dataPrivateKey(RsaUtils.getPrivateKeyString(dataKeyPair))
                .dataPublicKey(RsaUtils.getPublicKeyString(dataKeyPair))
                .status(1)
                .securityLevel(2)
                .build();

        // Mock filterChain 默认行为
        when(filterChain.filter(any())).thenReturn(Mono.empty());
    }

    // ==================== 辅助方法 ====================

    /**
     * 创建有效的网关请求
     */
    private GatewayRequest createValidRequest(String businessBody) {
        Map<String, Object> params = new HashMap<>();
        params.put("code", "TEST_CHANNEL");
        params.put("timestamp", System.currentTimeMillis());
        params.put("nonce", "test-nonce-" + System.nanoTime());
        params.put("businessBody", businessBody);

        String sign = RsaUtils.sign(params, RsaUtils.getPrivateKeyString(signKeyPair));

        return GatewayRequest.builder()
                .code("TEST_CHANNEL")
                .timestamp((Long) params.get("timestamp"))
                .nonce((String) params.get("nonce"))
                .businessBody(businessBody)
                .sign(sign)
                .build();
    }

    /**
     * 创建 V2 模式的加密请求
     */
    private GatewayRequest createV2EncryptedRequest(String plainBusinessBody) {
        // 使用后端数据公钥加密业务数据
        String encryptedBody = RsaUtils.encrypt(
                plainBusinessBody, 
                RsaUtils.getPublicKeyString(dataKeyPair)
        );

        Map<String, Object> params = new HashMap<>();
        params.put("code", "TEST_CHANNEL");
        params.put("timestamp", System.currentTimeMillis());
        params.put("nonce", "test-nonce-" + System.nanoTime());
        params.put("businessBody", encryptedBody);

        String sign = RsaUtils.sign(params, RsaUtils.getPrivateKeyString(signKeyPair));

        return GatewayRequest.builder()
                .code("TEST_CHANNEL")
                .timestamp((Long) params.get("timestamp"))
                .nonce((String) params.get("nonce"))
                .businessBody(encryptedBody)
                .sign(sign)
                .build();
    }

    /**
     * 创建 MockServerWebExchange
     */
    private MockServerWebExchange createExchange(String path, String body, String encryptMode) {
        MockServerHttpRequest.BodyBuilder builder = MockServerHttpRequest
                .post(path)
                .contentType(MediaType.APPLICATION_JSON);
        
        if (encryptMode != null) {
            builder.header("encrypt", encryptMode);
        }

        MockServerHttpRequest request = builder.body(body);
        return MockServerWebExchange.from(request);
    }

    // ==================== 白名单测试 ====================

    @Nested
    @DisplayName("白名单路径测试")
    class WhitelistTests {

        @Test
        @DisplayName("精确匹配白名单路径应该直接放行")
        void shouldSkipExactWhitelistPath() {
            // Given
            MockServerWebExchange exchange = createExchange("/api/health", "", null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(exchange);
            verifyNoInteractions(channelDataService);
        }

        @Test
        @DisplayName("通配符匹配白名单路径应该直接放行")
        void shouldSkipWildcardWhitelistPath() {
            // Given
            MockServerWebExchange exchange = createExchange("/api/public/info", "", null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(exchange);
        }

        @Test
        @DisplayName("安全校验禁用时应该直接放行")
        void shouldSkipWhenSecurityDisabled() {
            // Given
            securityProperties.setEnabled(false);
            MockServerWebExchange exchange = createExchange("/api/secure/data", "", null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(exchange);
        }
    }

    // ==================== 参数校验测试 ====================

    @Nested
    @DisplayName("参数校验测试")
    class ParamValidationTests {

        @Test
        @DisplayName("空请求体应该返回 400 错误")
        void shouldReturn400WhenBodyIsEmpty() {
            // Given
            MockServerWebExchange exchange = createExchange("/api/secure/data", "", null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.BAD_REQUEST);
            verify(filterChain, never()).filter(any());
        }

        @Test
        @DisplayName("无效 JSON 应该返回 400 错误")
        void shouldReturn400WhenJsonInvalid() {
            // Given
            MockServerWebExchange exchange = createExchange(
                    "/api/secure/data", 
                    "invalid json {", 
                    null
            );

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.BAD_REQUEST);
        }

        @Test
        @DisplayName("缺少必要参数应该返回 400 错误")
        void shouldReturn400WhenMissingRequiredParams() {
            // Given - 缺少 sign 字段
            String body = """
                    {
                        "code": "TEST_CHANNEL",
                        "timestamp": %d,
                        "nonce": "test-nonce",
                        "businessBody": "test"
                    }
                    """.formatted(System.currentTimeMillis());
            
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.BAD_REQUEST);
        }
    }

    // ==================== 时效性校验测试 ====================

    @Nested
    @DisplayName("时效性校验测试")
    class TimestampValidationTests {

        @Test
        @DisplayName("过期的请求应该返回 401 错误")
        void shouldReturn401WhenRequestExpired() {
            // Given - 创建一个 10 分钟前的请求
            GatewayRequest expiredRequest = GatewayRequest.builder()
                    .code("TEST_CHANNEL")
                    .timestamp(System.currentTimeMillis() - 600000L) // 10 分钟前
                    .nonce("test-nonce")
                    .businessBody("test")
                    .sign("fake-sign")
                    .build();

            String body = JSON.toJSONString(expiredRequest);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }

        @Test
        @DisplayName("未来时间的请求应该返回 401 错误")
        void shouldReturn401WhenRequestFromFuture() {
            // Given - 创建一个 10 分钟后的请求
            GatewayRequest futureRequest = GatewayRequest.builder()
                    .code("TEST_CHANNEL")
                    .timestamp(System.currentTimeMillis() + 600000L)
                    .nonce("test-nonce")
                    .businessBody("test")
                    .sign("fake-sign")
                    .build();

            String body = JSON.toJSONString(futureRequest);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }
    }

    // ==================== 渠道校验测试 ====================

    @Nested
    @DisplayName("渠道校验测试")
    class ChannelValidationTests {

        @Test
        @DisplayName("无效渠道码应该返回 401 错误")
        void shouldReturn401WhenChannelNotFound() {
            // Given
            when(channelDataService.getChannelDataByCode("UNKNOWN_CHANNEL"))
                    .thenReturn(Mono.empty());

            GatewayRequest request = GatewayRequest.builder()
                    .code("UNKNOWN_CHANNEL")
                    .timestamp(System.currentTimeMillis())
                    .nonce("test-nonce")
                    .businessBody("test")
                    .sign("fake-sign")
                    .build();

            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }

        @Test
        @DisplayName("已禁用的渠道应该返回 401 错误")
        void shouldReturn401WhenChannelDisabled() {
            // Given
            ChannelDataVo disabledChannel = ChannelDataVo.builder()
                    .code("TEST_CHANNEL")
                    .status(0) // 禁用状态
                    .build();
            
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(disabledChannel));

            GatewayRequest request = createValidRequest("test");
            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }
    }

    // ==================== 签名验证测试 ====================

    @Nested
    @DisplayName("签名验证测试")
    class SignatureValidationTests {

        @Test
        @DisplayName("有效签名应该通过验证并放行")
        void shouldPassWithValidSignature() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            GatewayRequest request = createValidRequest("{\"userId\": 123}");
            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(any());
        }

        @Test
        @DisplayName("无效签名应该返回 401 错误")
        void shouldReturn401WithInvalidSignature() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            // 创建一个带错误签名的请求
            GatewayRequest request = GatewayRequest.builder()
                    .code("TEST_CHANNEL")
                    .timestamp(System.currentTimeMillis())
                    .nonce("test-nonce")
                    .businessBody("test")
                    .sign("invalid-signature-base64")
                    .build();

            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
            verify(filterChain, never()).filter(any());
        }

        @Test
        @DisplayName("签名被篡改应该返回 401 错误")
        void shouldReturn401WhenSignatureTampered() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            // 创建有效请求,然后修改 businessBody
            GatewayRequest request = createValidRequest("{\"userId\": 123}");
            request.setBusinessBody("{\"userId\": 456}"); // 篡改数据

            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, null);

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }
    }

    // ==================== V2 加密模式测试 ====================

    @Nested
    @DisplayName("V2 加密模式测试")
    class V2EncryptionTests {

        @Test
        @DisplayName("V2 模式应该成功解密业务数据")
        void shouldDecryptBusinessBodyInV2Mode() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            String plainBusinessBody = "{\"userId\": 123, \"action\": \"query\"}";
            GatewayRequest request = createV2EncryptedRequest(plainBusinessBody);
            String body = JSON.toJSONString(request);
            
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, "v2");

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(any());
        }

        @Test
        @DisplayName("V2 模式解密失败应该返回 401 错误")
        void shouldReturn401WhenDecryptFails() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            // 创建一个带无法解密的 businessBody 的请求
            Map<String, Object> params = new HashMap<>();
            params.put("code", "TEST_CHANNEL");
            params.put("timestamp", System.currentTimeMillis());
            params.put("nonce", "test-nonce");
            params.put("businessBody", "not-a-valid-encrypted-base64");

            String sign = RsaUtils.sign(params, RsaUtils.getPrivateKeyString(signKeyPair));

            GatewayRequest request = GatewayRequest.builder()
                    .code("TEST_CHANNEL")
                    .timestamp((Long) params.get("timestamp"))
                    .nonce((String) params.get("nonce"))
                    .businessBody("not-a-valid-encrypted-base64")
                    .sign(sign)
                    .build();

            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, "v2");

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            assertThat(exchange.getResponse().getStatusCode())
                    .isEqualTo(HttpStatus.UNAUTHORIZED);
        }

        @Test
        @DisplayName("V1 模式不应该尝试解密")
        void shouldNotDecryptInV1Mode() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            // 明文的 businessBody
            GatewayRequest request = createValidRequest("{\"userId\": 123}");
            String body = JSON.toJSONString(request);
            
            MockServerWebExchange exchange = createExchange("/api/secure/data", body, "v1");

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(any());
        }
    }

    // ==================== 集成测试 ====================

    @Nested
    @DisplayName("完整流程集成测试")
    class IntegrationTests {

        @Test
        @DisplayName("完整 V1 流程:验签通过后应该放行")
        void shouldCompleteV1FlowSuccessfully() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            String businessData = """
                    {
                        "userId": 12345,
                        "action": "queryBalance",
                        "params": {
                            "accountType": "savings"
                        }
                    }
                    """;
            
            GatewayRequest request = createValidRequest(businessData);
            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/account/balance", body, "v1");

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(any());
            verify(channelDataService).getChannelDataByCode("TEST_CHANNEL");
        }

        @Test
        @DisplayName("完整 V2 流程:验签并解密后应该放行")
        void shouldCompleteV2FlowSuccessfully() {
            // Given
            when(channelDataService.getChannelDataByCode("TEST_CHANNEL"))
                    .thenReturn(Mono.just(testChannelData));

            String plainBusinessData = """
                    {
                        "userId": 12345,
                        "action": "transfer",
                        "params": {
                            "toAccount": "6222021234567890",
                            "amount": 1000.00
                        }
                    }
                    """;
            
            GatewayRequest request = createV2EncryptedRequest(plainBusinessData);
            String body = JSON.toJSONString(request);
            MockServerWebExchange exchange = createExchange("/api/account/transfer", body, "v2");

            // When
            Mono<Void> result = securityFilter.filter(exchange, filterChain);

            // Then
            StepVerifier.create(result)
                    .verifyComplete();

            verify(filterChain).filter(any());
        }
    }
}