架构之API网关

架构之API网关

引言

“API网关是微服务架构的守门人,它不仅是流量的入口,更是服务治理、安全防护、流量控制的核心枢纽。随着服务化在公司内的迅速推进,网关逐步成为应用程序暴露在外网的标准解决方案。”

在微服务架构中,随着服务数量的快速增长,我们面临着一个严峻的挑战:如何让客户端能够方便地访问分散在各个微服务中的功能?如何统一管理外部访问?如何确保系统的安全性和稳定性?API网关作为微服务架构的基础设施,正是解决这些问题的关键组件。

本文将深入探讨API网关的核心价值、设计原则、实现策略以及最佳实践,帮助构建高效、安全、可扩展的微服务入口层。

API网关的核心理念

微服务架构下的接入挑战

在传统的单体应用中,所有的功能都集中在一个应用中,客户端只需要知道一个访问地址即可。但在微服务架构下,情况变得复杂:

客户端

用户服务

订单服务

商品服务

支付服务

库存服务

10.0.1.10:8080

10.0.1.20:8080

10.0.1.30:8080

10.0.1.40:8080

10.0.1.50:8080

接入挑战

服务发现复杂

协议多样化

安全认证分散

流量控制困难

监控统计分散

API网关的价值定位

API网关作为微服务架构的统一入口,承担着多重角色:

API网关

统一接入

协议转换

安全防护

流量控制

服务治理

监控分析

统一域名

统一端口

服务路由

HTTP/HTTPS

WebSocket

gRPC转换

GraphQL

身份认证

权限控制

数据加密

防攻击

限流熔断

负载均衡

缓存加速

重试机制

服务发现

健康检查

灰度发布

版本管理

访问日志

性能监控

错误统计

业务分析

API网关的核心功能

1. 统一接入与路由

智能路由策略
// Spring Cloud Gateway路由配置
@Configuration
public class GatewayRoutingConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // 用户服务路由
            .route("user-service", r -> r
                .path("/api/v1/users/**")
                .and().method(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT)
                .filters(f -> f
                    .stripPrefix(2)
                    .addRequestHeader("X-Service-Id", "user-service")
                    .circuitBreaker(config -> config
                        .setName("userServiceCircuitBreaker")
                        .setFallbackUri("forward:/fallback/user-service"))
                    .retry(retryConfig -> retryConfig
                        .setRetries(3)
                        .setBackoff(Duration.ofMillis(100), Duration.ofMillis(1000), 2, true)))
                .uri("lb://user-service"))
            
            // 订单服务路由
            .route("order-service", r -> r
                .path("/api/v1/orders/**")
                .and().header("X-Request-Version", "v2")
                .filters(f -> f
                    .stripPrefix(2)
                    .requestRateLimiter(rateConfig -> rateConfig
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(userKeyResolver()))
                    .addResponseHeader("X-Response-Version", "v2"))
                .uri("lb://order-service"))
            
            // 商品服务路由 - 基于路径变量
            .route("product-service", r -> r
                .path("/api/v1/products/{category}/**")
                .filters(f -> f
                    .stripPrefix(3)
                    .addRequestHeader("X-Product-Category", "{category}")
                    .hystrix(config -> config
                        .setName("productService")
                        .setFallbackUri("forward:/fallback/product-service")))
                .uri("lb://product-service"))
            
            // 支付服务路由 - 权重负载
            .route("payment-service", r -> r
                .path("/api/v1/payments/**")
                .and().weight("payment-group", 80)
                .filters(f -> f.stripPrefix(2))
                .uri("lb://payment-service-v1"))
            
            .route("payment-service-v2", r -> r
                .path("/api/v1/payments/**")
                .and().weight("payment-group", 20)
                .filters(f -> f.stripPrefix(2))
                .uri("lb://payment-service-v2"))
            
            .build();
    }
    
    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(100, 200); // 每秒100个请求,突发200
    }
    
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> ReactiveSecurityContextHolder.getContext()
            .map(securityContext -> securityContext.getAuthentication().getName())
            .switchIfEmpty(Mono.just("anonymous"));
    }
}
动态路由管理
// 动态路由服务
@Service
public class DynamicRouteService implements ApplicationEventPublisherAware {
    
    private final RouteDefinitionWriter routeDefinitionWriter;
    private final RouteDefinitionLocator routeDefinitionLocator;
    private ApplicationEventPublisher publisher;
    
    @Autowired
    public DynamicRouteService(RouteDefinitionWriter routeDefinitionWriter,
                               RouteDefinitionLocator routeDefinitionLocator) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator = routeDefinitionLocator;
    }
    
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
    
    // 添加新路由
    public Mono<String> addRoute(RouteDefinition routeDefinition) {
        return routeDefinitionWriter.save(Mono.just(routeDefinition))
            .then(Mono.defer(() -> {
                publisher.publishEvent(new RefreshRoutesEvent(this));
                return Mono.just("Route added successfully");
            }));
    }
    
    // 更新路由
    public Mono<String> updateRoute(RouteDefinition routeDefinition) {
        return routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()))
            .then(routeDefinitionWriter.save(Mono.just(routeDefinition)))
            .then(Mono.defer(() -> {
                publisher.publishEvent(new RefreshRoutesEvent(this));
                return Mono.just("Route updated successfully");
            }));
    }
    
    // 删除路由
    public Mono<String> deleteRoute(String routeId) {
        return routeDefinitionWriter.delete(Mono.just(routeId))
            .then(Mono.defer(() -> {
                publisher.publishEvent(new RefreshRoutesEvent(this));
                return Mono.just("Route deleted successfully");
            }));
    }
    
    // 获取所有路由
    public Flux<RouteDefinition> getAllRoutes() {
        return routeDefinitionLocator.getRouteDefinitions();
    }
}

