Spring Cloud Gateway 详细说明文档

1. Spring Cloud Gateway 是什么?

Spring Cloud Gateway 是 Spring Cloud 生态系统中的现代化 API 网关组件,用于构建微服务架构中的统一入口网关。它基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.x 构建,采用响应式编程模型,提供高性能、非阻塞式的 API 路由和横切关注点处理能力。

1.1 技术架构基础

  • 响应式编程:基于 Project Reactor 的响应式流处理
  • WebFlux:使用 Spring WebFlux 而非传统的 Servlet 模型
  • 函数式编程:支持 Java 8+ 的函数式编程风格
  • 高性能:相比 Zuul 1.x(阻塞式),性能提升显著

1.2 在微服务架构中的定位

客户端 → Spring Cloud Gateway → 微服务A
                             → 微服务B  
                             → 微服务C

作为所有微服务的统一入口点,客户端只需要与网关交互,无需知道后端服务的具体位置。

2. Spring Cloud Gateway 的作用

2.1 核心作用

  • 统一入口:为所有微服务提供单一访问入口
  • 路由转发:根据配置规则将请求路由到对应的服务
  • 负载均衡:集成 Spring Cloud LoadBalancer 实现服务发现和负载均衡
  • 协议适配:支持 HTTP/HTTPS、WebSocket 等协议
  • 监控指标:收集请求日志、性能指标等监控数据

2.2 横切关注点处理

  • 认证(Authentication):统一处理用户身份验证
  • 安全防护:防重放攻击、IP 黑白名单、基础安全头设置
  • 限流熔断:实现请求限流、服务降级等保护机制
  • 请求/响应修改:修改请求头、响应头等

3. Spring Cloud Gateway 的特点

3.1 技术特点

  • 响应式非阻塞:基于 Reactor 的异步非阻塞处理模型
  • 高性能:单机可处理数万 QPS
  • 灵活路由:支持多种路由匹配方式(Path、Host、Method、Header 等)
  • 丰富过滤器:内置多种过滤器,支持自定义过滤器
  • 动态配置:支持运行时动态修改路由配置
  • 服务发现集成:无缝集成 Eureka、Consul、Nacos 等注册中心

3.2 功能组件

  • Route(路由):定义请求如何被转发到目标服务
  • Predicate(断言):匹配 HTTP 请求的各种条件
  • Filter(过滤器):修改请求和响应的逻辑
  • GlobalFilter(全局过滤器):应用于所有路由的过滤器

4. 网关应该做什么 vs 不应该做什么

4.1 ✅ 网关应该做的事情

4.1.1 路由和转发
  • 根据路径、主机名等条件路由请求
  • 实现负载均衡和服务发现
  • 处理协议转换(如 WebSocket)
4.1.2 基础安全(认证层面)
  • 身份认证:验证 JWT Token、API Key 等的有效性
  • Token 验证:检查签名、过期时间、基本格式
  • 安全头处理:添加 X-Forwarded-For、X-Real-IP 等头信息
  • 基础防护:IP 黑白名单、防刷、防重放攻击
4.1.3 运维和监控
  • 统一日志:记录访问日志、请求响应时间
  • 指标收集:收集 QPS、响应时间、错误率等指标
  • 健康检查:提供网关自身的健康检查端点
  • 限流熔断:基于 Redis 或内存的请求限流
4.1.4 请求/响应处理
  • 路径重写:StripPrefix、PrefixPath 等
  • 头信息修改:添加、删除、修改请求/响应头
  • 重试机制:对失败请求进行重试

4.2 ❌ 网关不应该做的事情

4.2.1 业务权限控制(授权)
  • 不应该进行细粒度权限验证(如:用户A能否访问用户B的数据)
  • 不应该验证业务级别的权限(如:是否有删除权限、编辑权限)
  • 不应该替代业务服务的安全逻辑
4.2.2 业务逻辑处理
  • 不应该包含业务逻辑(如:订单状态验证、库存检查)
  • 不应该调用业务数据库进行复杂查询
  • 不应该处理业务数据转换
4.2.3 复杂数据处理
  • 不应该解析和验证复杂的请求体
  • 不应该进行业务数据的校验和转换
  • 不应该缓存业务数据

4.3 🏗️ 正确的安全分层架构

客户端 → 网关层(Authentication) → 业务服务层(Authorization)
         ↓                          ↓
    验证"你是谁"                验证"你能做什么"
    Token有效性验证             业务权限验证
    基础安全防护               数据权限控制

5. 详细使用示例

5.1 基础环境搭建

