pom.xml
XML
<?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>
<groupId>com.example</groupId>
<artifactId>api-gateway</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.example.gateway.ApiGatewayApplication</mainClass>
<javaVersion>11</javaVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>
验签拦截
java
package com.example.gateway.filter;
import com.example.gateway.util.SignatureUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Component
public class SignatureGlobalFilter implements GlobalFilter, Ordered {
private static final String SIGNATURE_HEADER = "X-Signature";
private static final String TIMESTAMP_HEADER = "X-Timestamp";
private static final String NONCE_HEADER = "X-Nonce";
private static final String APP_ID_HEADER = "X-App-Id";
private static final long TIMESTAMP_VALIDITY = 5 * 60 * 1000;
private final RedisTemplate<String, String> redisTemplate;
public SignatureGlobalFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String signature = getHeader(request, SIGNATURE_HEADER);
String timestamp = getHeader(request, TIMESTAMP_HEADER);
String nonce = getHeader(request, NONCE_HEADER);
String appId = getHeader(request, APP_ID_HEADER);
// 基础参数校验
if (!validateBasicParams(signature, timestamp, nonce, appId)) {
return setErrorResponse(exchange, HttpStatus.BAD_REQUEST, "Missing required headers");
}
// 时间戳校验
if (!validateTimestamp(timestamp)) {
return setErrorResponse(exchange, HttpStatus.BAD_REQUEST, "Invalid timestamp");
}
// 防重放攻击校验
if (!validateNonce(nonce, timestamp)) {
return setErrorResponse(exchange, HttpStatus.BAD_REQUEST, "Duplicate request");
}
// 签名验证
if (!validateSignature(request, signature, appId)) {
return setErrorResponse(exchange, HttpStatus.UNAUTHORIZED, "Invalid signature");
}
// 记录nonce到Redis,防止重放
String nonceKey = "nonce:" + appId + ":" + nonce;
redisTemplate.opsForValue().set(nonceKey, timestamp, java.time.Duration.ofMinutes(10));
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 高优先级
}
private String getHeader(ServerHttpRequest request, String headerName) {
return request.getHeaders().getFirst(headerName);
}
private boolean validateBasicParams(String signature, String timestamp,
String nonce, String appId) {
return signature != null && !signature.isEmpty() &&
timestamp != null && !timestamp.isEmpty() &&
nonce != null && !nonce.isEmpty() &&
appId != null && !appId.isEmpty();
}
private boolean validateTimestamp(String timestampStr) {
try {
long timestamp = Long.parseLong(timestampStr);
long currentTime = System.currentTimeMillis();
return Math.abs(currentTime - timestamp) <= TIMESTAMP_VALIDITY;
} catch (NumberFormatException e) {
return false;
}
}
private boolean validateNonce(String nonce, String timestamp) {
String nonceKey = "nonce:" + nonce;
return !Boolean.TRUE.equals(redisTemplate.hasKey(nonceKey));
}
private boolean validateSignature(ServerHttpRequest request,
String receivedSignature,
String appId) {
try {
String appSecret = getAppSecret(appId);
if (appSecret == null) {
return false;
}
String dataToSign = buildDataToSign(request);
String calculatedSignature = SignatureUtil.calculateSignature(dataToSign, appSecret);
return SignatureUtil.safeEquals(calculatedSignature, receivedSignature);
} catch (Exception e) {
return false;
}
}
private String buildDataToSign(ServerHttpRequest request) {
StringBuilder sb = new StringBuilder();
sb.append(request.getMethod()).append("\n");
sb.append(request.getURI().getPath()).append("\n");
sb.append(getHeader(request, TIMESTAMP_HEADER)).append("\n");
sb.append(getHeader(request, NONCE_HEADER)).append("\n");
sb.append(getHeader(request, APP_ID_HEADER)).append("\n");
// 添加查询参数
request.getQueryParams().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
String key = entry.getKey();
String value = entry.getValue().get(0);
sb.append(key).append("=").append(value).append("\n");
});
return sb.toString();
}
private String getAppSecret(String appId) {
// 从Redis或数据库获取应用密钥
String redisKey = "app_secret:" + appId;
return redisTemplate.opsForValue().get(redisKey);
}
private Mono<Void> setErrorResponse(ServerWebExchange exchange,
HttpStatus status, String message) {
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
String jsonResponse = String.format(
"{\"code\":%d,\"message\":\"%s\",\"timestamp\":%d}",
status.value(), message, System.currentTimeMillis()
);
byte[] bytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
return exchange.getResponse().writeWith(Mono.just(
exchange.getResponse().bufferFactory().wrap(bytes)
));
}
}
java
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/ApiGatewayApplication.java title=网关应用主入口 entrypoint=true runnable=true project_final_file=false>
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/filter/SignatureGlobalFilter.java title=全局签名验证过滤器 entrypoint=false runnable=false project_final_file=false>
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/util/SignatureUtil.java title=签名工具类 entrypoint=false runnable=false project_final_file=false>
package com.example.gateway.util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class SignatureUtil {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String calculateSignature(String data, String secret) {
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(signatureBytes);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate signature", e);
}
}
public static boolean safeEquals(String a, String b) {
if (a == null || b == null) {
return false;
}
byte[] aBytes = a.getBytes(StandardCharsets.UTF_8);
byte[] bBytes = b.getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(aBytes, bBytes);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
public static String generateNonce() {
byte[] randomBytes = new byte[16];
new java.security.SecureRandom().nextBytes(randomBytes);
return bytesToHex(randomBytes);
}
}
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/filter/RateLimitFilter.java title=限流过滤器 entrypoint=false runnable=false project_final_file=false>
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
@Component
public class RateLimitFilter implements GlobalFilter, Ordered {
private final RedisTemplate<String, String> redisTemplate;
public RateLimitFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String appId = exchange.getRequest().getHeaders().getFirst("X-App-Id");
String clientIp = getClientIp(exchange);
String rateLimitKey = "rate_limit:" + appId + ":" + clientIp;
// 使用Lua脚本实现原子性限流
String luaScript =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) " +
"local current = redis.call('GET', key) " +
"if current then " +
" if tonumber(current) >= limit then " +
" return 0 " +
" else " +
" redis.call('INCR', key) " +
" return 1 " +
" end " +
"else " +
" redis.call('SET', key, 1, 'EX', window) " +
" return 1 " +
"end";
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
List<String> keys = Arrays.asList(rateLimitKey);
Long result = redisTemplate.execute(script, keys, "100", "60"); // 100请求/分钟
if (result == 0) {
return setErrorResponse(exchange, HttpStatus.TOO_MANY_REQUESTS, "Rate limit exceeded");
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -2; // 在签名验证之前执行
}
private String getClientIp(ServerWebExchange exchange) {
String xForwardedFor = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
}
private Mono<Void> setErrorResponse(ServerWebExchange exchange,
HttpStatus status, String message) {
exchange.getResponse().setStatusCode(status);
String jsonResponse = String.format(
"{\"code\":%d,\"message\":\"%s\"}",
status.value(), message
);
byte[] bytes = jsonResponse.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return exchange.getResponse().writeWith(Mono.just(
exchange.getResponse().bufferFactory().wrap(bytes)
));
}
}
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/config/RedisConfig.java title=Redis配置类 entrypoint=false runnable=false project_final_file=false>
package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
<code_end>
<code_start project_name=java_api_gateway filename=src/main/java/com/example/gateway/config/GatewayConfig.java title=网关路由配置 entrypoint=false runnable=false project_final_file=false>
package com.example.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.uri("http://localhost:8081"))
.route("order-service", r -> r.path("/api/orders/**")
.uri("http://localhost:8082"))
.route("product-service", r -> r.path("/api/products/**")
.uri("http://localhost:8083"))
.route("public-api", r -> r.path("/api/public/**")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:8084"))
.build();
}
}
<code_end>
<code_start project_name=java_api_gateway filename=src/main/resources/application.yml title=应用配置文件 entrypoint=false runnable=false project_final_file=true>
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: false
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-methods: "*"
allowed-headers: "*"
redis:
host: localhost
port: 6379
database: 0
timeout: 2000ms
logging:
level:
com.example.gateway: DEBUG
<code_end>
结论
该API网关案例提供完整的微服务安全防护机制:
-
全局签名验证:基于Spring Cloud Gateway的GlobalFilter实现统一验签
-
防重放攻击:使用Redis存储nonce防止请求重复提交
-
限流保护:实现基于应用ID和IP地址的分布式限流
-
路由转发:支持多服务动态路由和负载均衡
-
安全配置:包含CORS跨域支持和响应头去重
-
高性能设计:采用响应式编程模型和Lua脚本原子操作