2. 安全防护机制

统一认证授权
// JWT认证过滤器
@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
    
    private final JwtTokenProvider tokenProvider;
    private final RedisTemplate<String, String> redisTemplate;
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    
    // 白名单路径
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/api/v1/auth/login",
        "/api/v1/auth/register",
        "/api/v1/public/**",
        "/actuator/health"
    );
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        // 白名单直接放行
        if (isWhiteListPath(path)) {
            return chain.filter(exchange);
        }
        
        // 获取token
        String token = extractToken(request);
        if (token == null) {
            return handleUnauthorized(exchange, "Missing authentication token");
        }
        
        try {
            // 验证token
            if (!tokenProvider.validateToken(token)) {
                return handleUnauthorized(exchange, "Invalid authentication token");
            }
            
            // 检查token是否被吊销
            String jti = tokenProvider.getJti(token);
            if (Boolean.TRUE.equals(redisTemplate.hasKey("token:blacklist:" + jti))) {
                return handleUnauthorized(exchange, "Token has been revoked");
            }
            
            // 解析用户信息
            Claims claims = tokenProvider.getClaims(token);
            String userId = claims.getSubject();
            String role = claims.get("role", String.class);
            String permissions = claims.get("permissions", String.class);
            
            // 权限校验
            if (!hasPermission(path, request.getMethod().name(), role, permissions)) {
                return handleForbidden(exchange, "Insufficient permissions");
            }
            
            // 添加用户信息到请求头
            ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", userId)
                .header("X-User-Role", role)
                .header("X-User-Permissions", permissions)
                .build();
            
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
            
        } catch (Exception e) {
            log.error("Authentication error", e);
            return handleUnauthorized(exchange, "Authentication failed");
        }
    }
    
    private boolean isWhiteListPath(String path) {
        return WHITE_LIST.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
    }
    
    private String extractToken(ServerHttpRequest request) {
        String bearerToken = request.getHeaders().getFirst("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
    
    private boolean hasPermission(String path, String method, String role, String permissions) {
        // 权限校验逻辑
        return true;
    }
    
    private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
        return handleError(exchange, HttpStatus.UNAUTHORIZED, message);
    }
    
    private Mono<Void> handleForbidden(ServerWebExchange exchange, String message) {
        return handleError(exchange, HttpStatus.FORBIDDEN, message);
    }
    
    private Mono<Void> handleError(ServerWebExchange exchange, HttpStatus status, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(status);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("code", status.value());
        errorResponse.put("message", message);
        errorResponse.put("timestamp", Instant.now().toString());
        errorResponse.put("path", exchange.getRequest().getURI().getPath());
        
        byte[] bytes = new ObjectMapper().writeValueAsBytes(errorResponse);
        return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes)));
    }
    
    @Override
    public int getOrder() {
        return -100; // 高优先级
    }
}
API安全策略
// API安全过滤器
@Component
public class ApiSecurityFilter implements GlobalFilter, Ordered {
    
    private final RedisTemplate<String, String> redisTemplate;
    private final RateLimiter rateLimiter;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String clientIp = getClientIp(request);
        String path = request.getURI().getPath();
        
        // IP黑名单检查
        if (isBlacklistedIp(clientIp)) {
            return handleBlocked(exchange, "IP address is blacklisted");
        }
        
        // 防SQL注入检查
        if (containsSqlInjection(request)) {
            logSecurityEvent("SQL_INJECTION", clientIp, path);
            return handleBlocked(exchange, "Potential SQL injection detected");
        }
        
        // 防XSS攻击检查
        if (containsXssAttack(request)) {
            logSecurityEvent("XSS_ATTACK", clientIp, path);
            return handleBlocked(exchange, "Potential XSS attack detected");
        }
        
        // 频率限制检查
        if (!rateLimiter.tryAcquire(clientIp)) {
            logSecurityEvent("RATE_LIMIT_EXCEEDED", clientIp, path);
            return handleRateLimited(exchange);
        }
        
