为了账号安全,请及时绑定邮箱和手机立即绑定

网关过滤器接入鉴权校验资料详解与入门教程

概述

本文详细介绍了网关过滤器在微服务架构中的作用,包括安全性、负载均衡、日志记录和内容转换等功能。文章还深入讲解了接入鉴权的基本概念和常见方式,如API密钥、OAuth和JWT鉴权,确保服务的安全性和合规性。此外,文章探讨了校验资料的生成与验证方法,以确保数据的完整性和一致性。文中还通过具体示例展示了如何在网关中配置鉴权过滤器和校验资料。网关过滤器接入鉴权校验资料是本文的核心主题。

网关过滤器简介

什么是网关过滤器

网关过滤器是部署在应用程序前端的中间件组件,用于在请求到达目标服务之前对其进行处理。这些过滤器可以在请求到达应用程序之前执行各种操作,如日志记录、安全检查、内容转换和路由决策等。网关过滤器的主要目的是增强应用程序的安全性、提高性能和提供一致的用户体验。

网关过滤器的作用

网关过滤器在微服务架构中扮演着至关重要的角色,其主要作用包括:

  1. 安全性: 网关过滤器可以执行各种安全检查,如鉴权验证、访问控制和防止SQL注入等。
  2. 负载均衡与路由: 网关可以将请求路由到不同的服务,并在多个服务之间进行负载均衡。
  3. 监控与日志: 网关可以收集请求和响应的详细信息,帮助进行故障诊断和性能监控。
  4. 内容转换: 网关可以转换请求和响应的内容类型,如 JSON 转换为 XML。
  5. 缓存与压缩: 网关可以缓存常见的请求响应,并对内容进行压缩,以提高传输效率。

常见的网关过滤器类型

常见的网关过滤器类型包括:

  1. 鉴权过滤器: 验证用户的身份和权限,如 OAuth、JWT 鉴权。
  2. 路由过滤器: 根据请求的路径或参数将请求路由到不同的服务。
  3. 日志记录过滤器: 记录每个请求和响应的日志,便于后续的故障排查和性能分析。
  4. 限流过滤器: 限制特定客户端或 IP 地址的请求频率,防止恶意攻击和过载。
  5. 缓存过滤器: 对常见请求进行缓存,减少后端服务的负担。
  6. 内容过滤器: 对请求和响应的内容进行转换或处理,如格式转换、内容加密等。

接入鉴权的基本概念

什么是接入鉴权

接入鉴权是一种用于验证客户端请求合法性的机制。这种机制确保只有合法的客户端才能访问应用程序。接入鉴权通常通过验证客户端提供的某种形式的凭证来实现,这些凭证可以是 API 密钥、访问令牌或数字证书等。

常见的接入鉴权方式

常见的接入鉴权方式包括:

  1. API 密钥: 为每个客户端分配一个唯一的密钥,客户端在每个请求中都需要提供此密钥。
  2. OAuth: 一种开放标准授权协议,允许第三方应用程序访问服务,而无需共享用户的密码。
  3. JWT(JSON Web Token): 一种基于 JSON 的开放标准,用于在各方之间安全地发送信息。
  4. 数字证书: 使用公钥基础设施(PKI)来验证客户端的身份,通常用于企业级应用。
  5. Basic 认证: 使用 Base64 编码的用户名和密码进行认证,但这种方式安全性较低,不推荐用于生产环境。

接入鉴权的重要性

接入鉴权的重要性体现在以下几个方面:

  1. 安全性: 通过接入鉴权,可以确保只有合法的客户端才能访问服务,从而防止非法访问和数据泄露。
  2. 责任追溯: 接入鉴权可以记录每个客户端的身份,便于追踪和审计,明确责任归属。
  3. 性能优化: 通过限制非法请求,可以减少后端服务的处理负担,提高系统整体性能。
  4. 合法合规: 在一些行业或领域,接入鉴权是合规要求的一部分,确保服务合法运行。

校验资料的基础知识

什么是校验资料

校验资料是指用于验证请求或响应的数据完整性和合法性的信息。这些资料可以是哈希值、签名或数字证书等。通过校验资料,可以确保数据在传输过程中未被篡改,确保数据的一致性和可靠性。

如何生成校验资料

校验资料的生成方法依赖于具体的校验方法:

  1. 哈希值: 通过哈希算法(如 SHA-256)生成数据的哈希值。示例代码如下:
import hashlib

def generate_hash(input_data: str) -> str:
    hash_object = hashlib.sha256()
    hash_object.update(input_data.encode('utf-8'))
    return hash_object.hexdigest()
  1. 数字签名: 数字签名使用公钥加密和私钥解密机制,确保数据的完整性和不可否认性。示例代码如下:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend

