网关整合验签

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网关案例提供完整的微服务安全防护机制:

  1. 全局签名验证:基于Spring Cloud Gateway的GlobalFilter实现统一验签

  2. 防重放攻击:使用Redis存储nonce防止请求重复提交

  3. 限流保护:实现基于应用ID和IP地址的分布式限流

  4. 路由转发:支持多服务动态路由和负载均衡

  5. 安全配置:包含CORS跨域支持和响应头去重

  6. 高性能设计:采用响应式编程模型和Lua脚本原子操作

相关推荐
hnjzsyjyj5 小时前
洛谷 P12141:[蓝桥杯 2025 省 A] 红黑树
数据结构·蓝桥杯·二叉树
fei_sun5 小时前
【总结】数据结构---排序
数据结构
程序员卡卡西6 小时前
2025年下半年软考高级系统架构师题目和答案
系统架构
茗鹤APS和MES7 小时前
APS高级计划排程:汽车零部件厂生产排产的智慧之选
大数据·制造·精益生产制造·aps高级排程系统
蒙特卡洛的随机游走8 小时前
Spark的persist和cache
大数据·分布式·spark
蒙特卡洛的随机游走8 小时前
Spark 中 distribute by、sort by、cluster by 深度解析
大数据·分布式·spark
梦里不知身是客118 小时前
Spark中的宽窄依赖-宽窄巷子
大数据·分布式·spark
-指短琴长-8 小时前
数据结构进阶——红黑树
数据结构
Croa-vo8 小时前
PayPal OA 全流程复盘|题型体验 + 成绩反馈 + 通关经验
数据结构·经验分享·算法·面试·职场和发展