        return chain.filter(exchange);
    }
    
    private boolean isBlacklistedIp(String ip) {
        return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:ip:" + ip));
    }
    
    private boolean containsSqlInjection(ServerHttpRequest request) {
        // 检查查询参数
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        for (List<String> values : queryParams.values()) {
            for (String value : values) {
                if (isSqlInjectionPattern(value)) {
                    return true;
                }
            }
        }
        
        // 检查请求头
        HttpHeaders headers = request.getHeaders();
        for (List<String> headerValues : headers.values()) {
            for (String value : headerValues) {
                if (isSqlInjectionPattern(value)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean isSqlInjectionPattern(String value) {
        if (value == null || value.isEmpty()) {
            return false;
        }
        
        String lowerValue = value.toLowerCase();
        String[] sqlKeywords = {
            "select", "insert", "update", "delete", "drop", "union", "where", 
            "or", "and", "exec", "script", "declare", "cast", "convert"
        };
        
        int count = 0;
        for (String keyword : sqlKeywords) {
            if (lowerValue.contains(keyword)) {
                count++;
            }
        }
        
        return count >= 2; // 包含多个SQL关键字
    }
    
    private boolean containsXssAttack(ServerHttpRequest request) {
        // 简单的XSS检测逻辑
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        for (List<String> values : queryParams.values()) {
            for (String value : values) {
                if (value.contains("<script") || value.contains("javascript:")) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private void logSecurityEvent(String eventType, String clientIp, String path) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType(eventType)
            .clientIp(clientIp)
            .targetPath(path)
            .timestamp(Instant.now())
            .build();
        
        redisTemplate.opsForList().rightPush("security:events", event.toJson());
        
        log.warn("Security event detected: {} from {} on {}", eventType, clientIp, path);
    }
    
    @Override
    public int getOrder() {
        return -90; // 高优先级,在认证之前执行
    }
}

3. 流量控制与熔断

智能限流策略
// 分布式限流过滤器
@Component
public class DistributedRateLimitFilter implements GlobalFilter, Ordered {
    
    private final RedisTemplate<String, String> redisTemplate;
    private final RateLimitRuleService rateLimitRuleService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String clientId = getClientIdentifier(request);
        String apiPath = request.getURI().getPath();
        String method = request.getMethod().name();
        
        // 获取限流规则
        RateLimitRule rule = rateLimitRuleService.getApplicableRule(apiPath, method);
        if (rule == null) {
            return chain.filter(exchange); // 没有规则,直接放行
        }
        
        // 检查是否超限
        RateLimitResult result = checkRateLimit(clientId, apiPath, rule);
        
        if (result.isAllowed()) {
            // 添加响应头
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().add("X-RateLimit-Limit", String.valueOf(rule.getLimit()));
            response.getHeaders().add("X-RateLimit-Remaining", String.valueOf(result.getRemaining()));
            response.getHeaders().add("X-RateLimit-Reset", String.valueOf(result.getResetTime()));
            
            return chain.filter(exchange);
        } else {
            // 超限处理
            return handleRateLimitExceeded(exchange, result);
        }
    }
    
    private RateLimitResult checkRateLimit(String clientId, String apiPath, RateLimitRule rule) {
        String key = buildRateLimitKey(clientId, apiPath, rule);
        long currentTime = System.currentTimeMillis();
        long windowStart = getWindowStart(currentTime, rule.getWindowSize());
        
        switch (rule.getAlgorithm()) {
            case FIXED_WINDOW:
                return checkFixedWindow(key, windowStart, rule);
            case SLIDING_WINDOW:
                return checkSlidingWindow(key, currentTime, rule);
            case TOKEN_BUCKET:
                return checkTokenBucket(key, rule);
            case LEAKY_BUCKET:
                return checkLeakyBucket(key, rule);
            default:
                throw new IllegalArgumentException("Unknown rate limit algorithm: " + rule.getAlgorithm());
        }
    }
    
    private RateLimitResult checkFixedWindow(String key, long windowStart, RateLimitRule rule) {
        String windowKey = key + ":" + windowStart;
        String countStr = redisTemplate.opsForValue().get(windowKey);
        int count = countStr != null ? Integer.parseInt(countStr) : 0;
        
        if (count < rule.getLimit()) {
            // 增加计数
            redisTemplate.opsForValue().increment(windowKey);
            redisTemplate.expire(windowKey, rule.getWindowSize(), TimeUnit.SECONDS);
            
            return RateLimitResult.allowed(rule.getLimit() - count - 1, windowStart + rule.getWindowSize() * 1000);
        } else {
            return RateLimitResult.denied(rule.getLimit() - count, windowStart + rule.getWindowSize() * 1000);
        }
    }
    
    private RateLimitResult checkSlidingWindow(String key, long currentTime, RateLimitRule rule) {
        String windowKey = key + ":sliding";
        long windowSizeMs = rule.getWindowSize() * 1000;
        long windowStart = currentTime - windowSizeMs;
        
        // 清理过期请求记录
        redisTemplate.opsForZSet().removeRangeByScore(windowKey, 0, windowStart);
        
        // 获取当前窗口内的请求数
        Long currentCount = redisTemplate.opsForZSet().count(windowKey, windowStart, currentTime);
        
        if (currentCount < rule.getLimit()) {
            // 记录当前请求
            redisTemplate.opsForZSet().add(windowKey, String.valueOf(currentTime), currentTime);
            redisTemplate.expire(windowKey, rule.getWindowSize(), TimeUnit.SECONDS);
            
            return RateLimitResult.allowed(rule.getLimit() - currentCount.intValue() - 1, currentTime + windowSizeMs);
        } else {
            return RateLimitResult.denied(rule.getLimit() - currentCount.intValue(), currentTime + windowSizeMs);
        }
    }
    
    private RateLimitResult checkTokenBucket(String key, RateLimitRule rule) {
        String bucketKey = key + ":bucket";
        String tokensStr = redisTemplate.opsForValue().get(bucketKey);
        double tokens = tokensStr != null ? Double.parseDouble(tokensStr) : rule.getLimit();
        
        long lastRefillTime = getLastRefillTime(key);
        long currentTime = System.currentTimeMillis();
        
        // 计算需要补充的令牌
        double tokensToAdd = (currentTime - lastRefillTime) * rule.getLimit() / (rule.getWindowSize() * 1000.0);
        tokens = Math.min(rule.getLimit(), tokens + tokensToAdd);
        
        if (tokens >= 1) {
            // 消耗一个令牌
            tokens -= 1;
            redisTemplate.opsForValue().set(bucketKey, String.valueOf(tokens));
            setLastRefillTime(key, currentTime);
            
            return RateLimitResult.allowed((int) tokens, currentTime + 1000);
        } else {
            return RateLimitResult.denied((int) tokens, currentTime + 1000);
        }
    }
    
    private String buildRateLimitKey(String clientId, String apiPath, RateLimitRule rule) {
        StringBuilder keyBuilder = new StringBuilder("ratelimit:");
        
        switch (rule.getScope()) {
            case GLOBAL:
                keyBuilder.append("global:");
                break;
            case API:
                keyBuilder.append("api:").append(apiPath).append(":");
                break;
            case USER:
                keyBuilder.append("user:").append(clientId).append(":");
                break;
            case IP:
                keyBuilder.append("ip:").append(clientId).append(":");
                break;
        }
        
        keyBuilder.append(rule.getId());
        return keyBuilder.toString();
    }
    
    @Override
    public int getOrder() {
        return -50;
    }
}
熔断降级机制
// 熔断器实现
@Component
public class CircuitBreakerFilter implements GatewayFilterFactory<CircuitBreakerFilter.Config> {
    
    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final FallbackHandler fallbackHandler;
    
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String routeId = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR).getId();
            CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(routeId);
            
            return circuitBreaker.executeSupplier(() -> 
                chain.filter(exchange)
                    .doOnSuccess(aVoid -> recordSuccess(circuitBreaker))
                    .doOnError(throwable -> recordError(circuitBreaker, throwable))
            ).onErrorResume(throwable -> {
                log.error("Circuit breaker triggered for route: {}", routeId, throwable);
                return fallbackHandler.handleFallback(exchange, routeId, throwable);
            });
        };
    }
    
    private void recordSuccess(CircuitBreaker circuitBreaker) {
        circuitBreaker.onSuccess(0, TimeUnit.MILLISECONDS);
    }
    
    private void recordError(CircuitBreaker circuitBreaker, Throwable throwable) {
        circuitBreaker.onError(0, TimeUnit.MILLISECONDS, throwable);
    }
    
    @Data
    public static class Config {
        private String fallbackUri;
        private int failureRateThreshold = 50;
        private int waitDurationInOpenState = 10000;
        private int slidingWindowSize = 100;
        private int minimumNumberOfCalls = 10;
    }
}

4. 监控与分析

统一监控指标
// 网关监控配置
@Component
public class GatewayMetricsFilter implements GlobalFilter, Ordered {
    
    private final MeterRegistry meterRegistry;
    private final Timer.Sample timerSample;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String routeId = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR).getId();
        String method = request.getMethod().name();
        String path = request.getURI().getPath();
        
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();
        
        return chain.filter(exchange)
            .doOnSuccess(aVoid -> {
                recordMetrics(exchange, routeId, method, path, startTime, null);
            })
            .doOnError(throwable -> {
                recordMetrics(exchange, routeId, method, path, startTime, throwable);
            });
    }
    
    private void recordMetrics(ServerWebExchange exchange, String routeId, String method, 
                              String path, long startTime, Throwable throwable) {
        long duration = System.currentTimeMillis() - startTime;
        ServerHttpResponse response = exchange.getResponse();
        HttpStatus statusCode = response.getStatusCode();
        
        // 记录响应时间
        meterRegistry.timer("gateway.request.duration", 
            "route", routeId,
            "method", method,
            "status", String.valueOf(statusCode.value()))
            .record(duration, TimeUnit.MILLISECONDS);
        
        // 记录请求数
        meterRegistry.counter("gateway.request.count",
            "route", routeId,
            "method", method,
            "status", String.valueOf(statusCode.value()))
            .increment();
        
        // 记录错误数
        if (throwable != null || (statusCode != null && statusCode.isError())) {
            meterRegistry.counter("gateway.request.errors",
                "route", routeId,
                "method", method,
                "status", String.valueOf(statusCode != null ? statusCode.value() : "unknown"),
                "error_type", throwable != null ? throwable.getClass().getSimpleName() : "http_error")
                .increment();
        }
        
        // 记录活跃连接数
        meterRegistry.gauge("gateway.active.connections", 
            Tags.of("route", routeId), 
            exchange.getRequest().getRemoteAddress() != null ? 1 : 0);
        
        // 记录请求大小
        HttpHeaders requestHeaders = exchange.getRequest().getHeaders();
        long requestSize = requestHeaders.getContentLength();
        if (requestSize > 0) {
            meterRegistry.summary("gateway.request.size",
                "route", routeId,
                "method", method)
                .record(requestSize);
        }
        
        // 记录响应大小
        HttpHeaders responseHeaders = response.getHeaders();
        long responseSize = responseHeaders.getContentLength();
        if (responseSize > 0) {
            meterRegistry.summary("gateway.response.size",
                "route", routeId,
                "method", method)
                .record(responseSize);
        }
        
        // 记录自定义业务指标
        recordBusinessMetrics(exchange, routeId, method, path);
    }
    
    private void recordBusinessMetrics(ServerWebExchange exchange, String routeId, 
                                     String method, String path) {
        // 用户相关指标
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        if (userId != null) {
            meterRegistry.counter("gateway.user.requests",
                "user_id", userId,
                "route", routeId)
                .increment();
        }
        
        // API版本指标
        String apiVersion = exchange.getRequest().getHeaders().getFirst("X-API-Version");
        if (apiVersion != null) {
            meterRegistry.counter("gateway.api.version.usage",
                "version", apiVersion,
                "route", routeId)
                .increment();
        }
    }
    
    @Override
    public int getOrder() {
        return -10;
    }
}
访问日志分析
// 访问日志过滤器
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
    
    private static final Logger accessLogger = LoggerFactory.getLogger("ACCESS_LOG");
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 构建访问日志
        AccessLogEntry logEntry = AccessLogEntry.builder()
            .requestId(request.getId())
            .timestamp(Instant.now())
            .remoteIp(getClientIp(request))
            .method(request.getMethod().name())
            .uri(request.getURI().toString())
            .userAgent(request.getHeaders().getFirst("User-Agent"))
            .referer(request.getHeaders().getFirst("Referer"))
            .build();
        
        // 记录请求开始
        long startTime = System.currentTimeMillis();
        
        return chain.filter(exchange)
            .doFinally(signalType -> {
                // 记录请求结束
                long duration = System.currentTimeMillis() - startTime;
                logEntry.setDuration(duration);
                logEntry.setStatusCode(exchange.getResponse().getStatusCode().value());
                
                // 记录响应信息
                HttpHeaders responseHeaders = exchange.getResponse().getHeaders();
                logEntry.setContentType(responseHeaders.getFirst("Content-Type"));
                logEntry.setContentLength(responseHeaders.getContentLength());
                
                // 记录用户信息
                String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
                if (userId != null) {
                    logEntry.setUserId(userId);
                }
                
                // 记录路由信息
                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                if (route != null) {
                    logEntry.setRouteId(route.getId());
                }
                
                // 异步记录日志
                writeAccessLog(logEntry);
            });
    }
    
    private void writeAccessLog(AccessLogEntry logEntry) {
        try {
            String logLine = objectMapper.writeValueAsString(logEntry);
            accessLogger.info(logLine);
        } catch (Exception e) {
            accessLogger.error("Failed to write access log", e);
        }
    }
    
    private String getClientIp(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        // 如果存在多个IP,取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.substring(0, ip.indexOf(",")).trim();
        }
        return ip;
    }
    
    @Override
    public int getOrder() {
        return -5;
    }
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class AccessLogEntry {
    private String requestId;
    private Instant timestamp;
    private String remoteIp;
    private String method;
    private String uri;
    private String userAgent;
    private String referer;
    private String userId;
    private String routeId;
    private long duration;
    private int statusCode;
    private String contentType;
    private long contentLength;
}

