Https协议通信过程以及在Android平台使用

Https协议通信过程

https通信一般使用非对称加密算法进行密钥传递,使用对称加密算法进行后续业务数据加密传输,一次完整的Https通信过程如下:

  1. TCP三次握手(建立通信双方可靠连接)
  2. 通信协议协商(确认客户端与服务端加密算法)
  3. 验证CA证书(客户端验证服务器CA证书合法性)
  4. 传递会话密钥(客户端与服务器协商会话密钥)
  5. 加密通信(使用协商加密算法和会话密钥进行加密通信)
TCP三次握手(建立可靠通信连接)

原文来源

TCP是面向连接的协议,客户端与服务器进行通信之前,需要建立可靠的连接。因此TCP协议通过三次握手来建立客户端与服务器的可靠连接。

先了解一下涉及的关键词,其中比较重要的字段有:

  • Seq序列号(Sequence number) 占32位,用来对TCP发起方发送的报文进行标记。

  • ack确认序列号(Acknowlegment number) 占32位,ack=Seq+1(即确认序列号等于发送发的Seq+1)

    注意:只有ACK标志位为1时,该字段才有效

  • Flags:标志位(Flags)共6个,即URG、ACK、PSH、RST、SYN、FIN等。具体含义如下:

    • SYN:该标志位,表示请求建立一个新连接

    • ACK:该标志位,表示确认序列号有效

    • FIN:该标志位,表示释放一个连接

    • RST:该标志位,表示重置连接

    • URG:该标志位,表示紧急指针(urgent pointer)有效

    • PSH:该标志位,表示接收方应该尽快将这个报文交给应用层

TCP三次握手示意图
在这里插入图片描述

  1. 【第一次握手】客户端向服务器发送一个TCP数据包,等待服务器确认,并自己进入SYN_SENT状态,数据包包含:
    • 标志位SYN:表示请求服务器连接新连接。
    • 序列号Seq:客户端发送TCP数据包序列号。
  2. 【第二次握手】服务器收到客户端的TCP数据包之后,结束LISTEN监听阶段,返回给客户端一个TCP数据包,自己进入SYN_REVD状态,数据包包含:
    • 标志位SYN:告诉客户端允许建立连接
    • 标志位ACK:确认序列号有效(即告诉客户端服务器能收到你的数据)
    • 序列号Seq:服务端生成的序列号
    • 确认序列号ack(客户端的Seq+1):服务端返回给客户端的确认序列号(客户端的序列号+1)
  3. 【第三次握手】客户端收到服务的数据包,对ack确认序列号进行验证(自身的Seq+1于其做比对),如果验证通过,则会回复给服务器一个数据包,并进入ESTABLELISTED建立阶段,数据包包含:
    • 标志位ACK:确认序列号有效(告诉服务器,我知道你收到我发的数据了)。
    • 序列号Seq(服务返回的ack):将服务器返回的确认序列号作为自己的Seq。
    • 确认序列号ack(服务器的Seq+1):将服务器的Seq序列号+1作为确认序列号

随后,服务器收到来自客户端的TCP报文之后,验证ack之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。至此TCP三次握手结束,客户端与服务器已经建立了可靠的连接,后续可进行数据通信。

注意:握手中涉及的Seq和ack值都是在初始值基础上进行计算的,一旦中途出现某一方发送的TCP报文丢失,变无法继续进行握手,以此确保了"三次握手"的顺利完成,保证了TCP报文传输的连贯性

通信协议协商(确认通信双方加密算法)

