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 + 请求体加密/解密插件为敏感数据传输提供了强有力的安全保障。通过在网关层统一处理加密解密,我们可以:
核心要点:
- 统一处理:在网关层统一加密解密,业务服务无感知
- 灵活配置:支持按路径配置加密策略
- 密钥管理:支持密钥动态生成和轮换
- 日志脱敏:避免敏感信息泄露到日志
适用场景:
- 用户注册/登录(身份证、手机号)
- 支付接口(银行卡号、CVV)
- 企业 API(商业机密)
- 医疗系统(病历信息)
注意事项:
- 生产环境请使用复杂密钥,并定期轮换
- 密钥不要硬编码,建议使用密钥管理系统
- 考虑使用 HTTPS + 请求体加密双重保障
- 做好性能测试,加密解密会带来一定性能损耗
如果本文对你有帮助,欢迎关注「服务端技术精选」公众号,获取更多后端技术干货。
互动题:
- 在你的项目中,如何处理敏感数据传输安全?
- 加密解密会带来多少性能损耗?如何优化?
- 如何设计一个支持多租户的加密方案?
欢迎在评论区分享你的想法和经验,我们一起交流学习!
标题:Spring Cloud Gateway + 请求体加密/解密插件:敏感数据(如身份证)传输全程加密
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/09/1772950966991.html
公众号:服务端技术精选
评论
0 评论