使用TLS单向/双向认证,加密Netty程序

使用TLS单向/双向认证,加密Netty程序

一. 项目准备

创建一个Netty服务端和客户端项目。

参考https://blog.csdn.net/u013071014/article/details/117325053?spm=1001.2014.3001.5501

二. KeyTool生成证书

  1. 生成Netty服务器公钥、私钥和证书仓库:

    keytool -genkey -alias server -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=cn" -keypass 123456 -storepass 123456 -keystore serverStore.jks
    
  2. 导出Netty服务端签名证书

    keytool -export -alias server -keystore serverStore.jks -storepass 123456 -file server.cer
    
  3. 生成Netty客户端公钥、私钥和仓库证书

    keytool -genkey -alias client -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=cn" -keypass 123456 -storepass 123456 -keystore clientStore.jks
    
  4. 将Netty服务端证书导入到客户端证书仓库

    keytool -import -trustcacerts -alias server -file server.cer -storepass 123456 -keystore clientStore.jks
    
  5. 导出Netty客户端签名证书

    keytool -export -alias client -keystore clientStore.jks -storepass 123456 -file client.cer
    
  6. 将客户端签名证书导入服务端证书仓库

    keytool -import -trustcacerts -alias client -file client.cer -storepass 123456 -keystore serverStore.jks
    
-genkey 生成秘钥
-alias 别名
-keyalg 秘钥算法
-keysize 秘钥长度
-validity 有效期
-keystore 生成秘钥库的存储路径和名称
-keypass 秘钥口令
-storepass 秘钥库口令
-dname 拥有者信息,CN:姓名;OU:组织单位名称;O:组织名称;L:省/市/自治区名称;C:国家/地区代码

查看证书:
keytool -list -keystore serverStore.jks -storepass 123456

在这里插入图片描述
在这里插入图片描述