private_key = serialization.load_pem_private_key(
    b"-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
    password=None,
    backend=default_backend()
)

def generate_signature(message: str) -> bytes:
    encoded_message = message.encode('utf-8')
    signature = private_key.sign(
        encoded_message,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    return signature
  1. 数字证书: 数字证书由证书颁发机构(CA)签发,包含公钥和其他身份信息。示例代码如下:
import OpenSSL

# original_certificate 是原始证书的 PEM 格式
original_certificate = b"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----"

# 从证书中提取公钥
public_key = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, original_certificate).get_pubkey().to_cryptography_key()

如何验证校验资料

校验资料的验证方法依赖于具体的校验方法:

  1. 哈希值: 通过比较生成的哈希值与预期的哈希值,验证数据的完整性。示例代码如下:
def verify_hash(input_data: str, expected_hash: str) -> bool:
    actual_hash = generate_hash(input_data)
    return actual_hash == expected_hash
  1. 数字签名: 使用公钥解密数字签名,验证数据的完整性和来源。示例代码如下:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend

public_key = serialization.load_pem_public_key(
    b"-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
    backend=default_backend()
)

def verify_signature(message: str, signature: bytes) -> bool:
    encoded_message = message.encode('utf-8')
    try:
        public_key.verify(
            signature,
            encoded_message,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH
            ),
            hashes.SHA256()
        )
        return True
    except:
        return False
  1. 数字证书: 使用公钥验证数字证书的签名,确保证书的来源和完整性。示例代码如下:
import OpenSSL

# original_certificate 是原始证书的 PEM 格式
original_certificate = b"-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----"

# 从证书中提取公钥
public_key = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, original_certificate).get_pubkey().to_cryptography_key()

# 验证签名
def verify_certificate_signature(cert: bytes, signature: bytes) -> bool:
    # 验证签名逻辑
    pass

校验资料的作用

校验资料的主要作用包括:

  1. 数据完整性: 通过校验资料,确保数据在传输过程中未被篡改。
  2. 数据一致性: 通过校验资料,确保数据的一致性,避免数据被恶意修改。
  3. 身份验证: 通过数字签名和证书,验证发送者的身份和数据的来源。
  4. 责任追溯: 通过校验资料,可以追踪数据的来源和传输路径,便于审计和追踪责任。

实战演练:配置网关过滤器进行接入鉴权

准备工作:设置开发环境

为了配置网关过滤器进行接入鉴权,你需要一个支持网关过滤器的平台。以下是一些常用的网关服务器和相关工具:

  1. Spring Cloud Gateway: Spring Cloud 提供的一个轻量级网关框架。示例代码如下:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("path_route", r -> r.path("/api/**").uri("lb://service"))
            .build();
    }

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
  1. Kong: 一个基于 NGINX 的高性能 API 网关,支持多种接入鉴权方式。示例代码如下:
api_version: 1
name: global
config:
  host: 0.0.0.0
  port: 8000
  access_log: false
  admin_gui: true
  admin_gui_access_token: <PASSWORD>
  proxy_access_log: false
  proxy_buffer_size: 512
  proxy_read_timeout: 60000
  proxy_send_timeout: 60000
  proxy_connect_timeout: 60000
  proxy_bypass_admin: true

services:
  - name: service-1
    url: http://localhost:8080

