06、Kerberos安全认证之配置和访问Kerberos安全认证的Kafka集群学习笔记

前言

个人介绍

博主介绍:✌目前全网粉丝3W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

视频平台:b站-Coder长路

当前博客相关内容介绍

学习kerberos主要原因是目前部门里会有测试kerberos连通性的问题bug,所以以此来系统学习下kerberos安全认证,主要是学习在kerberos安全配置下如何去访问各个大数据组件。

Kerberos安全认证系列学习教程(B站):https://www.bilibili.com/video/BV1rr421t7Zt【马士兵的系列课程】

学习配套源码(Gitee):https://gitee.com/changluJava/demo-exer/tree/master/bigdata/kerberos/kerberosAuth


一、Kafka配置Kerberos

Kafka也支持通过Kerberos进行认证,避免非法用户操作读取Kafka中的数据,对Kafka进行Kerberos认证可以按照如下步骤实现。

1、创建 Kafka 服务 Princial 主体并写入到 keytab 文件

在kerberos服务端【node1】节点执行如下命令创建Kafka服务主体:

[root@node1 ~]# kadmin.local -q "addprinc -pw 123456 kafka/node1"
[root@node1 ~]# kadmin.local -q "addprinc -pw 123456 kafka/node2"
[root@node1 ~]# kadmin.local -q "addprinc -pw 123456 kafka/node3"

在kerberos服务端【node1】节点执行如下命令将Kafka服务主体写入到keytab文件

#node1节点执行命令,将主体写入到keytab
[root@node1 ~]# kadmin.local -q "ktadd -norandkey -kt /home/keytabs/kafka.service.keytab kafka/node1@EXAMPLE.COM"
[root@node1 ~]# kadmin.local -q "ktadd -norandkey -kt /home/keytabs/kafka.service.keytab kafka/node2@EXAMPLE.COM"
[root@node1 ~]# kadmin.local -q "ktadd -norandkey -kt /home/keytabs/kafka.service.keytab kafka/node3@EXAMPLE.COM"

以上命令执行完成后,在【node1】节点/home/keytabs目录下生成kafka.service.keytab文件,将该文件分发到各个节点并赋权,这里可以只发送到node1~node3 Kafka所在节点,为了保证各个大数据集群节点的keytabs一致,这里分发到所有节点。

[root@node1 ~]# scp /home/keytabs/kafka.service.keytab node2:/home/keytabs/
[root@node1 ~]# scp /home/keytabs/kafka.service.keytab node3:/home/keytabs/
[root@node1 ~]# scp /home/keytabs/kafka.service.keytab node4:/home/keytabs/
[root@node1 ~]# scp /home/keytabs/kafka.service.keytab node5:/home/keytabs/

分发完成后,在集群各个节点【node1-5】上执行如下命令,修改kafka.service.keytab密钥文件访问权限:

chmod 770 /home/keytabs/kafka.service.keytab

2、修改配置 server.properties 文件

在Kafka各个节点【node1、2、3】KAFKA_HOME/config/server.properties文件中加入如下配置以支持Kerberos安全认证。

vim $KAFKA_HOME/config/server.properties

配置内容如下:

#在node1~node3所有节点单独配置
delete.topic.enable=true    #如果不加这个参数,被删除的topic会被标记为marked for deletion
# 指定kafka监听的协议和端口,SASL_PLAINTEXT表示使用SASL(Simple Authentication and Security Layer)机制进行认证和加密通信。
listeners=SASL_PLAINTEXT://:9092
# 指定Kafka Broker 之间通信使用SASL_PLAINTEXT机制进行认证和加密通信。
inter.broker.listener.name=SASL_PLAINTEXT
# 指定broker之间通信使用SASL机制,GSSAPI是一种基于Kerberos的SASL机制,它使用GSS-API(Generic Security Services Application Programming Interface)进行认证。
sasl.mechanism.inter.broker.protocol=GSSAPI
# 只启用了GSSAPI机制,表示Kafka只接受使用Kerberos进行认证的连接。
sasl.enabled.mechanisms=GSSAPI
# 指定Kafka在Kerberos中注册的服务名称,以便用于进行身份验证和授权。
sasl.kerberos.service.name=kafka
# 指定Kafka使用的授权器类。
authorizer.class.name=kafka.security.authorizer.AclAuthorizer
# 是否在ZooKeeper中设置ACL(Access Control List),这里设置为false,表示不对ZooKeeper节点设置ACL。
zookeeper.set.acl=false
# 指定当没有匹配的ACL规则时,是否允许所有用户访问。
allow.everyone.if.no.acl.found=true