三. 单向认证

  1. 单向认证过程:

    SSL单向认证只要求站点部署了ssl证书就行,任何用户都可以去访问(IP被限制除外等),只是服务端提供了身份认证。
    
    1.客户端的浏览器向服务器传送客户端SSL协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。
    
    2.服务器向客户端传送SSL协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
    
    3.客户利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA是否可靠,发行者证书的公钥能否正确解开服务器证书的"发行者的数字签名",服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。
    
    4.用户端随机产生一个用于后面通讯的"对称密码",然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的"预主密码"传给服务器。
    
    5.如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的"预主密码"一起传给服务器。
    
    6.如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的"预主密码",然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
    
    7.服务器和客户端用相同的主密码即"通话密码",一个对称密钥用于SSL协议的安全数据通讯的加解密通讯。同时在SSL通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
    
    8.客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤7中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
    
    9.服务器向客户端发出信息,指明后面的数据通讯将使用的步骤7中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
    
    10.SSL的握手部分结束,SSL安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。
    
  2. Netty服务端启动程序

    package com.example.nettyserver.netty;
    
    import com.example.nettyserver.utils.SslContextFactoryOne;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.codec.string.StringDecoder;
    import io.netty.handler.codec.string.StringEncoder;
    import io.netty.handler.ssl.SslHandler;
    import io.netty.util.CharsetUtil;
    
    import javax.net.ssl.SSLEngine;
    import java.io.File;
    
    public class NettyServer {
    
        public static final String path = "serverStore.jks";
    
        public void start() {
            // 主线程组,用于接收请求
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
            // 工作线程组,用于处理数据
            NioEventLoopGroup workerGroup = new NioEventLoopGroup(8);
    
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        // 设置队列大小
                        .option(ChannelOption.SO_BACKLOG, 128)
                        // 两小时内没有数据通信时,TCP会自动发送一个活动探测报文
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
    
                                // add TLS,单向认证
                                File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path);
                                String serverPath = directory.getCanonicalPath();
                                SSLEngine engine = SslContextFactoryOne.getServerContext(serverPath, serverPath).createSSLEngine();
                                // 设置服务端模式
                                engine.setUseClientMode(false);
                                // 需要验证客户端身份,如果是双向验证,需要设为true
                                engine.setNeedClientAuth(false);
                                pipeline.addLast("ssl", new SslHandler(engine));
    
                                // 添加编解码
                                pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
                                pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                                // 添加业务处理器
                                pipeline.addLast(new NettyServerHandler());
                            }
                        });
                // 绑定端口,接收连接
                System.out.println(System.currentTimeMillis() + " Start data channel");
                ChannelFuture future = serverBootstrap.bind("127.0.0.1", 8888).sync();
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    }
    
  3. 单向认证工具类

    package com.example.nettyserver.utils;
    
    import javax.net.ssl.KeyManagerFactory;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.TrustManagerFactory;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.KeyStore;
    
    public class SslContextFactoryOne {
        private static final String PROTOCOL = "TLS";
    
        // 服务器安全套接字协议
        private static SSLContext SERVER_CONTEXT;
    
        // 客户端安全套接字协议
        private static SSLContext CLIENT_CONTEXT;
    
        // 使用KeyTool生成密钥库和密钥时配置的密码
        private static String keyPassword = "123456";
    
        public static SSLContext getServerContext(String pkPath,String caPath) {
            if (SERVER_CONTEXT != null) {
                return SERVER_CONTEXT;
            }
            InputStream in =null;
    
            try{
                //密钥管理器
                KeyManagerFactory kmf = null;
                if(pkPath!=null){
                    //密钥库KeyStore
                    KeyStore ks = KeyStore.getInstance("JKS");
                    //加载服务端证书
                    in = new FileInputStream(pkPath);
                    //加载服务端的KeyStore  ;sNetty是生成仓库时设置的密码,用于检查密钥库完整性的密码
                    ks.load(in, keyPassword.toCharArray());
    
                    kmf = KeyManagerFactory.getInstance("SunX509");
                    //初始化密钥管理器
                    kmf.init(ks, keyPassword.toCharArray());
                }
                //获取安全套接字协议(TLS协议)的对象
                SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL);
                //初始化此上下文
                //参数一:认证的密钥      参数二:对等信任认证  参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
                SERVER_CONTEXT.init(kmf.getKeyManagers(), null, null);
    
            }catch(Exception e){
                throw new Error("Failed to initialize the server-side SSLContext", e);
            }finally{
                if(in !=null){
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
            }
            return SERVER_CONTEXT;
    
        }
    
        public static SSLContext getClientContext(String pkPath,String caPath){
            if (CLIENT_CONTEXT!=null) {
                return CLIENT_CONTEXT;
            }
    
            InputStream tIn = null;
            try{
                //信任库
                TrustManagerFactory tf = null;
                if (caPath != null) {
                    //密钥库KeyStore
                    KeyStore tks = KeyStore.getInstance("JKS");
                    //加载客户端证书
                    tIn = new FileInputStream(caPath);
                    tks.load(tIn, keyPassword.toCharArray());
                    tf = TrustManagerFactory.getInstance("SunX509");
                    // 初始化信任库
                    tf.init(tks);
                }
    
                CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL);
                //设置信任证书
                CLIENT_CONTEXT.init(null,tf == null ? null : tf.getTrustManagers(), null);
    
            }catch(Exception e){
                throw new Error("Failed to initialize the client-side SSLContext");
            }finally{
                if(tIn !=null){
                    try {
                        tIn.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            return CLIENT_CONTEXT;
        }
    }
    
  4. 客户端启动程序

    package com.example.nettyclient.netty;
    
    import com.example.nettyclient.utils.SslContextFactoryOne;
    import io.netty.bootstrap.Bootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.handler.codec.string.StringDecoder;
    import io.netty.handler.codec.string.StringEncoder;
    import io.netty.handler.ssl.SslHandler;
    
    import javax.net.ssl.SSLEngine;
    import java.io.File;
    
    public class NettyClient {
    
        public static final String path = "clientStore.jks";
    
        public void start() {
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
    
                                File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path);
                                String clientPath = directory.getCanonicalPath();
                                SSLEngine engine = SslContextFactoryOne.getClientContext(clientPath, clientPath).createSSLEngine();
                                engine.setUseClientMode(true);//客户方模式
                                pipeline.addLast("ssl", new SslHandler(engine));
    
                                pipeline.addLast("decoder", new StringDecoder());
                                pipeline.addLast("encoder", new StringEncoder());
                                pipeline.addLast(new NettyClientHandler());
                            }
                        });
                ChannelFuture future = bootstrap.connect("127.0.0.1", 8888).sync();
                System.out.println(System.currentTimeMillis() + " Client connect success...");
                // 发送消息
                future.channel().writeAndFlush("Client say Hello");
                // 等待连接被关闭
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                group.shutdownGracefully();
            }
        }
    }
    