API网关的高可用设计

1. 集群部署架构

# Kubernetes部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
  namespace: infrastructure
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - api-gateway
            topologyKey: kubernetes.io/hostname
      containers:
      - name: api-gateway
        image: api-gateway:latest
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 8081
          name: admin
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "production"
        - name: REDIS_HOST
          value: "redis-cluster"
        - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE
          value: "http://eureka1:8761/eureka,http://eureka2:8761/eureka,http://eureka3:8761/eureka"
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: admin
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: admin
          initialDelaySeconds: 30
          periodSeconds: 10
        volumeMounts:
        - name: config
          mountPath: /app/config
          readOnly: true
      volumes:
      - name: config
        configMap:
          name: api-gateway-config
---
apiVersion: v1
kind: Service
metadata:
  name: api-gateway-service
  namespace: infrastructure
spec:
  selector:
    app: api-gateway
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: admin
    port: 8081
    targetPort: 8081
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: api-gateway-loadbalancer
  namespace: infrastructure
spec:
  selector:
    app: api-gateway
  ports:
  - name: http
    port: 80
    targetPort: 8080
  type: LoadBalancer

2. 配置中心集成

// 配置中心客户端
@Component
@ConfigurationProperties(prefix = "gateway")
@Data
public class GatewayProperties {
    