Https在经过TCP三次握手客户端与服务器建立了可靠通信连接之后,紧接着客户端与服务器要进行通信协议的协商,来确定后续的通信加密方式,具体流程如下:

  1. 客户端发送一段消息给服务器携带支持的加密算法(图中Cipher Suites)和一段随机数字Random1。其中随机数字Random1会在后面的对称加密中用到
    在这里插入图片描述
  2. 服务器收到后,会在客户端支持的加密算法中选择一个自己也支持的加密算法,作为后面通信使用的加密算法以及随机数字Random2,并告知客户端。(本次实验中服务端选择TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 作为加密算法在这里插入图片描述
证书验证

如果上一步通信协议协商,服务器支持客户端的通信协议,则服务端发送证书到客户端进行身份验证(CA证书认证)。在进行证书验证之前我们先了解一下Https证书认证工作流程以及证书链

  • Https证书认证工作流程

    1. 服务器生成一对密钥,私钥自己留着(用来解密客户端使用数字证书内的公钥加密的数据),公钥部分个人信息提交给数字证书认证机构(CA)。
    2. CA进行审核,并用CA自己的私钥对服务器提供的公钥及信息进行签名生成数字证书,并将此证书发给提交者。
    3. 在https建立连接时,客户端从服务器获取数字证书,客户端使用内置CA根证书的公钥,对服务器证书内CA私钥签名的数据进行验签,如果验签通过,说明该数字证书确实是CA颁发的,从而可以确认该证书中的公钥确实是合法服务器端提供的。
  • 证书链认证

    如果服务器的证书不是CA根证书颁发,而是通过中级证书机构签名颁发的证书,服务器在 SSL 握手期间不会仅向客户端发送它的证书,而是发送一个证书链,包括服务器 CA 以及到达可信的根 CA 所需要的任意中间证书,客户端或浏览器使用内置的CA根证书公钥对中间证书进行验证,如果根证书信任该中级证书,则该中级证书颁发的证书也是可信的,这就是证书链了。

    例如,主要在本次的消息中的两个证书:中级证书颁发机构的证书及其颁发的服务器证书在这里插入图片描述

传递会话密钥

上一步客户端对服务器证书认证之后,客户端生成一个随机数Random3,然后使用服务器证书内的公钥,对Random3加密后发送给服务器(只有服务器使用对应的私钥才能解开)。这样https加密的密钥传递流程才算走完。(此时客户端和服务器都具备了随机数Random1+Random2+Random3)

加密通信

由于非对称加密的运算成本较高,所以非对称加密算法一般只用来进行秘钥传递,所以完成秘钥传递之后,客户端一般会使用之前与服务器协商的加密算法,将Random1+Random2+Random3作为对称加密算法的秘钥进行加密通信。至此整个Https协议通信结束(包含:建立连接、通信协议协商、证书认证、密钥传递、加密通信)

Https证书认证在Android中应用

在Android4.2(Jelly Bean)开始,Android平台目前包含在每个版本中更新的100多个CA(证书授权机构)。CA具有一个证书个一个私钥,这点与服务器相似。为服务器颁发证书时,CA使用其私钥对证书进行签名。然后客户端可以使用CA公钥对服务器证书内签名数据进行验签,以此来确认服务器证书是否是客户端CA颁发的有效证书。

HTTPS示例

HTTPS通信所用到的证书由CA提供,需要在服务器中进行相应的设置才能生效。

HttpURLConnection中已经支持了Https证书验证功能且默认为 SSLSocketFactory,只要是通过知名CA机构签发的证书,那么,可以使用下面简单代码发起安全的请求,因为Android平台已经包含了该知名CA。

URL url = new URL("https://wikipedia.org");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
验证服务器证书常见问题

不过还有一些注意事项

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

出现这种情况原因有很多,其中包括:

  1. 未知CA(颁发服器器证书的CA未知)
  2. 自签名证书(服务器证书不是CA颁发,而是自签名生成的)
  3. 缺少中间证书授权机构(服务器配置缺少中间CA)

针对上面情况讨论如何解决,同时保持与服务器的连接出于安全状态

未知CA颁发服务器证书如何验签?
  • 问题描述

    在这种情况下,由于您具有系统不信任的 CA,将发生 SSLHandshakeException。原因可能是您有一个由 Android 尚不信任的新 CA 颁发的证书,或您的应用在没有 CA 的较旧版本上运行。CA 未知的原因通常是因为它不是公共 CA,而是政府、公司或教育机构等组织颁发的仅供自己使用的私有 CA。

  • 解决方案

    首先需要办法证书的未知CA提供公钥证书,让HttpsURLConnection 来信任您指定的 CA证书(而非系统默认的CA集)具体操作:

    1. 使用InputStrem获取一个特定的CA证书

    2. 用该CA证书创建keyStore

    3. KeyStore创建和初始化TrustManager

      TrustManager是系统用来验证来自服务器证书的工具,可以使用一个或多个CA证书从KeyStore创建TrustManager,而这些创建的TrustManager仅信任这些CA

    4. 通过TrustManager来初始化SSLContext

      SSLContext它会提供一个 SSLSocketFactory,您可以用来替换来自 HttpsURLConnection 的默认 SSLSocketFactory。这样一来,连接将使用您的 CA 验证证书。

    5. 替换HttpsURLConnection中的默认的SSLSocketFactory为SSLContext 创建的SSLSocketFactory

    /**
     *1.从本地路径加载创建证书
     */
    InputStream caInput = new BufferedInputStream(new FileInputStream("server.crt"));
    //约定证书公钥格式为x.509来实例化证书工厂类
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    //生成本地证书
    Certificate ca;
    try {
         ca = cf.generateCertificate(caInput);
    } finally {
         caInput.close();
    }
    /**
     *2.通过CA证书创建一个包含可信ca的密钥存储库keyStore
     */
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);
    
    /**
     *3.通过keystore初始化TrustManagers
     */
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);
    TrustManagers mTrustManagers = tmf.getTrustManagers()
          
    /**
     *4.通过TrustManager来初始化SSLContext 
     */
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, mTrustManagers, null);
    
    /**
     *5.替换HttpsURLConnection中默认的SSLSocketFactory,至此连接将使用您的 CA 来验证证书
     */
    URL url = new URL("https://certs.cac.washington.edu/CAtest/");
    HttpsURLConnection urlConnection =(HttpsURLConnection)url.openConnection();
    urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
    ...
    
