Spring Cloud Gateway + 请求体加密/解密插件:敏感数据(如身份证)传输全程加密

引言:数据安全的痛点

公司的用户注册接口被黑客抓包分析,导致大量用户的身份证、手机号等敏感信息泄露。虽然数据库是加密的,但传输过程中却是明文,成为了安全漏洞。

敏感数据传输安全是每个系统都必须重视的问题。无论是用户的身份证、银行卡号,还是企业的商业机密,一旦在传输过程中被截获,后果不堪设想。

Spring Cloud Gateway + 请求体加密/解密插件是解决这个问题的利器。通过在网关层统一处理加密解密,我们可以实现敏感数据的全程加密传输,让黑客即使截获了数据包,也无法获取真实内容。


一、为什么需要传输加密?

1.1 常见的安全隐患

┌─────────────────────────────────────────────────────────────┐
│                    数据传输安全隐患                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐         ┌──────────┐         ┌──────────┐   │
│  │   客户端  │  ───>  │  网络传输  │  ───>  │  服务端   │   │
│  └──────────┘         └──────────┘         └──────────┘   │
│        │                    │                    │         │
│        │                    │                    │         │
│        ▼                    ▼                    ▼         │
│   ┌─────────┐          ┌─────────┐          ┌─────────┐   │
│   │ 明文传输 │          │ 中间人   │          │ 日志泄露 │   │
│   │ 风险    │          │ 攻击    │          │ 风险    │   │
│   └─────────┘          └─────────┘          └─────────┘   │
│                                                             │
│  风险点:                                                   │
│  1. 客户端明文存储敏感信息                                   │
│  2. 网络传输被抓包分析                                       │
│  3. 服务端日志记录明文数据                                   │
│  4. 中间人攻击(MITM)                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 传输加密的必要性

场景风险解决方案
用户注册身份证、手机号明文传输请求体加密
支付接口银行卡号、CVV 泄露端到端加密
企业 API商业机密被窃取双向证书认证 + 加密
日志记录敏感信息明文存储脱敏处理

1.3 加密方案对比

方案优点缺点适用场景
HTTPS简单易用只能加密传输通道通用场景
请求体加密应用层加密,更灵活需要额外开发高敏感数据
端到端加密最高安全性复杂度高金融、医疗
字段级加密细粒度控制实现复杂部分字段敏感

二、Spring Cloud Gateway 加密/解密架构

2.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│              加密/解密架构设计                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐                                           │
│  │    客户端    │                                           │
│  └──────┬──────┘                                           │
│         │ 1. 加密请求体                                      │
│         │    {                                              │
│         │      "data": "加密后的数据"                        │
│         │    }                                              │
│         ▼                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Spring Cloud Gateway                    │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │         请求解密过滤器                        │   │   │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐     │   │   │
│  │  │  │ 接收请求 │─>│ 解密数据 │─>│ 转发请求 │     │   │   │
│  │  │  └─────────┘  └─────────┘  └─────────┘     │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  │                                                        │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │         响应加密过滤器                        │   │   │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐     │   │   │
│  │  │  │ 接收响应 │─>│ 加密数据 │─>│ 返回客户端│     │   │   │
│  │  │  └─────────┘  └─────────┘  └─────────┘     │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────┘   │
│         │                                                   │
│         │ 2. 明文请求                                       │
│         ▼                                                   │
│  ┌─────────────┐                                           │
│  │   微服务    │                                           │
│  └─────────────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 核心组件

┌─────────────────────────────────────────────────────────────┐
│              核心组件设计                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              加密/解密工具类                         │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐              │   │
│  │  │ AES加密 │ │ RSA加密 │ │ 混合加密 │              │   │
│  │  └─────────┘ └─────────┘ └─────────┘              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Gateway过滤器                           │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐              │   │
│  │  │ 解密过滤器│ │ 加密过滤器│ │ 配置属性 │              │   │
│  │  └─────────┘ └─────────┘ └─────────┘              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              密钥管理                                │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐              │   │
│  │  │ 密钥生成 │ │ 密钥存储 │ │ 密钥轮换 │              │   │
│  │  └─────────┘ └─────────┘ └─────────┘              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

三、Spring Cloud Gateway 实现加密/解密

3.1 项目依赖

<dependencies>
    <!-- Spring Cloud Gateway -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
    <!-- Spring Boot Starter Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- FastJSON -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.40</version>
    </dependency>
    
    <!-- Commons Codec -->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>
</dependencies>

3.2 加密/解密工具类

@Component
@Slf4j
public class CryptoUtil {

    private static final String ALGORITHM = "AES";
    private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
    private static final String CHARSET = "UTF-8";