5.1.1 Maven 依赖配置
<?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>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-gateway-demo</artifactId>
    <version>1.0.0</version>
    <name>spring-cloud-gateway-demo</name>
    
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
    </properties>
    
    <dependencies>
        <!-- Spring Cloud Gateway 核心依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        
        <!-- 服务发现客户端(用于集成注册中心) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        
        <!-- Actuator 监控端点 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Redis 依赖(用于限流) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</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>
</project>
5.1.2 基础配置文件 (application.yml)
server:
  port: 9000  # 网关服务端口

spring:
  application:
    name: api-gateway  # 应用名称
  cloud:
    gateway:
      # 全局跨域配置
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"      # 允许所有源
            allowedMethods: "*"      # 允许所有HTTP方法
            allowedHeaders: "*"      # 允许所有请求头
            allowCredentials: true   # 允许携带凭证
            
      # 路由配置
      routes:
        # 用户服务路由 - 网关只负责路由和基础认证
        - id: user-service-route
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**     # 匹配用户相关路径
          filters:
            - StripPrefix=2          # 去掉 /api/users 前缀,实际转发到 /**
            
        # 订单服务路由
        - id: order-service-route
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=2

# Actuator 监控配置
management:
  endpoints:
    web:
      exposure:
        include: '*'               # 暴露所有监控端点
  endpoint:
    gateway:
      enabled: true                # 启用网关管理端点

5.2 路由配置详解

5.2.1 Java 代码配置路由
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;

/**
 * 网关路由配置类
 * 使用 Java 代码方式配置路由,相比配置文件更加灵活
 * 注意:这里只配置路由规则,不包含业务权限逻辑
 */
@Configuration
public class GatewayRouteConfig {

    /**
     * 配置自定义路由规则
     * @param builder RouteLocatorBuilder 用于构建路由
     * @return RouteLocator 路由定位器
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                // 用户服务路由
                .route("user-service", r -> r
                        .path("/api/users/**")           // 匹配路径
                        .uri("http://localhost:8081")    // 目标服务地址
                        .filters(f -> f.stripPrefix(2))  // 去掉前两个路径段 (/api/users)
                )
                // 订单服务路由
                .route("order-service", r -> r
                        .path("/api/orders/**")
                        .uri("http://localhost:8082")
                        .filters(f -> f.stripPrefix(2))
                )
                // 公开API路由(无需认证)
                .route("public-api", r -> r
                        .path("/api/public/**")
                        .uri("http://localhost:8083")
                        .filters(f -> f.stripPrefix(2))
                )
                .build();
    }
}

5.3 断言(Predicates)使用示例

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 PredicateConfig {

    @Bean
    public RouteLocator predicateRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                // 1. Path 断言:基于URL路径匹配
                .route("path-route", r -> r
                        .path("/api/v1/**", "/api/v2/**")  // 支持多个路径模式
                        .uri("http://localhost:8081")
                )
                // 2. Method 断言:基于HTTP方法匹配
                .route("method-route", r -> r
                        .path("/api/method/**")
                        .and()
                        .method("GET", "POST")  // 只匹配GET和POST请求
                        .uri("http://localhost:8082")
                )
                // 3. Header 断言:基于请求头匹配
                .route("header-route", r -> r
                        .path("/api/header/**")
                        .and()
                        .header("X-API-Version", "v1")  // 必须包含指定请求头
                        .uri("http://localhost:8083")
                )
                // 4. Query 断言:基于查询参数匹配
                .route("query-route", r -> r
                        .path("/api/query/**")
                        .and()
                        .query("version", "1.0")  // 查询参数 version=1.0
                        .uri("http://localhost:8084")
                )
                // 5. Host 断言:基于Host头匹配
                .route("host-route", r -> r
                        .host("api.example.com", "api.test.com")  // 匹配指定域名
                        .uri("http://localhost:8085")
                )
                .build();
    }
}

5.4 过滤器(Filters)使用示例

5.4.1 内置过滤器配置
package com.example.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
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 FilterConfig {

    @Bean
    public RouteLocator filterRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                // 1. 添加请求头过滤器
                .route("add-header-route", r -> r
                        .path("/api/add-header/**")
                        .filters(f -> f
                                // 添加网关来源标识
                                .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                                // 添加请求时间戳
                                .addRequestHeader("X-Request-Timestamp", 
                                    String.valueOf(System.currentTimeMillis()))
                        )
                        .uri("http://localhost:8081")
                )
                // 2. 添加响应头过滤器
                .route("add-response-header-route", r -> r
                        .path("/api/add-response-header/**")
                        .filters(f -> f
                                .addResponseHeader("X-Gateway-Processed", "true")
                        )
                        .uri("http://localhost:8082")
                )
                // 3. 路径重写过滤器
                .route("strip-prefix-route", r -> r
                        .path("/api/strip/**")
                        .filters(f -> f.stripPrefix(1))  // 去掉第一个路径段
                        .uri("http://localhost:8083")
                )
                // 4. 限流过滤器(使用Redis)
                .route("rate-limiter-route", r -> r
                        .path("/api/rate-limiter/**")
                        .filters(f -> f
                                .requestRateLimiter()
                                .rateLimiter(RedisRateLimiter.class)
                                // 配置限流参数:每秒5个请求,突发容量10个
                                .configure(c -> c.setBurstCapacity(10).setReplenishRate(5))
                        )
                        .uri("http://localhost:8084")
                )
                // 5. 重试过滤器
                .route("retry-route", r -> r
                        .path("/api/retry/**")
                        .filters(f -> f.retry(3))  // 失败时重试3次
                        .uri("http://localhost:8085")
                )
                .build();
    }
}

5.5 网关安全配置(正确的做法)

5.5.1 网关层认证过滤器(只做身份认证)
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.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.util.Arrays;
import java.util.List;

/**
 * 网关认证过滤器 - 只负责身份认证,不负责权限授权
 * 
 * 职责范围:
 * ✅ 验证JWT Token的有效性(签名、过期时间)
 * ✅ 验证Token的基本格式
 * ✅ 将用户信息传递给下游服务
 * ❌ 不进行业务权限验证
 * ❌ 不调用业务数据库
 * ❌ 不处理复杂的业务逻辑
 */
@Component
public class AuthenticationGlobalFilter implements GlobalFilter, Ordered {