自签名服务器证书如何验签?
  • 问题描述

    如果使用自签名的服务器证书,也就是服务器充当自己的 CA。这与上面的未知CA情况相似,因此您可以使用前面介绍的方法。当然你也可以通过重写校验证书链TrustManager中的方法checkServerTrusted()来使用指定CA证书对服务器证书进行验证。

  • 解决方案

    首先需要服务端提供自签名的公钥证书,后续我们将使用此(windows(.cer)/linux(.crt))证书对服务器证书进行校验。具体步骤如下:

    1. 创建一个X509TrustManager接口实现类A,实现该接口内的checkServerTrusted()方法

    2. checkServerTrusted()方法中使用服务器提供的公钥的证书,来对服务器证书进行验证(使用证书内的公钥来验签服务器证书内通过私钥签名的数据)。

    3. 使用实现X509TrustManager接口的A类来初始化SSLContext对象

    4. 通过SSLContext对象提供的getSocketFactory()方法返回的SSLSocketFactory对象来覆盖HttpsURLConnection类默认的的SSLSocketFactory对象

    创建实现X509TrustManager接口checkServerTrusted()方法的TrustManagerImpl类,来对服务器证书进行验证。

    class TrustManagerImpl implements X509TrustManager {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s) 
              throws CertificateException {
              //TODO 用来验证客户端证书的方法,这里没有做双向验证因此忽略
            }
    
            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, 
                                           String authType)throws CertificateException {
              //TODO 用于验证服务器证书的方法
                try {
                    /**
                     *验证服务器证书链是否有效
                     */
                    if (x509Certificates == null) {
                        System.err.println("X509Certificate array is null");
                        return;
                    }
                    if (x509Certificates.length <= 0) {
                        System.err.println("X509Certificate is empty");
                        return;
                    }
                    if (null == authType || !authType.contains("RSA")) {
                        System.err.println("authType is not RSA");
                        return;
                    }
                    /**
                     *如果上述条件都通过,使用本地证书公钥验签服务器证书内私钥签名的数据(如果验签通过,
                     *说明该服务器证书是合法自签名证书)。
                     */
                     //从本地路径加载证书流
                     InputStream certInput = new 
                                 BufferedInputStream(context.getAssets().open("server.crt"));
                     //约定证书公钥格式为x.509来实例化证书工厂类
                     CertificateFactory certificateFactory = 
                                 CertificateFactory.getInstance("X.509");
                     //生成本地X509Certificate证书
                     X509Certificate localcertificate;
                     try {
                          localcertificate = (X509Certificate) 
                                      certificateFactory.generateCertificate(certInput);
                      } finally {
                          certInput.close();
                      }
                     //获取服务器证书链的根证书
                     X509Certificate certificate = x509Certificates[0];
                     //验证根证书有效期
                     certificate.checkValidity();
                     //使用本地证书公钥来验签服务器证书私钥签名的数据
                     certificate.verify(localCertificate.getPublicKey());
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (NoSuchProviderException e) {
                    e.printStackTrace();
                } catch (SignatureException e) {
                    e.printStackTrace();
                }
            }
    
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        }
    

    使用实现X509TrustManager接口的TrustManagerImpl类来初始化SSLContext对象

    SSLContext sslContext = SSlContext.getInstance("TLS");
    sslContext.init(null,new TrustManager[]{new TrustManagerImpl()},null);
    

    通过SSLContext对象提供的getSocketFactory()方法返回的SSLSocketFactory对象来覆盖HttpsURLConnection类默认的的SSLSocketFactory对象

    URL url = new URL("https://xxx.xxxx.xxxx");
    HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
    httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
    ....
    