    @Value("${crypto.aes.key:1234567890123456}")
    private String aesKey;

    /**
     * AES加密
     */
    public String encrypt(String content) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(CHARSET), ALGORITHM);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(content.getBytes(CHARSET));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            log.error("加密失败", e);
            throw new RuntimeException("加密失败", e);
        }
    }

    /**
     * AES解密
     */
    public String decrypt(String encryptedContent) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(aesKey.getBytes(CHARSET), ALGORITHM);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(encryptedContent);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, CHARSET);
        } catch (Exception e) {
            log.error("解密失败", e);
            throw new RuntimeException("解密失败", e);
        }
    }

    /**
     * 生成随机密钥
     */
    public String generateKey() {
        byte[] keyBytes = new byte[16];
        new SecureRandom().nextBytes(keyBytes);
        return Base64.getEncoder().encodeToString(keyBytes);
    }
}

3.3 请求解密过滤器

@Component
@Order(-1)
@Slf4j
public class RequestDecryptFilter implements GlobalFilter {

    @Autowired
    private CryptoUtil cryptoUtil;

    @Autowired
    private CryptoProperties cryptoProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 检查是否需要解密
        if (!needDecrypt(request)) {
            return chain.filter(exchange);
        }

        log.info("开始解密请求:{}", request.getURI());

        // 获取请求体
        return DataBufferUtils.join(request.getBody())
                .flatMap(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);

                    String body = new String(bytes, StandardCharsets.UTF_8);
                    log.debug("原始请求体:{}", body);

                    try {
                        // 解析JSON
                        JSONObject jsonObject = JSON.parseObject(body);
                        String encryptedData = jsonObject.getString("data");

                        if (encryptedData == null || encryptedData.isEmpty()) {
                            return Mono.error(new RuntimeException("请求体格式错误,缺少data字段"));
                        }

                        // 解密数据
                        String decryptedData = cryptoUtil.decrypt(encryptedData);
                        log.debug("解密后数据:{}", decryptedData);

                        // 构建新的请求体
                        JSONObject decryptedJson = JSON.parseObject(decryptedData);
                        
                        // 添加解密标记
                        ServerHttpRequest mutatedRequest = request.mutate()
                                .header("X-Decrypted", "true")
                                .build();

                        // 构建新的请求体
                        byte[] newBody = decryptedJson.toJSONString().getBytes(StandardCharsets.UTF_8);

                        // 重新构建请求
                        ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(mutatedRequest) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return Flux.just(exchange.getResponse().bufferFactory().wrap(newBody));
                            }

                            @Override
                            public HttpHeaders getHeaders() {
                                HttpHeaders headers = new HttpHeaders();
                                headers.putAll(super.getHeaders());
                                headers.setContentLength(newBody.length);
                                return headers;
                            }
                        };

                        return chain.filter(exchange.mutate().request(requestDecorator).build());

                    } catch (Exception e) {
                        log.error("解密请求失败", e);
                        return Mono.error(new RuntimeException("解密请求失败:" + e.getMessage()));
                    }
                });
    }

    /**
     * 判断是否需要解密
     */
    private boolean needDecrypt(ServerHttpRequest request) {
        String path = request.getURI().getPath();
        String contentType = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);

        // 只处理POST/PUT请求
        if (!HttpMethod.POST.equals(request.getMethod()) && 
            !HttpMethod.PUT.equals(request.getMethod())) {
            return false;
        }

        // 只处理JSON请求
        if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
            return false;
        }

        // 检查是否在白名单中
        return cryptoProperties.getDecryptPaths().stream()
                .anyMatch(path::startsWith);
    }
}

3.4 响应加密过滤器

@Component
@Order(-2)
@Slf4j
public class ResponseEncryptFilter implements GlobalFilter {

    @Autowired
    private CryptoUtil cryptoUtil;

    @Autowired
    private CryptoProperties cryptoProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 检查是否需要加密
        if (!needEncrypt(request)) {
            return chain.filter(exchange);
        }

        log.info("开始加密响应:{}", request.getURI());

        // 包装响应
        ServerHttpResponse originalResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();

        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        DataBufferUtils.release(dataBuffer);

                        String responseBody = new String(content, StandardCharsets.UTF_8);
                        log.debug("原始响应体:{}", responseBody);