    // 公开路径列表(无需认证的路径)
    private static final List<String> PUBLIC_PATHS = Arrays.asList(
        "/api/public/**",
        "/api/auth/login",
        "/api/auth/register",
        "/actuator/**"
    );

    /**
     * 全局过滤器执行逻辑
     * @param exchange 当前请求交换对象
     * @param chain 过滤器链
     * @return Mono<Void> 响应式返回
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestPath = exchange.getRequest().getURI().getPath();
        
        // 1. 检查是否为公开路径,如果是则直接放行
        if (isPublicPath(requestPath)) {
            return chain.filter(exchange);
        }
        
        // 2. 从请求头中提取认证令牌
        String token = extractToken(exchange);
        if (token == null || token.isEmpty()) {
            return handleUnauthorized(exchange, "Missing authentication token");
        }
        
        try {
            // 3. 验证令牌有效性(只验证签名和过期时间,不验证业务权限)
            if (!validateTokenSignatureAndExpiry(token)) {
                return handleUnauthorized(exchange, "Invalid or expired token");
            }
            
            // 4. 从令牌中提取用户基本信息
            String userId = extractUserIdFromToken(token);
            String userRoles = extractUserRolesFromToken(token);
            
            // 5. 将用户信息添加到请求头,传递给下游业务服务
            // 下游服务将基于这些信息进行具体的权限验证
            ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
                    .header("X-User-Id", userId)
                    .header("X-User-Roles", userRoles)
                    .header("X-Authenticated", "true")
                    .build();
            
            return chain.filter(exchange.mutate().request(modifiedRequest).build());
            
        } catch (Exception e) {
            return handleUnauthorized(exchange, "Authentication processing failed: " + e.getMessage());
        }
    }

    /**
     * 判断路径是否为公开路径
     * @param path 请求路径
     * @return 是否为公开路径
     */
    private boolean isPublicPath(String path) {
        return PUBLIC_PATHS.stream()
                .anyMatch(publicPath -> path.matches(publicPath.replace("**", ".*")));
    }

    /**
     * 从请求中提取认证令牌
     * 支持 Bearer Token 和自定义 Header
     */
    private String extractToken(ServerWebExchange exchange) {
        // 优先从 Authorization 头获取
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7); // 去掉 "Bearer " 前缀
        }
        
        // 也可以从自定义头或查询参数获取
        String tokenHeader = exchange.getRequest().getHeaders().getFirst("X-API-Token");
        if (tokenHeader != null) {
            return tokenHeader;
        }
        