至此,我们可通过自签名服务器证书及未知CA颁发证书实现与服务器https传输证书验证。

主机名验证常见问题

在验证SSL连接有两个关键环节,首先是上面验证证书是否来自值的信任的来源,还有就是主机名验证

常见错误如下:

java.io.IOException: Hostname 'example.com' was not verified
...
...
java.io.IOException: HTTPS hostname wrong:  should be <xx.xxx.xxx.xxx>

出现上述问题一个原因是服务器配置错误。配置服务器所使用的证书不具有与您尝试连接的服务器的主题或主题备用名称字段。在握手期间,如果请求URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接(如果是主机名一致是不会调用该函数,即已明确主机名有效,并且不需要您的帮助),如果回调内实现不恰当,默认接受所有域名,则有安全风险。

因此我们在实现的HostnameVerifier子类中,需要使用verify函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。

//反例
HostnameVerifier hnv=new HosernameVerifier(){
  @Override
  public boolean verify(String hostname,SSLSession session){
      return ture;//对URL与主机名不一致的回调,不做处理,直接接受所有域名,存在安全隐患
  }
}
//正列
HostnameVerifier hnv=new HosernameVerifier(){
@Override
public boolean verify(String hostname,SSLSession session){
    //对请求URL与主机名不一致的回调,我们做合法性验证,目标主机名是否为我们指定的一致
    if("youhostname".equals(hostname)){
        return true;
    }else{
        HostnameVerifier hv=HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify(hostname,session);
    }
  }
}

扩展

根证书、CA解释以及数字证书办法过程
  • CA(Certificate Authority):被称为证书授权中心,是数字证书发放和管理的机构。
  • 根证书:是CA认证中心给自己颁发的证书,是信任链的起始点。安装根证书意味着对这个CA认证中心的信任。
  • 数字证书颁发过程一般为:用户首先产生自己的密钥对,并将公共密钥及部分个人身份信息传送给CA认证中心。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来,然后,认证中心将发给用户一个数字证书,该数字证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息
根证书与中级(中间根)证书
  • 根证书

    根证书是由CA机构自己颁发,是颁发SSL证书的核心,是信任链的起始点。根证书库是下载客户端浏览器时预先加载根证书的合集。因此根证书是十分重要的,因为它可确保浏览器自动信任已使用私钥签名的SSL证书。

  • 中级(中间根)证书

    中间根证书,是由CA机构的根证书颁发。证书颁发机构(CA)不会直接从根目录颁发服务器证书(即SSL证书),因为这种行为是十分危险的,因为一旦发生错误颁发或者需要撤销root,则使用root签名的每个证书都会被撤销信任。

    因此,为了规避上述风险,CA机构一般会引用中间根。CA机构使用其私钥对中间根进行签名,使浏览器信任中间根。然后CA机构使用中间根证书的私钥来签署用户申请的SSL证书。这种中间根的形式可以重复多次,即使用中间根签署另一个中间件,然后CA机构通过中间件签署SSL证书。

    计算机中内置的是最顶级机构的根证书,不过不用担心,根证书的公匙在子级也是适用的(通过中级证书颁发机构(中间根)签发的证书也可通过根证书公钥验签)。