                        try {
                            // 加密响应数据
                            String encryptedData = cryptoUtil.encrypt(responseBody);
                            
                            // 构建加密后的响应
                            JSONObject result = new JSONObject();
                            result.put("data", encryptedData);
                            result.put("encrypted", true);
                            result.put("timestamp", System.currentTimeMillis());

                            String encryptedResponse = result.toJSONString();
                            log.debug("加密后响应体:{}", encryptedResponse);

                            byte[] encryptedBytes = encryptedResponse.getBytes(StandardCharsets.UTF_8);
                            
                            // 更新Content-Length
                            getHeaders().setContentLength(encryptedBytes.length);
                            
                            return bufferFactory.wrap(encryptedBytes);

                        } catch (Exception e) {
                            log.error("加密响应失败", e);
                            throw new RuntimeException("加密响应失败", e);
                        }
                    }));
                }
                return super.writeWith(body);
            }
        };

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

    /**
     * 判断是否需要加密
     */
    private boolean needEncrypt(ServerHttpRequest request) {
        String path = request.getURI().getPath();
        
        // 检查是否在白名单中
        return cryptoProperties.getEncryptPaths().stream()
                .anyMatch(path::startsWith);
    }
}

3.5 配置属性类

@Data
@Component
@ConfigurationProperties(prefix = "crypto")
public class CryptoProperties {

    /**
     * 是否启用加密
     */
    private boolean enabled = true;

    /**
     * AES密钥
     */
    private String aesKey = "1234567890123456";

    /**
     * 需要解密的路径
     */
    private List<String> decryptPaths = Arrays.asList("/api/");

    /**
     * 需要加密的路径
     */
    private List<String> encryptPaths = Arrays.asList("/api/");

    /**
     * 排除的路径
     */
    private List<String> excludePaths = Arrays.asList("/api/public/");

}

3.6 配置文件

server:
  port: 8080

spring:
  application:
    name: gateway-crypto-demo
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
        
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1

# 加密配置
crypto:
  enabled: true
  aes-key: "1234567890123456"  # 生产环境请使用复杂密钥
  decrypt-paths:
    - "/api/user/"
    - "/api/order/"
  encrypt-paths:
    - "/api/user/"
    - "/api/order/"
  exclude-paths:
    - "/api/public/"

logging:
  level:
    com.example.gateway: debug

四、客户端加密/解密示例

4.1 JavaScript 客户端

// crypto.js
const CryptoJS = require('crypto-js');

const AES_KEY = '1234567890123456'; // 与后端保持一致

/**
 * AES加密
 */
function encrypt(data) {
    const encrypted = CryptoJS.AES.encrypt(
        JSON.stringify(data),
        CryptoJS.enc.Utf8.parse(AES_KEY),
        {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
        }
    );
    return encrypted.toString();
}

/**
 * AES解密
 */
function decrypt(encryptedData) {
    const decrypted = CryptoJS.AES.decrypt(
        encryptedData,
        CryptoJS.enc.Utf8.parse(AES_KEY),
        {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
        }
    );
    return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
}

/**
 * 发送加密请求
 */
async function sendEncryptedRequest(url, data) {
    const encryptedData = encrypt(data);
    
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ data: encryptedData })
    });
    
    const result = await response.json();
    
    if (result.encrypted) {
        return decrypt(result.data);
    }
    
    return result;
}

// 使用示例
const userData = {
    idCard: '110101199001011234',
    phone: '13800138000',
    name: '张三'
};