    private Security security = new Security();
    private RateLimit rateLimit = new RateLimit();
    private CircuitBreaker circuitBreaker = new CircuitBreaker();
    private Monitoring monitoring = new Monitoring();
    
    @Data
    public static class Security {
        private boolean enabled = true;
        private Jwt jwt = new Jwt();
        private List<String> whiteList = new ArrayList<>();
        private List<String> blackList = new ArrayList<>();
        
        @Data
        public static class Jwt {
            private String secret;
            private long expiration = 3600000; // 1 hour
            private String header = "Authorization";
            private String prefix = "Bearer ";
        }
    }
    
    @Data
    public static class RateLimit {
        private boolean enabled = true;
        private int defaultLimit = 100;
        private int defaultWindow = 60; // seconds
        private Redis redis = new Redis();
        
        @Data
        public static class Redis {
            private String host = "localhost";
            private int port = 6379;
            private String password;
            private int database = 0;
        }
    }
    
    @Data
    public static class CircuitBreaker {
        private boolean enabled = true;
        private int failureRateThreshold = 50;
        private int waitDurationInOpenState = 10000; // milliseconds
        private int slidingWindowSize = 100;
        private int minimumNumberOfCalls = 10;
    }
    
    @Data
    public static class Monitoring {
        private boolean enabled = true;
        private Metrics metrics = new Metrics();
        private Logging logging = new Logging();
        