3、准备 kafka_jaas.conf 文件

在node1~node3各个节点中准备kafka_jaas.conf文件,该文件配置kafka服务端和zookeeper客户端的身份验证和授权配置,由于zookeeper开启了Kerberos认证,所以这里需要进行zookeeper客户端的身份验证配置。

这里在【node1、2、3】各个kafka节点KAFKA_HOME/config/目录中创建kafka_jaas.conf文件:

vim $KAFKA_HOME/config/kafka_jaas.conf

注意:在node1~node3各个kafka节点中该文件中的KafkaServer对应的principal不同,为对应各个节点hostname。

【node1】内容如下:

KafkaServer {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node1@EXAMPLE.COM";
};
Client {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 serviceName="zookeeper"
 keyTab="/home/keytabs/zookeeper.service.keytab" 
 principal="zookeeper/node3@EXAMPLE.COM";
};

【node2】内容如下:

KafkaServer {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node2@EXAMPLE.COM";
};
Client {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 serviceName="zookeeper"
 keyTab="/home/keytabs/zookeeper.service.keytab" 
 principal="zookeeper/node3@EXAMPLE.COM";
};

【node3】内容如下:

KafkaServer {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node2@EXAMPLE.COM";
};
Client {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 serviceName="zookeeper"
 keyTab="/home/keytabs/zookeeper.service.keytab" 
 principal="zookeeper/node3@EXAMPLE.COM";
};

4、准备 kafka_client_jaas.conf

在各个kafka节点配置kafka_client_jaas.conf配置文件,该文件作用主要是对kafka 客户端进行身份认证。这里在node1~node3节点KAFKA_HOME/config/中创建kafka_client_jaas.conf文件。

在【node1、2、3】中编辑配置文件:

vim $KAFKA_HOME/config/kafka_client_jaas.conf

【node1】配置文件内容如下:

KafkaClient {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node1@EXAMPLE.COM";
};

【node2】配置文件内容如下:

KafkaClient {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node2@EXAMPLE.COM";
};

【node3】配置文件内容如下:

KafkaClient {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="/home/keytabs/kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node3@EXAMPLE.COM";
};

注意:node1-3对应节点修改配置对应的Principal信息为对应的hostname。

5、修改启动脚本 kafka-server-start.sh

在kafka各个节点中配置KAFKA_HOME/bin/kafka-server-start.sh启动脚本,在该脚本中加入kafka_jaas.conf配置。

【node1-3】节点编辑该文件:

vim $KAFKA_HOME/bin/kafka-server-start.sh

配置内容如下,配置在顶部第一行中:

#在node1~node3各个节点都要配置kafka-server-start.sh
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/server/kafka_2.13-3.3.1/config/kafka_jaas.conf"

6、修改 kafka 操作脚本

在当前集群中node1~node3节点是kafka服务端,同时如果在这3个节点上进行kafka 命令操作,这三个节点也是kafka客户端。在操作kafka时我们通常会操作KAFKA_HOME中的kafka-topic.sh、kafka-console-producer.sh、kafka-console-consumer.sh脚本,这些脚本需要进行kerberos认证,可以通过前面配置的kafka_client_jaas.conf文件进行Kerberos认证,所以这里在各个脚本中加入如下配置,避免在操作对应脚本时没有进行认证从而没有操作权限。

在【node1~node3】各个kafka 客户端配置以上脚本,可以先在node1节点进行配置各文件然后分发到其他kafka客户端,对应操作文件增加如下配置:

#vim /opt/server/kafka_2.13-3.3.1/bin/kafka-topics.sh
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/server/kafka_2.13-3.3.1/config/kafka_client_jaas.conf"