sendEncryptedRequest('/api/user/register', userData)
    .then(response => {
        console.log('注册成功:', response);
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

4.2 Java 客户端

@Component
public class CryptoClient {

    @Autowired
    private CryptoUtil cryptoUtil;

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 发送加密请求
     */
    public <T> T sendEncryptedRequest(String url, Object requestData, Class<T> responseType) {
        // 加密请求数据
        String encryptedData = cryptoUtil.encrypt(JSON.toJSONString(requestData));
        
        // 构建请求体
        JSONObject requestBody = new JSONObject();
        requestBody.put("data", encryptedData);
        
        // 发送请求
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity = new HttpEntity<>(requestBody.toJSONString(), headers);
        
        ResponseEntity<String> response = restTemplate.exchange(
                url,
                HttpMethod.POST,
                entity,
                String.class
        );
        
        // 解析响应
        JSONObject responseBody = JSON.parseObject(response.getBody());
        
        if (responseBody.getBooleanValue("encrypted")) {
            // 解密响应数据
            String decryptedData = cryptoUtil.decrypt(responseBody.getString("data"));
            return JSON.parseObject(decryptedData, responseType);
        }
        
        return JSON.parseObject(responseBody.toJSONString(), responseType);
    }
}

五、最佳实践

5.1 密钥管理

@Service
@Slf4j
public class KeyManagementService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "crypto:key:";
    private static final long KEY_EXPIRE_DAYS = 30;

    /**
     * 生成新密钥
     */
    public String generateNewKey(String keyId) {
        String newKey = generateRandomKey();
        
        // 存储到Redis
        String redisKey = KEY_PREFIX + keyId;
        redisTemplate.opsForValue().set(redisKey, newKey, KEY_EXPIRE_DAYS, TimeUnit.DAYS);
        
        log.info("生成新密钥:keyId={}", keyId);
        return newKey;
    }

    /**
     * 获取当前密钥
     */
    public String getCurrentKey(String keyId) {
        String redisKey = KEY_PREFIX + keyId;
        String key = redisTemplate.opsForValue().get(redisKey);
        
        if (key == null) {
            // 密钥不存在,生成新密钥
            key = generateNewKey(keyId);
        }
        
        return key;
    }

    /**
     * 轮换密钥
     */
    public void rotateKey(String keyId) {
        String oldKey = getCurrentKey(keyId);
        String newKey = generateNewKey(keyId);
        
        // 保留旧密钥一段时间,用于解密旧数据
        String oldKeyRedisKey = KEY_PREFIX + keyId + ":old";
        redisTemplate.opsForValue().set(oldKeyRedisKey, oldKey, 7, TimeUnit.DAYS);
        
        log.info("密钥轮换完成:keyId={}", keyId);
    }

    private String generateRandomKey() {
        byte[] keyBytes = new byte[16];
        new SecureRandom().nextBytes(keyBytes);
        return Base64.getEncoder().encodeToString(keyBytes);
    }
}

5.2 敏感字段脱敏

@Component
public class DataMaskingUtil {

    /**
     * 身份证号脱敏
     */
    public String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 8) {
            return idCard;
        }
        return idCard.substring(0, 4) + "****" + idCard.substring(idCard.length() - 4);
    }

    /**
     * 手机号脱敏
     */
    public String maskPhone(String phone) {
        if (phone == null || phone.length() < 7) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }

    /**
     * 银行卡号脱敏
     */
    public String maskBankCard(String bankCard) {
        if (bankCard == null || bankCard.length() < 8) {
            return bankCard;
        }
        return bankCard.substring(0, 4) + " **** **** " + bankCard.substring(bankCard.length() - 4);
    }

    /**
     * 姓名脱敏
     */
    public String maskName(String name) {
        if (name == null || name.isEmpty()) {
            return name;
        }
        if (name.length() == 2) {
            return "*" + name.substring(1);
        }
        return name.substring(0, 1) + "*" + name.substring(name.length() - 1);
    }
}

5.3 日志脱敏处理

@Component
@Slf4j
public class MaskingLogFilter implements GlobalFilter, Ordered {

    @Autowired
    private DataMaskingUtil maskingUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 记录请求日志时脱敏
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    logRequest(exchange.getRequest());
                });
    }

    private void logRequest(ServerHttpRequest request) {
        String path = request.getURI().getPath();
        String query = request.getURI().getQuery();
        
        // 对查询参数脱敏
        if (query != null) {
            query = maskSensitiveParams(query);
        }
        
        log.info("请求: {}?{}", path, query);
    }

    private String maskSensitiveParams(String query) {
        // 对敏感参数脱敏
        return query.replaceAll("(idCard=)[^&]*", "$1****")
                   .replaceAll("(phone=)[^&]*", "$1****")
                   .replaceAll("(bankCard=)[^&]*", "$1****");
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

六、总结

Spring Cloud Gateway + 请求体加密/解密插件为敏感数据传输提供了强有力的安全保障。通过在网关层统一处理加密解密,我们可以:

核心要点:

  1. 统一处理:在网关层统一加密解密,业务服务无感知
  2. 灵活配置:支持按路径配置加密策略
  3. 密钥管理:支持密钥动态生成和轮换
  4. 日志脱敏:避免敏感信息泄露到日志

适用场景:

  • 用户注册/登录(身份证、手机号)
  • 支付接口(银行卡号、CVV)
  • 企业 API(商业机密)
  • 医疗系统(病历信息)

注意事项:

  1. 生产环境请使用复杂密钥,并定期轮换
  2. 密钥不要硬编码,建议使用密钥管理系统
  3. 考虑使用 HTTPS + 请求体加密双重保障
  4. 做好性能测试,加密解密会带来一定性能损耗

如果本文对你有帮助,欢迎关注「服务端技术精选」公众号,获取更多后端技术干货。


互动题:

  1. 在你的项目中,如何处理敏感数据传输安全?
  2. 加密解密会带来多少性能损耗?如何优化?
  3. 如何设计一个支持多租户的加密方案?

欢迎在评论区分享你的想法和经验,我们一起交流学习!


标题:Spring Cloud Gateway + 请求体加密/解密插件:敏感数据(如身份证)传输全程加密
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/09/1772950966991.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消