知名CA机构
TCP为什么要进行三次握手,一次或者两次不行吗?
  • 首先考虑一次握手

    TCP区别于UDP,TCP是面向连接的协议,因此在通信之前,需要确保客户端与服务器已经建立起了可靠的连接。而一次握手(即客户端向服务器发出连接请求,没有收到服务器的应答)无法确认是否已经建立连接,因此一次握手不符合TCP面向连接的设计思想。

  • 再来看看两次握手

    两次握手即客户端向服务器发出连接请求,服务器接收到并告诉客户端允许连接这是两次握手。那么TCP为什么不使用两次握手,两次握手会出现什么问题呢?

    假如,客户端第一次发送一个TCP请求连接包SYNA+Seq,由于网络原因没有到达服务器。此时客户端会会将此次请求认为无效,进而重新发送一个请求连接包SYNB+Seq,此时服务收到该连接请求包SYNB+Seq,为该请求申请连接资源,并应答一个SYN+ACKB。与此同时客户端第一次发送的连接请求包SYNA+Seq延迟后到达了服务器,此时服务器认为是一个新的连接请求,所以服务器又为这个连接申请资源并返回Seq+ACKA。但是客户端会认为这个ACKA是无效的,并不会理会。但是服务器会一直为这个连接维持着资源,造成资源浪费。

  • 三次握手,可解决连接资源浪费问题

    我们再来看看三次握手时如何解决上述连接资源浪费问题的,当客户端第二次发送的连接请求SYNB+Seq包,到达服务器,服务器应答SYN+ACKB后,客户端紧接着回复一个ACK,告诉服务器,我收到你的允许连接应答了,咱门可以进行连接了。如果此时客户端第一次发送的请求连接SYNA+Seq包到达了服务器,服务器为该连接申请资源,并应答客户端SYN+ACKA,此时客户端认为该应答时无效的,不予理会。此时服务器为该连接申请的资源一直迟迟等不到客户端的反馈,服务器也认为该连接时无效的,便会释放相关连接资源。

    但是这时会有一个问题,就是客户端在完成两次握手之后,便认为连接已经建立,而第三次握手可能由于网络原因在传输中丢失,服务器便会认为连接时无效的,这时候,如果客户端向服务器写数据,服务器将以RST(重置连接)包应答,这时客户端便可感知到服务器的错误。

    因此TCP使用三次握手来建立可靠的连接,可避免服务器申请无效的连接资源。

TCP四次挥手

在这里插入图片描述
挥手之前主动释放连接的客户端结束ESTABLISHED阶段。随后开始四次挥手

  1. 客户端向服务器发送释放连接TCP包,此时,客户端进入FIN-WAIT-1(终止等待1)阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。数据包内容包含:

    • 标志位FIN:表示请求释放连接
    • 序列号Seq:客户端发送数据的序列号(TCP规定,FIN报文段即使不携带数据,也要消耗一个序号

    注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文

  2. 服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT(半关闭状态)阶段并返回一段TCP报文,其中:

    • 标志位ACK:表示接收到客户端发送的释放连接的请求
    • 确认序列号ack:服务端返回给客户端的确认序列号(客户端的序列号+1)
    • 序列号Seq:服务器响应客户端的TCP包序列号
  3. 客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段。

  4. 服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据其中:

    • 标志位FIN
    • 标志位ACK:表示已经准备好释放连接了(注意:这里的ACK并不是确认收到服务器端报文的确认报文
    • 确认序列号ack:表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。
    • 序列号Seq
  5. 客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:

    • 标志位ACK:表示接收到服务器准备好释放连接的信号
    • 确认序列号ack:表示是在收到了服务器端报文的基础上,将其序号Seq值+1作为本段报文确认号的值。
    • 序列号Seq:表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。

    注意:注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

  6. 服务器端收到从客户端发出的TCP报文之后结束LAST-ACK阶段,进入CLOSED阶段。由此正式确认关闭服务器端到客户端方向上的连接。

    客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。

PS:为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

参考文章:

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值