        @Data
        public static class Metrics {
            private boolean enabled = true;
            private List<String> exportEndpoints = Arrays.asList("prometheus", "influxdb");
        }
        
        @Data
        public static class Logging {
            private boolean enabled = true;
            private String level = "INFO";
            private boolean structured = true;
        }
    }
}

API网关的最佳实践

1. 设计原则

// API设计规范
@RestController
@RequestMapping("/api/v1")
public class ApiDesignController {
    
    // RESTful API设计
    @GetMapping("/users/{userId}")
    public ResponseEntity<UserDTO> getUser(@PathVariable String userId) {
        // 1. 使用名词复数形式
        // 2. 使用HTTP方法表示操作
        // 3. 使用状态码表示结果
        return ResponseEntity.ok(userService.getUser(userId));
    }
    
    @PostMapping("/users")
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDTO user = userService.createUser(request);
        // 返回创建资源的URI
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{userId}")
            .buildAndExpand(user.getUserId())
            .toUri();
        return ResponseEntity.created(location).body(user);
    }
    
    @PutMapping("/users/{userId}")
    public ResponseEntity<UserDTO> updateUser(@PathVariable String userId, 
                                             @Valid @RequestBody UpdateUserRequest request) {
        UserDTO user = userService.updateUser(userId, request);
        return ResponseEntity.ok(user);
    }
    
    @DeleteMapping("/users/{userId}")
    public ResponseEntity<Void> deleteUser(@PathVariable String userId) {
        userService.deleteUser(userId);
        return ResponseEntity.noContent().build();
    }
    
    // 批量操作
    @PostMapping("/users/batch")
    public ResponseEntity<BatchOperationResult> batchCreateUsers(
            @Valid @RequestBody List<CreateUserRequest> requests) {
        BatchOperationResult result = userService.batchCreateUsers(requests);
        return ResponseEntity.ok(result);
    }
    
    // 分页查询
    @GetMapping("/users")
    public ResponseEntity<PageResult<UserDTO>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) String sort,
            @RequestParam(required = false) String filter) {
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by(sort));
        PageResult<UserDTO> users = userService.getUsers(pageRequest, filter);
        return ResponseEntity.ok(users);
    }
}

2. 版本管理策略

// API版本管理
@Component
public class ApiVersionManager {
    
    private final Map<String, ApiVersionInfo> versionMap = new ConcurrentHashMap<>();
    
    public void registerVersion(String apiPath, String version, Date deprecationDate, String migrationGuide) {
        ApiVersionInfo versionInfo = ApiVersionInfo.builder()
            .apiPath(apiPath)
            .version(version)
            .releaseDate(new Date())
            .deprecationDate(deprecationDate)
            .migrationGuide(migrationGuide)
            .status(ApiStatus.ACTIVE)
            .build();
        
        versionMap.put(apiPath + ":" + version, versionInfo);
    }
    
    public ApiVersionInfo getVersionInfo(String apiPath, String version) {
        return versionMap.get(apiPath + ":" + version);
    }
    
    public List<ApiVersionInfo> getAllVersions(String apiPath) {
        return versionMap.entrySet().stream()
            .filter(entry -> entry.getKey().startsWith(apiPath + ":"))
            .map(Map.Entry::getValue)
            .sorted(Comparator.comparing(ApiVersionInfo::getVersion).reversed())
            .collect(Collectors.toList());
    }
    
    public boolean isVersionDeprecated(String apiPath, String version) {
        ApiVersionInfo versionInfo = getVersionInfo(apiPath, version);
        if (versionInfo == null) {
            return false;
        }
        
        Date deprecationDate = versionInfo.getDeprecationDate();
        return deprecationDate != null && new Date().after(deprecationDate);
    }
    
    public String getRecommendedVersion(String apiPath) {
        return getAllVersions(apiPath).stream()
            .filter(v -> v.getStatus() == ApiStatus.ACTIVE)
            .findFirst()
            .map(ApiVersionInfo::getVersion)
            .orElse(null);
    }
}

// 版本过滤器
@Component
public class ApiVersionFilter implements GlobalFilter, Ordered {
    
    private final ApiVersionManager versionManager;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        // 提取版本信息
        String version = extractVersion(request);
        if (version == null) {
            // 没有指定版本,使用最新版本
            version = versionManager.getRecommendedVersion(path);
            if (version != null) {
                // 添加版本到请求头
                ServerHttpRequest mutatedRequest = request.mutate()
                    .header("X-API-Version", version)
                    .build();
                return chain.filter(exchange.mutate().request(mutatedRequest).build());
            }
        } else {
            // 检查版本是否已废弃
            if (versionManager.isVersionDeprecated(path, version)) {
                // 添加废弃警告头
                ServerHttpResponse response = exchange.getResponse();
                response.getHeaders().add("X-API-Deprecated", "true");
                response.getHeaders().add("X-API-Deprecation-Date", 
                    versionManager.getVersionInfo(path, version).getDeprecationDate().toString());
                
                String recommendedVersion = versionManager.getRecommendedVersion(path);
                if (recommendedVersion != null) {
                    response.getHeaders().add("X-API-Recommended-Version", recommendedVersion);
                }
            }
        }
        