routes:
  - name: service-1-public
    paths:
      - /api/v1/*
    methods:
      - GET
      - POST
    service: service-1
    hosts:
      - "service.example.com"
    strip_path: true
    preserve_host: true

plugins:
  - name: "api-key"
    service: service-1
    config:
      api_key_header_name: "X-API-Key"

consumers:
  - username: "user1"
    custom_id: "user1-id"
    tags:
      - key: "tag"
        values:
          - "user1-tag"

plugins:
  - name: "jwt"
    config:
      claim_name: "sub"
      secret: "mysecretkey"
      issuer: "issuer"
      audience: "audience"
  1. Envoy: 一个高性能、可扩展的代理,支持多种接入鉴权方式。示例代码如下:
static_resources:
  listeners:
    - name: http-listener
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 80
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              config:
                codec_type: HTTP
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/api/v1"
                          route:
                            cluster: service-cluster
                http_filters:
                  - name: envoy.filters.http.jwt_authn
                    config:
                      providers:
                        jwt:
                          issuer: "https://example.com"
                          audience: "api"
                          jwks:
                            file:
                              filename: "/etc/secret-volume/jwks.json"
                  - name: envoy.filters.http.router

步骤一:接入鉴权配置

接入鉴权配置通常涉及在网关中添加相关的过滤器或插件,以验证客户端请求的合法性。以下是一个使用 Spring Cloud Gateway 的示例配置:

  1. 添加依赖:
    pom.xml 文件中添加 Spring Cloud Gateway 依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  1. 配置接入鉴权:
    application.yml 文件中配置 JWT 接入鉴权:
spring:
  cloud:
  gateway:
    routes:
      - id: jwt_route
        uri: lb://service
        predicates:
          - Path=/api/**
        filters:
          - name: OAuth2
            args:
              client-id: client
              client-secret: secret
              token-type: bearer
              realm: api
  1. 实现 JWT 接入鉴权过滤器:
    实现一个自定义的 JWT 接入鉴权过滤器,示例代码如下:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import reactor.core.publisher.Mono;

@Configuration
public class GatewayConfig {

    private final JwtDecoder jwtDecoder;
    private final Environment env;

    public GatewayConfig(JwtDecoder jwtDecoder, Environment env) {
        this.jwtDecoder = jwtDecoder;
        this.env = env;
    }

    @Bean
    public GatewayFilter jwtGatewayFilter() {
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                String authorization = exchange.getRequest().getHeaders().getFirst("Authorization");
                if (authorization != null && authorization.startsWith("Bearer ")) {
                    String token = authorization.substring("Bearer ".length());
                    Jwt jwt = jwtDecoder.decode(token);
                    SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt));
                }
                return chain.filter(exchange);
            }
        }, 3);
    }

    @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("jwt_route", r -> r.path("/api/**")
                .filters(f -> f.filter(jwtGatewayFilter()))
                .uri("lb://service"))
            .build();
    }
}
  1. 测试接入鉴权:
    使用 Postman 或其他工具发送带 JWT 令牌的请求,示例代码如下:
GET /api/v1/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsImlzcyI6Imh0dHBzOi8vbG9jYWwuY29tIiwiYXVkIjoibG9jYWwiLCJpYXQiOjE1NjU5OTYwNjEsImV4cCI6MTU2NTk5NjQ2MSwibmJmIjowLCJwcm4iOiJzZGQwMmQ1MjciLCJ0eXBlIjoic2Vuc2VzIn0.uyI26Xw2c9sQ7s8s

步骤二:校验资料设置

校验资料设置涉及在接收到请求或响应时,验证数据的完整性和合法性。以下是一个使用 Kafka 消息队列的示例配置:

  1. 配置 Kafka:
    配置 Kafka 生产者和消费者,确保消息的完整性和安全性。示例代码如下:
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.util.Properties;

public class KafkaExample {
    public static void main(String[] args) {
        // 生产者配置
        Properties producerProps = new Properties();
        producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

        KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps);

        // 发送消息
        String hash = generateHash("message");
        producer.send(new ProducerRecord<>("topic-name", "key", "value + " " + hash));

        // 消费者配置
        Properties consumerProps = new Properties();
        consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "group-id");
        consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
        consumer.subscribe(Arrays.asList("topic-name"));

        // 消费消息
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
        }
    }
}
  1. 生成校验资料:
    在发送消息时,生成相应的校验资料。示例代码如下:
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;

public class KafkaProducerExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        String hash = generateHash("message");
        producer.send(new ProducerRecord<>("topic-name", "key", "value + " " + hash));

        producer.close();
    }

    private static String generateHash(String input) {
        return java.util.Base64.getEncoder().encodeToString(input.getBytes());
    }
}
  1. 验证校验资料:
    在接收消息时,验证校验资料。示例代码如下:
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;

public class KafkaConsumerExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("group.id", "group-id");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("topic-name"));

        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : records) {
                String[] parts = record.value().split(" ");
                String message = parts[0];
                String hash = parts[1];
                if (verifyHash(message, hash)) {
                    System.out.println("Message is valid: " + message);
                } else {
                    System.out.println("Message is invalid: " + message);
                }
            }
        }
    }

    private static boolean verifyHash(String message, String hash) {
        return java.util.Base64.getEncoder().encodeToString(message.getBytes()).equals(hash);
    }
}

常见问题及解决方案

错误代码解析与常见问题

在配置网关过滤器进行接入鉴权时,可能会遇到一些常见的错误代码和问题。以下是一些常见错误代码及其解析:

  1. 401 Unauthorized:

    • 原因: 客户端请求未携带有效的访问令牌或鉴权凭证。
    • 解决方法: 确保客户端请求中携带了正确的访问令牌或鉴权凭证。
    • 示例代码:
      try {
       // 发送请求并处理响应
       Response response = client.target("http://example.com/api/v1/users")
                               .request()
                               .header("Authorization", "Bearer token")
                               .get();
       System.out.println("Status: " + response.getStatus());
       System.out.println("Body: " + response.readEntity(String.class));
      } catch (NotFoundException e) {
       System.out.println("404 Not Found: " + e.getMessage());
      } catch (Exception e) {
       System.out.println("Error: " + e.getMessage());
      }
  2. 403 Forbidden:

    • 原因: 客户端请求携带的访问令牌或鉴权凭证无效或不被允许。
    • 解决方法: 检查访问令牌或鉴权凭证的有效性和权限设置。
    • 示例代码:
      try {
       Response response = client.target("http://example.com/api/v1/users")
                               .request()
                               .header("Authorization", "Bearer invalid-token")
                               .get();
       System.out.println("Status: " + response.getStatus());
       System.out.println("Body: " + response.readEntity(String.class));
      } catch (NotFoundException e) {
       System.out.println("404 Not Found: " + e.getMessage());
      } catch (Exception e) {
       System.out.println("Error: " + e.getMessage());
      }
  3. 500 Internal Server Error:

    • 原因: 服务器端发生错误,如配置错误、服务端代码异常等。
    • 解决方法: 查看服务器日志,定位并修复服务器端的错误。
    • 示例代码:
      try {
       Response response = client.target("http://example.com/api/v1/users")
                               .request()
                               .get();
       System.out.println("Status: " + response.getStatus());
       System.out.println("Body: " + response.readEntity(String.class));
      } catch (NotFoundException e) {
       System.out.println("404 Not Found: " + e.getMessage());
      } catch (Exception e) {
       System.out.println("Error: " + e.getMessage());
      }
  4. 404 Not Found:
    • 原因: 请求路径未被网关正确路由。
    • 解决方法: 检查路由配置,确保请求路径被正确配置。
    • 示例代码:
      try {
       Response response = client.target("http://example.com/api/v1/nonexistent")
                               .request()
                               .get();
       System.out.println("Status: " + response.getStatus());
       System.out.println("Body: " + response.readEntity(String.class));
      } catch (NotFoundException e) {
       System.out.println("404 Not Found: " + e.getMessage());
      } catch (Exception e) {
       System.out.println("Error: " + e.getMessage());
      }

常见问题的解决方法

以下是解决一些常见问题的方法:

  1. 鉴权失败:

    • 问题描述: 客户端请求未通过鉴权验证。
    • 解决方法:
      • 确保客户端请求中携带了正确的访问令牌或鉴权凭证。
      • 检查鉴权配置,确保鉴权方式和配置正确无误。
      • 检查鉴权插件或过滤器的实现,确保其逻辑正确。
  2. 路由失败:

    • 问题描述: 客户端请求未被正确路由到目标服务。
    • 解决方法:
      • 检查路由配置,确保路径和规则配置正确。
      • 检查目标服务的健康状态,确保目标服务正常运行。
      • 检查网关配置,确保网关与目标服务的连接正常。
  3. 校验资料验证失败:

    • 问题描述: 客户端请求携带的校验资料无效。
    • 解决方法:
      • 检查校验资料生成和验证的逻辑,确保其正确无误。
      • 检查校验资料的格式和内容,确保其符合预期。
      • 检查校验资料的签名和加密配置,确保其一致且正确。
  4. 性能瓶颈:
    • 问题描述: 网关服务性能下降,导致请求响应延迟或超时。
    • 解决方法:
      • 优化网关配置,确保其资源分配合理。
      • 使用负载均衡和缓存机制,减少后端服务的负担。
      • 分析请求数据,识别和优化瓶颈环节。

常见错误排查技巧

在排查接入鉴权和网关过滤器相关问题时,可以采用以下技巧:

  1. 日志检查:

    • 查看网关和相关服务的日志,定位错误或异常的源头。
    • 检查请求和响应的日志,分析请求路径和处理流程。
  2. 网络调试:

    • 使用工具如 Wireshark 或 Postman 进行网络抓包和调试,分析请求和响应的详细信息。
    • 检查网络连接,确保网关与目标服务的网络连接正常。
  3. 代码审查:

    • 仔细审查鉴权插件或过滤器的实现代码,确保逻辑正确且无误。
    • 检查配置文件和相关配置,确保其配置正确且符合预期。
  4. 性能测试:
    • 使用负载测试工具(如 JMeter)进行性能测试,验证系统的性能和稳定性。
    • 分析性能测试结果,识别并优化性能瓶颈点。

通过以上步骤和技巧,可以有效解决接入鉴权和网关过滤器配置中的常见问题,确保网关服务的正常运行和高效性能。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消