#vim /opt/server/kafka_2.13-3.3.1/bin/kafka-console-producer.sh
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/server/kafka_2.13-3.3.1/config/kafka_client_jaas.conf"

#vim /opt/server/kafka_2.13-3.3.1/bin/kafka-console-consumer.sh
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/opt/server/kafka_2.13-3.3.1/config/kafka_client_jaas.conf"

在【node1 】kafka 客户端配置完成后,将配置后的各个脚本文件分发到其他kafka客户端节点上:

[root@node1 ~]# cd /opt/server/kafka_2.13-3.3.1/bin/
[root@node1 bin]# scp ./kafka-topics.sh ./kafka-console-producer.sh ./kafka-console-consumer.sh node2:`pwd`
[root@node1 bin]# scp ./kafka-topics.sh ./kafka-console-producer.sh ./kafka-console-consumer.sh node3:`pwd`

7、准备 client.properites

在node1~node3各个kafka 客户端中准备client.properties配置文件,该配置文件内容如下,这里将该文件创建在各个节点的/root目录下。

vim /root/client.properties

配置内容如下:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=GSSAPI
sasl.kerberos.service.name=kafka

当Kafka通过Kerberos认证后,在执行KAFKA_HOME/bin目录下的脚本时,需要使用正确的协议、SASL机制及kerberos服务,所以这里将以上信息配置到client.properties文件中,在执行各个脚本时需要通过参数指定该文件,这样客户端可以和服务端正常通信。

在【node1】配置完成/root/client.properties文件后,分发到node2~node3节点中:

cd /root/

scp ./client.properties node2:`pwd`
scp ./client.properties node3:`pwd`

8、启动 kafka 集群

启动kafka集群前需要先启动Zookeeper,然后在各个kafka服务节点启动kafka,完成kafka集群启动。操作如下:

#node3~node5各节点启动zookeeper
zkServer.sh start

#node1~node3各节点启动kafka
startKafka.sh

启动效果,node1-3中kafka服务正常运行:

image-20240708001606828

若是启动失败,则可以通过查看kafka中server.properties配置文件中的日志异常报错。


二、客户端操作Kafka(kerberos认证方式)

启动Kafka集群后,通过如下命令在Kafka集群查询、创建topic以及向topic中写入数据,以下命令可以执行在node1~node3各个kafka 客户端中。

#【node1】创建kafka topic 
kafka-topics.sh --bootstrap-server node1:9092,node2:9092,node3:9092 --create --topic test --partitions 3 --replication-factor 1 --command-config  /root/client.properties

#【node3】查看集群topic
kafka-topics.sh --bootstrap-server node1:9092,node2:9092,node3:9092 --list --command-config /root/client.properties

# 【node2】删除kafka topic
kafka-topics.sh --bootstrap-server node1:9092,node2:9092,node3:9092 --delete --topic test --command-config  /root/client.properties

新建与查看topic列表如下:

image-20240709075512740

image-20240709075540465

生产者与消费者处理数据:

#【node1】向kafka topic中写入数据
[root@node1 ~]# kafka-console-producer.sh --broker-list node1:9092,node2:9092,node3:9092 --topic test --producer.config /root/client.properties 
>1
>2
>3
>4
>5

#【node2】读取kafka topic中的数据
[root@node2 ~]# kafka-console-consumer.sh --bootstrap-server node1:9092,node2:9092,node3:9092 --topic test --consumer.config /root/client.properties 
1
2
3
4
5

执行命令出现报错:

image-20240708010139587

后续解决:第二天重启虚拟机之后即可。

image-20240709075948272


三、Java API操作Kafka

可以按照如下步骤实现Java API操作Kerberos认证的Kafka数据。

image-20240709230333418

1、准备 krb5.conf 文件

将node1 kerberos服务端/etc/krb5.conf文件存放在IDEA项目中的resources资源目录中或者本地Window固定的某个目录中,用于编写代码时指定访问Kerberos的Realm。

2、准备用户 keytab 文件

在kerberos服务端node1节点上将生成的kafka.server.keytab文件存入到window路径中,这里放在项目resource资源目录下,后续需要该文件进行客户端认证。

cd /home/keytabs