        return chain.filter(exchange);
    }
    
    private String extractVersion(ServerHttpRequest request) {
        // 从请求头中提取版本
        String version = request.getHeaders().getFirst("X-API-Version");
        if (version != null) {
            return version;
        }
        
        // 从URL路径中提取版本
        String path = request.getURI().getPath();
        if (path.contains("/v1/")) {
            return "v1";
        } else if (path.contains("/v2/")) {
            return "v2";
        }
        
        return null;
    }
    
    @Override
    public int getOrder() {
        return -20;
    }
}

3. 性能优化策略

// 响应缓存配置
@Configuration
@EnableCaching
public class ResponseCacheConfig {
    
    @Bean
    public CacheManager gatewayCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats());
        return cacheManager;
    }
    
    @Bean
    public CacheManager apiCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(5000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .recordStats());
        return cacheManager;
    }
}

// 缓存过滤器
@Component
public class ResponseCacheFilter implements GlobalFilter, Ordered {
    
    private final CacheManager cacheManager;
    private final CacheKeyGenerator cacheKeyGenerator;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String method = request.getMethod().name();
        String path = request.getURI().getPath();
        
        // 只缓存GET请求
        if (!"GET".equals(method)) {
            return chain.filter(exchange);
        }
        
        // 生成缓存key
        String cacheKey = cacheKeyGenerator.generateKey(request);
        String cacheName = getCacheName(path);
        
        // 检查缓存
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            CachedResponse cachedResponse = cache.get(cacheKey, CachedResponse.class);
            if (cachedResponse != null && !cachedResponse.isExpired()) {
                // 返回缓存响应
                return writeCachedResponse(exchange, cachedResponse);
            }
        }
        
        // 继续处理请求
        return chain.filter(exchange)
            .then(Mono.defer(() -> {
                // 缓存响应
                return cacheResponse(exchange, cacheKey, cacheName);
            }));
    }
    
    private Mono<Void> cacheResponse(ServerWebExchange exchange, String cacheKey, String cacheName) {
        ServerHttpResponse response = exchange.getResponse();
        HttpStatusCode statusCode = response.getStatusCode();
        
        // 只缓存成功的响应
        if (statusCode != null && statusCode.is2xxSuccessful()) {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache != null) {
                // 获取响应内容
                DataBufferFactory bufferFactory = response.bufferFactory();
                
                // 这里需要实现响应内容的捕获和缓存逻辑
                // 简化示例
                CachedResponse cachedResponse = CachedResponse.builder()
                    .statusCode(statusCode.value())
                    .headers(response.getHeaders())
                    .body(new byte[0]) // 实际应该包含响应体
                    .expireTime(System.currentTimeMillis() + getCacheTTL(path))
                    .build();
                
                cache.put(cacheKey, cachedResponse);
            }
        }
        
        return Mono.empty();
    }
    
    private String getCacheName(String path) {
        if (path.startsWith("/api/v1/users")) {
            return "user-api-cache";
        } else if (path.startsWith("/api/v1/products")) {
            return "product-api-cache";
        } else {
            return "default-api-cache";
        }
    }
    
    private long getCacheTTL(String path) {
        if (path.startsWith("/api/v1/users")) {
            return TimeUnit.MINUTES.toMillis(5);
        } else if (path.startsWith("/api/v1/products")) {
            return TimeUnit.MINUTES.toMillis(10);
        } else {
            return TimeUnit.MINUTES.toMillis(1);
        }
    }
    
    @Override
    public int getOrder() {
        return 10;
    }
}

API网关的演进趋势

1. 云原生网关

# Istio服务网格配置
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: api-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - api.example.com
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: api-gateway-cert
    hosts:
    - api.example.com
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api-routes
  namespace: default
spec:
  hosts:
  - api.example.com
  gateways:
  - istio-system/api-gateway
  http:
  - match:
    - uri:
        prefix: /api/v1/users
    route:
    - destination:
        host: user-service
        port:
          number: 8080
      weight: 100
    - destination:
        host: user-service-v2
        port:
          number: 8080
      weight: 0
    fault:
      delay:
        percentage:
          value: 0.1
        fixedDelay: 5s
    retries:
      attempts: 3
      perTryTimeout: 2s
      retryOn: 5xx,reset,connect-failure,refused-stream
    timeout: 10s
    corsPolicy:
      allowOrigins:
      - exact: https://app.example.com
      allowMethods:
      - GET
      - POST
      - PUT
      - DELETE
      allowHeaders:
      - authorization
      - content-type
      - x-request-id
      maxAge: 24h

2. GraphQL网关

// GraphQL网关配置
@Configuration
public class GraphQLGatewayConfig {
    
    @Bean
    public GraphQLSchema graphQLSchema() {
        return Federation.transform(
            GraphQLSchema.newSchema()
                .query(GraphQLObjectType.newObject()
                    .name("Query")
                    .field(GraphQLFieldDefinition.newFieldDefinition()
                        .name("user")
                        .type(GraphQLTypeReference.typeRef("User"))
                        .argument(GraphQLArgument.newArgument()
                            .name("id")
                            .type(Scalars.GraphQLString)
                            .build())
                        .dataFetcher(environment -> {
                            String userId = environment.getArgument("id");
                            return userService.getUser(userId);
                        })
                        .build())
                    .build())
                .build()
        ).build();
    }
    