        return null;
    }

    /**
     * 验证令牌签名和过期时间
     * 注意:这里只做基础验证,不做业务权限验证
     */
    private boolean validateTokenSignatureAndExpiry(String token) {
        // 实际项目中应该使用 JWT 库进行验证
        // 这里简化处理,实际应该验证签名、过期时间等
        try {
            // 伪代码:验证 JWT 签名和过期时间
            // Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return token.length() > 20; // 简单验证
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 从令牌中提取用户ID
     * 实际项目中应该解析 JWT payload
     */
    private String extractUserIdFromToken(String token) {
        // 伪代码:从 JWT 中提取用户ID
        return "user123"; // 简化处理
    }

    /**
     * 从令牌中提取用户角色
     * 实际项目中应该解析 JWT payload 中的角色信息
     */
    private String extractUserRolesFromToken(String token) {
        // 伪代码:从 JWT 中提取角色
        return "USER,PREMIUM"; // 简化处理
    }

    /**
     * 处理未授权请求
     */
    private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().add("Content-Type", "application/json");
        String response = "{\"error\":\"Unauthorized\",\"message\":\"" + message + "\"}";
        return exchange.getResponse().writeWith(
            Mono.just(exchange.getResponse().bufferFactory().wrap(response.getBytes()))
        );
    }

    /**
     * 设置过滤器执行顺序
     * 数值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return -100; // 在很早的阶段执行认证
    }
}
5.5.2 业务服务层权限控制(真正的授权)
// 用户服务中的权限控制示例
package com.example.userservice.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;

/**
 * 用户控制器 - 在业务服务中进行真正的权限验证
 * 
 * 网关已经完成了身份认证,这里进行业务级别的权限授权
 */
@RestController
@RequestMapping("/api")
public class UserController {

    /**
     * 获取用户详情
     * 需要验证当前用户是否有权限访问目标用户
     */
    @GetMapping("/users/{userId}")
    public User getUser(@RequestHeader("X-User-Id") String currentUserId,
                       @RequestHeader("X-User-Roles") String userRoles,
                       @PathVariable String userId) {
        
        // 1. 验证是否是访问自己的信息(普通用户只能访问自己)
        if (!currentUserId.equals(userId)) {
            // 2. 如果不是访问自己,检查是否具有管理员角色
            if (!userRoles.contains("ADMIN")) {
                throw new ResponseStatusException(HttpStatus.FORBIDDEN, 
                    "Insufficient permissions to access this user data");
            }
        }
        
        // 3. 执行业务逻辑
        return userService.findById(userId);
    }

    /**
     * 删除用户 - 需要管理员权限
     * 使用 Spring Security 注解进行权限控制
     */
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/users/{userId}")
    public void deleteUser(@PathVariable String userId) {
        userService.delete(userId);
    }
}

5.6 日志和监控配置

5.6.1 访问日志过滤器
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

/**
 * 访问日志全局过滤器
 * 记录请求的基本信息,用于监控和审计
 * 注意:只记录基础信息,不记录敏感业务数据
 */
@Component
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();
        String path = exchange.getRequest().getURI().getPath();
        String method = exchange.getRequest().getMethodValue();
        String clientIp = getClientIp(exchange);
        
        System.out.println(String.format(
            "[ACCESS-LOG] %s | %s | %s | %s", 
            LocalDateTime.now(), method, path, clientIp
        ));
        
        // 继续处理请求,并记录响应时间
        return chain.filter(exchange).doOnTerminate(() -> {
            long duration = System.currentTimeMillis() - startTime;
            System.out.println(String.format(
                "[ACCESS-LOG] Response time: %d ms for %s", duration, path
            ));
        });
    }

    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() != null ? 
               exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1; // 在最后执行,确保能获取到完整信息
    }
}

5.7 完整的启动类

package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Cloud Gateway 网关启动类
 * 
 * 设计原则:
 * ✅ 网关只负责:路由转发、身份认证、基础安全、监控日志
 * ❌ 网关不负责:业务权限、业务逻辑、数据验证
 * 
 * 启动后可用端点:
 * - 网关服务:http://localhost:9000
 * - 路由信息:http://localhost:9000/actuator/gateway/routes
 * - 健康检查:http://localhost:9000/actuator/health
 */
@SpringBootApplication
public class GatewayApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
        System.out.println("=====================================");
        System.out.println("Spring Cloud Gateway 启动成功!");
        System.out.println("网关地址: http://localhost:9000");
        System.out.println("路由管理: http://localhost:9000/actuator/gateway/routes");
        System.out.println("=====================================");
    }
}

6. 最佳实践总结

6.1 网关职责边界

  • 网关层:路由 + 认证 + 基础安全 + 监控
  • 业务层:授权 + 业务逻辑 + 数据验证

6.2 性能优化建议

  • 合理配置线程池和连接池
  • 避免在过滤器中执行耗时操作
  • 使用缓存减少重复计算

6.3 安全最佳实践

  • 网关只做身份认证,不做权限授权
  • 敏感信息不要在网关层处理
  • 使用 HTTPS 保护传输安全

6.4 监控和运维

  • 集成 Prometheus + Grafana
  • 配置合理的健康检查
  • 记录详细的访问日志

这份文档详细说明了 Spring Cloud Gateway 的正确使用方式,特别强调了网关的职责边界,避免了常见的架构设计误区。所有示例都包含详细的中文注释,帮助您理解每个组件的正确用途和实现方式。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值