image-20240709225215156

3、准备 kafka_client_jaas.conf 文件

将Kafka 中kafka_client_jaas.conf文件放在window中某个路径中,并修改该文件中keytab路径为window路径:

KafkaClient {
 com.sun.security.auth.module.Krb5LoginModule required
 useKeyTab=true
 storeKey=true
 keyTab="E:\\自学历程\\Gitee仓库\\demo-exer\\bigdata\\kerberos\\kerberosAuth\\kerberosAuth\\src\\main\\resources\\kafka\\kafka.service.keytab"
 serviceName="kafka"
 principal="kafka/node1@EXAMPLE.COM";
};

特别需要注意的是该文件中指定window路径时使用"/“或”"隔开各目录,否则客户端认证时读取不到keytab文件。这里将修改后的kafka_client_jaas.conf文件存入到项目resource资源目录下。

4、编写 Java 代码向 Kafka topic 中读写数据

项目pom.xml中引入如下依赖:

<!-- kafka client依赖包 -->
<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka-clients</artifactId>
  <version>3.3.1</version>
</dependency>

Java API读写Kafka Topic代码如下:

package com.changlu.kafka;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
import java.util.UUID;

/**
 *  Java API 操作Kerbros认证的Kafka
 *  使用 JAAS 来进行 Kerberos 认证
 *  注意:kafka_client_jaas.conf文件中的keytab文件路径需要使用双斜杠或者反单斜杠
 */
public class OperateAuthKafka {
    public static void main(String[] args) {
        //准备JAAS配置文件路径
        String kafkaClientJaasFile = "E:\\自学历程\\Gitee仓库\\demo-exer\\bigdata\\kerberos\\kerberosAuth\\kerberosAuth\\src\\main\\resources\\kafka\\kafka_client_jaas.conf";
        // Kerberos配置文件路径
        String krb5FilePath = "E:\\自学历程\\Gitee仓库\\demo-exer\\bigdata\\kerberos\\kerberosAuth\\kerberosAuth\\src\\main\\resources\\krb5.conf";

        System.setProperty("java.security.auth.login.config", kafkaClientJaasFile);
        System.setProperty("java.security.krb5.conf", krb5FilePath);

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "node1:9092,node2:9092,node3:9092");
        props.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //kerberos安全认证
        props.setProperty("security.protocol", "SASL_PLAINTEXT");
        props.setProperty("sasl.mechanism", "GSSAPI");
        props.setProperty("sasl.kerberos.service.name", "kafka");

        //向Kafka topic中发送消息
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
        kafkaProducer.send(new ProducerRecord<>("test", "100"));
        kafkaProducer.send(new ProducerRecord<>("test", "200"));
        kafkaProducer.send(new ProducerRecord<>("test", "300"));
        kafkaProducer.send(new ProducerRecord<>("test", "400"));
        kafkaProducer.send(new ProducerRecord<>("test", "500"));

        kafkaProducer.close();
        System.out.println("消息发送成功");

        /**
         * 从Kafka topic中消费消息
         */
        props.setProperty("group.id", "test"+ UUID.randomUUID());
        //设置消费的位置,earliest表示从头开始消费,latest表示从最新的位置开始消费
        props.setProperty("auto.offset.reset", "earliest");
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
        kafkaConsumer.subscribe(Arrays.asList("test"));
        while (true) {
            // 拉取数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : consumerRecords) {
                // 获取数据对应的分区号
                int partition = record.partition();
                // 对应数据值
                String value = record.value();
                //对应数据的偏移量
                long lastoffset = record.offset();
                //对应数据发送的key
                String key = record.key();

                System.out.println("数据的key为:"+ key +
                        ",数据的value为:" + value +
                        ",数据的offset为:"+ lastoffset +
                        ",数据的分区为:"+ partition);
            }
        }

    }
}

程序运行测试:

①首先在【node2】节点上跑一个消费topic为test的命令

kafka-console-consumer.sh --bootstrap-server node1:9092,node2:9092,node3:9092 --topic test --consumer.config /root/client.properties 

②接着跑该Java程序

image-20240709230205390

image-20240709230224063