    @Bean
    public GraphQL graphQL(GraphQLSchema schema) {
        return GraphQL.newGraphQL(schema)
            .instrumentation(new TracingInstrumentation())
            .queryExecutionStrategy(new AsyncExecutionStrategy())
            .mutationExecutionStrategy(new AsyncSerialExecutionStrategy())
            .build();
    }
}

// GraphQL控制器
@RestController
@RequestMapping("/graphql")
public class GraphQLController {
    
    private final GraphQL graphQL;
    private final ObjectMapper objectMapper;
    
    @PostMapping
    public Mono<ResponseEntity<Map<String, Object>>> graphql(
            @RequestBody Map<String, Object> request,
            @RequestHeader Map<String, String> headers) {
        
        String query = (String) request.get("query");
        String operationName = (String) request.get("operationName");
        Map<String, Object> variables = (Map<String, Object>) request.get("variables");
        
        ExecutionInput executionInput = ExecutionInput.newExecutionInput()
            .query(query)
            .operationName(operationName)
            .variables(variables != null ? variables : Collections.emptyMap())
            .context(headers)
            .build();
        
        return Mono.fromCallable(() -> graphQL.execute(executionInput))
            .subscribeOn(Schedulers.boundedElastic())
            .map(executionResult -> {
                Map<String, Object> response = new LinkedHashMap<>();
                response.put("data", executionResult.getData());
                
                if (!executionResult.getErrors().isEmpty()) {
                    response.put("errors", executionResult.getErrors().stream()
                        .map(error -> Map.of(
                            "message", error.getMessage(),
                            "locations", error.getLocations(),
                            "path", error.getPath()
                        ))
                        .collect(Collectors.toList()));
                }
                
                return ResponseEntity.ok(response);
            });
    }
}

总结

API网关作为微服务架构的核心基础设施,承担着统一接入、安全防护、流量控制、服务治理等多重职责。通过遵循API网关法则,我们能够:

核心价值

  1. 统一服务入口:为所有微服务提供统一的访问入口,简化客户端调用
  2. 增强安全防护:集中处理认证授权、数据加密、攻击防护等安全问题
  3. 智能流量控制:通过限流、熔断、负载均衡等机制保障系统稳定性
  4. 简化服务治理:统一处理服务发现、健康检查、灰度发布等治理需求
  5. 提升可观测性:集中收集访问日志、性能指标、错误统计等监控数据

关键原则

  1. 单一职责:每个过滤器只负责特定的功能,保持职责单一
  2. 可配置性:支持动态配置路由规则、安全策略、限流参数等
  3. 高可用性:通过集群部署、故障转移、降级策略确保服务可用
  4. 性能优先:通过缓存、异步处理、连接池等技术优化性能
  5. 可扩展性:支持自定义过滤器、插件机制,便于功能扩展

成功要素

  1. 合理的路由设计:基于业务域设计清晰的路由规则
  2. 完善的安全机制:建立多层次的安全防护体系
  3. 智能的流量管理:根据业务特点制定合适的限流熔断策略
  4. 全面的监控覆盖:建立从基础设施到业务指标的完整监控体系
  5. 持续的性能优化:通过缓存、压缩、连接复用等技术提升性能

记住:API网关是微服务架构的守门人,它不仅决定了外部如何访问内部服务,更直接影响着整个系统的安全性、稳定性和性能表现。通过遵循API网关法则,我们能够构建出既安全又高效、既稳定又可扩展的优秀微服务架构。

API网关法则提醒我们:在微服务架构中,统一的服务入口层是不可或缺的。只有通过API网关的统一管理,我们才能真正发挥微服务架构的优势,同时避免分布式系统带来的复杂性。通过遵循这一原则,我们能够构建出既满足当前需求,又具备未来扩展性的优秀架构。

### 微服务架构中的API网关作用 在微服务架构中,API网关扮演着至关重要的角色。其主要职责在于作为系统的单一入口点,负责处理来自客户端的所有请求并将其转发给相应的内部微服务[^1]。 API网关不仅简化了前端应用与后端多个独立部署的服务之间的交互过程,还提供了诸如身份验证、限流控制以及协议转换等功能,这些特性有助于增强整体的安全性和性能表现[^2]。 然而值得注意的是,在引入API网关的同时也增加了系统复杂度,特别是在路由管理和高可用设计方面提出了更高的要求;如果未能妥善解决这些问题,则可能导致新的瓶颈或潜在风险,比如单点故障的发生几率增大等问题。 ### 实现方式概述 对于API网关的具体实现方案而言,通常有多种技术选型可供选择: #### 1. 使用开源框架构建自定义解决方案 可以基于Spring Cloud Gateway 或者 Kong这类流行的开源项目快速搭建起满足业务需求的基础平台,并在此基础上进一步定制化开发以支持特定场景下的高级功能[^3]。 ```java // Spring Cloud Gateway 配置示例 @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/api/v1/users/**") .uri("lb://USER-SERVICE")) .build(); } ``` #### 2. 利用云服务商提供的托管产品 像AWS API Gateway这样的云计算平台上所提供的即开即用的产品可以直接用于生产环境而无需过多关注底层运维细节,非常适合希望专注于核心业务逻辑的企业采用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值