四. 双向认证

  1. 双向认证过程

    双向认证则是需要服务端与客户端提供身份认证,只能是服务端允许的客户能去访问,安全性相对于要高一些。
    
    1.浏览器发送一个连接请求给安全服务器。
    
    2.服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。
    
    3.客户浏览器检查服务器送过来的证书是否是由自己信赖的CA中心(如沃通CA)所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。
    
    4.接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。
    
    5.服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。
    
    6.客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。
    
    7.服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。
    
    8.浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。
    
    9.服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。
    
    10.服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。
    
  2. Netty服务端启动程序

    package com.example.nettyserver.netty;
    
    import com.example.nettyserver.utils.SslContextFactoryOne;
    import com.example.nettyserver.utils.SslContextFactoryTwo;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.handler.codec.string.StringDecoder;
    import io.netty.handler.codec.string.StringEncoder;
    import io.netty.handler.ssl.SslHandler;
    import io.netty.util.CharsetUtil;
    
    import javax.net.ssl.SSLEngine;
    import java.io.File;
    
    public class NettyServer {
    
        public static final String path = "serverStore.jks";
    
        public void start() {
            // 主线程组,用于接收请求
            NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
            // 工作线程组,用于处理数据
            NioEventLoopGroup workerGroup = new NioEventLoopGroup(8);
    
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        // 设置队列大小
                        .option(ChannelOption.SO_BACKLOG, 128)
                        // 两小时内没有数据通信时,TCP会自动发送一个活动探测报文
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
    
                                // add TLS,单向认证
                                File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path);
                                String serverPath = directory.getCanonicalPath();
                                SSLEngine engine = SslContextFactoryTwo.getServerContext(serverPath, serverPath).createSSLEngine();
                                // 设置服务端模式
                                engine.setUseClientMode(false);
                                // 需要验证客户端身份,如果是双向验证,需要设为true
                                engine.setNeedClientAuth(true);
                                pipeline.addLast("ssl", new SslHandler(engine));
    
                                // 添加编解码
                                pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
                                pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                                // 添加业务处理器
                                pipeline.addLast(new NettyServerHandler());
                            }
                        });
                // 绑定端口,接收连接
                System.out.println(System.currentTimeMillis() + " Start data channel");
                ChannelFuture future = serverBootstrap.bind("127.0.0.1", 8888).sync();
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    }
    
  3. 单向认证工具类

    package com.example.nettyserver.utils;
    
    import javax.net.ssl.KeyManagerFactory;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.TrustManagerFactory;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.KeyStore;
    
    public class SslContextFactoryTwo {
        private static final String PROTOCOL = "TLS";
    
        // 服务器安全套接字协议
        private static SSLContext SERVER_CONTEXT;
    
        // 客户端安全套接字协议
        private static SSLContext CLIENT_CONTEXT;
    
        // 使用KeyTool生成密钥库和密钥时配置的密码
        private static String keyPassword = "123456";
    
    
        public static SSLContext getServerContext(String pkPath,String caPath){
            if(SERVER_CONTEXT!=null) {
                    return SERVER_CONTEXT;
            }
            InputStream in =null;
            InputStream tIn = null;
    
            try{
                //密钥管理器
                KeyManagerFactory kmf = null;
                if(pkPath!=null){
                    KeyStore ks = KeyStore.getInstance("JKS");
                    in = new FileInputStream(pkPath);
                    ks.load(in, keyPassword.toCharArray());
    
                    kmf = KeyManagerFactory.getInstance("SunX509");
                    kmf.init(ks, keyPassword.toCharArray());
                }
                //信任库
                TrustManagerFactory tf = null;
                if (caPath != null) {
                    KeyStore tks = KeyStore.getInstance("JKS");
                    tIn = new FileInputStream(caPath);
                    tks.load(tIn, keyPassword.toCharArray());
                    tf = TrustManagerFactory.getInstance("SunX509");
                    tf.init(tks);
                }
    
                SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL);
                //初始化此上下文
                //参数一:认证的密钥      参数二:对等信任认证  参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
                SERVER_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null);
    
            }catch(Exception e){
                throw new Error("Failed to initialize the server-side SSLContext", e);
            }finally{
                if(in !=null){
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    in = null;
                }
    
                if (tIn != null){
                    try {
                        tIn.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    tIn = null;
                }
            }
    
            return SERVER_CONTEXT;
        }
    
    
        public static SSLContext getClientContext(String pkPath,String caPath){
            if(CLIENT_CONTEXT!=null) {
                return CLIENT_CONTEXT;
            }
    
            InputStream in = null;
            InputStream tIn = null;
            try{
                KeyManagerFactory kmf = null;
                if (pkPath != null) {
                    KeyStore ks = KeyStore.getInstance("JKS");
                    in = new FileInputStream(pkPath);
                    ks.load(in, keyPassword.toCharArray());
                    kmf = KeyManagerFactory.getInstance("SunX509");
                    kmf.init(ks, keyPassword.toCharArray());
                }
    
                TrustManagerFactory tf = null;
                if (caPath != null) {
                    KeyStore tks = KeyStore.getInstance("JKS");
                    tIn = new FileInputStream(caPath);
                    tks.load(tIn, keyPassword.toCharArray());
                    tf = TrustManagerFactory.getInstance("SunX509");
                    tf.init(tks);
                }
    
                CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL);
                //初始化此上下文
                //参数一:认证的密钥      参数二:对等信任认证  参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
                CLIENT_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null);
    
            }catch(Exception e){
                throw new Error("Failed to initialize the client-side SSLContext");
            }finally{
                if(in !=null){
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    in = null;
                }
    
                if (tIn != null){
                    try {
                        tIn.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    tIn = null;
                }
            }
    
            return CLIENT_CONTEXT;
        }
    }
    
  4. 客户端启动程序

    package com.example.nettyclient.netty;
    
    import com.example.nettyclient.utils.SslContextFactoryOne;
    import com.example.nettyclient.utils.SslContextFactoryTwo;
    import io.netty.bootstrap.Bootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    import io.netty.handler.codec.string.StringDecoder;
    import io.netty.handler.codec.string.StringEncoder;
    import io.netty.handler.ssl.SslHandler;
    
    import javax.net.ssl.SSLEngine;
    import java.io.File;
    
    public class NettyClient {
    
        public static final String path = "clientStore.jks";
    
        public void start() {
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
    
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
    
                                File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path);
                                String clientPath = directory.getCanonicalPath();
                                SSLEngine engine = SslContextFactoryTwo.getClientContext(clientPath, clientPath).createSSLEngine();
                                engine.setUseClientMode(true);//客户方模式
                                pipeline.addLast("ssl", new SslHandler(engine));
    
                                pipeline.addLast("decoder", new StringDecoder());
                                pipeline.addLast("encoder", new StringEncoder());
                                pipeline.addLast(new NettyClientHandler());
                            }
                        });
                ChannelFuture future = bootstrap.connect("127.0.0.1", 8888).sync();
                System.out.println(System.currentTimeMillis() + " Client connect success...");
                // 发送消息
                future.channel().writeAndFlush("Client say Hello");
                // 等待连接被关闭
                future.channel().closeFuture().sync();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                group.shutdownGracefully();
            }
        }
    }
    
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值