四、StructuredStreaming操作Kafka(暂有问题)

StructuredStreaming操作Kafka时同样需要准备krb5.conf、kafka.service.keytab、kafka_client_jaas.conf配置文件,步骤参考Java API操作Kafka部分。

image-20240709234726778

在项目pom.xml中引入如下依赖:

<!-- SparkSQL -->
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql_2.12</artifactId>
  <version>3.4.0</version>
</dependency>
<!-- Kafka 0.10+ Source For Structured Streaming-->
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql-kafka-0-10_2.12</artifactId>
  <version>3.4.0</version>
</dependency>

代码如下:

package com.changlu.kafka;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.connector.kafka.source.reader.deserializer.KafkaRecordDeserializationSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import org.apache.kafka.clients.consumer.ConsumerRecord;

import java.io.IOException;

/**
 * Flink 读取Kerberos 认证的Kafka数据
 */
public class FlinkReadAuthKafka {
    public static void main(String[] args) throws Exception {
        //准备JAAS配置文件路径
        String kafkaClientJaasFile = "E:\\自学历程\\Gitee仓库\\demo-exer\\bigdata\\kerberos\\kerberosAuth\\kerberosAuth\\src\\main\\resources\\kafka\\kafka_client_jaas.conf";
        // Kerberos配置文件路径
        String krb5FilePath = "E:\\自学历程\\Gitee仓库\\demo-exer\\bigdata\\kerberos\\kerberosAuth\\kerberosAuth\\src\\main\\resources\\krb5.conf";

        System.setProperty("java.security.auth.login.config", kafkaClientJaasFile);
        System.setProperty("java.security.krb5.conf", krb5FilePath);

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        KafkaSource<Tuple2<String, String>> kafkaSource = KafkaSource.<Tuple2<String, String>>builder()
                .setBootstrapServers("node1:9092,node2:9092,node3:9092") //设置Kafka 集群节点
                .setTopics("test") //设置读取的topic
                .setGroupId("my-test-group") //设置消费者组
                //kerberos安全认证
                .setProperty("security.protocol", "SASL_PLAINTEXT")
                .setProperty("sasl.mechanism", "GSSAPI")
                .setProperty("sasl.kerberos.service.name", "kafka")

                .setStartingOffsets(OffsetsInitializer.earliest()) //设置读取数据位置
                .setDeserializer(new KafkaRecordDeserializationSchema<Tuple2<String, String>>() {
                    //设置key ,value 数据获取后如何处理
                    @Override
                    public void deserialize(ConsumerRecord<byte[], byte[]> consumerRecord, Collector<Tuple2<String, String>> collector) throws IOException {
                        String key = null;
                        String value = null;
                        if(consumerRecord.key() != null){
                            key = new String(consumerRecord.key(), "UTF-8");
                        }
                        if(consumerRecord.value() != null){
                            value = new String(consumerRecord.value(), "UTF-8");
                        }
                        collector.collect(Tuple2.of(key, value));
                    }

                    //设置置返回的二元组类型
                    @Override
                    public TypeInformation<Tuple2<String, String>> getProducedType() {
                        return TypeInformation.of(new TypeHint<Tuple2<String, String>>() {
                        });
                    }
                })
                .build();

        DataStreamSource<Tuple2<String, String>> kafkaDS = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "kafka-source");

        kafkaDS.print();

        env.execute();
    }
}

运行结果:目前版本测试有些问题

image-20240710003002612


五、Flink操作Kafka

Flink操作Kafka时同样需要准备krb5.conf、kafka.service.keytab、kafka_client_jaas.conf配置文件,步骤参考Java API操作Kafka部分。

image-20240710003301820

编写代码前,需要在项目pom.xml中引入如下依赖:

<!-- Flink批和流开发依赖包 -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-clients</artifactId>
  <version>1.16.0</version>
</dependency>

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-streaming-java</artifactId>
  <version>1.16.0</version>
</dependency>
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-connector-kafka</artifactId>
  <version>1.16.0</version>
</dependency>

运行测试结果:

image-20240709234607011

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

长路 ㅤ   

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

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

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

打赏作者

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

抵扣说明:

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

余额充值