JAVA安全管理器和类装载器

SecurityManager相关概念理解


Java语言具有完善的安全框架,从编程语言,编译器、解释程序到Java虚拟机,

都能确保Java系统不被无效的代码或敌对的编译器暗中破坏,基本上,它们保证

了Java代码按预定的规则运作。但是,当我们需要逾越这些限制时,例如,读写

文件,监听和读写Socket,退出Java系统等,就必须使用数字签名或安全策略文

件(*.Policy)。
  在企业内部网中,本文提出了使用安全策略文件来设置java程序权限的一种

简单的方法。由于企业内部网中各台计算机的位置、用途和安全性明确,更适于

使用安全策略文件来设置java的权限,软件的安装、设置、升级和迁移都非常的

方便,并且,还可以和数字签名配合使用,更重要的是,可以细分每个java程序

的权限,使用起来灵活方便。
一. Java中安全策略的概念
  Java应用程序环境的安全策略,详细说明了对于不同的代码所拥有的不同资

源的许可,它由一个Policy对象来表达。为了让applet(或者运行在

SecurityManager下的一个应用程序)能够执行受保护的行为,例如读写文件,

applet(或Java应用程序)必须获得那项操作的许可,安全策略文件就是用来实现

这些许可。
  Policy对象可能有多个实体,虽然任何时候只能有一个起作用。当前安装的

Policy对象,在程序中可以通过调用getPolicy方法得到,也可以通过调用setPolicy

方法改变。Policy对象评估整个策略,返回一个适当的Permissions对象,详细说

明那些代码可以访问那些资源。
  策略文件可以储存在无格式的ASCII文件,或Policy类的二进制文件,或数据

库中。本文仅讨论无格式的ASCII文件的形式。
二. Policy文件的格式
  为了能够更好地理解下面的内容,建议在阅读时参照\jdk1.2

\jre\lib\security\java.policy文件和\jdk1.2\jre\lib\security\java.security文件的内

容。
  1. Policy文件的语法格式与说明
  一个Policy文件实质上是一个记录列表,它可能含有一个“keystore”记录,以

及含有零个或多个“grant”记录。其格式如下:
keystore "some_keystore_url", "keystore_type";
grant [SignedBy "signer_names"] [, CodeBase "URL"] {
Permission permission_class_name
[ "target_name" ] [, "action"] [, SignedBy "signer_names"];
Permission ...};
  1.1"keystore"记录
  一个keystore是一个私有密钥(privatekeys)数据库和相应的数字签名,例

如X.509证书。Policy文件中可能只有一条keystore记录(也可能不含有该记录)

,它可以出现在文件中grant记录以外的任何地方。Policy配置文件中指定的

keystores用于寻找grant记录中指定的、签名者的公共密钥(publickeys),如果

任何grant记录指定签名者(signer_names),那么,keystore记录必须出现在

policy配置文件中。
  "some_keystore_url"是指keystore的URL位置,"keystore_type"是指

keystore的类型。第二个选项是可选项,如果没有指定,该类型则假定由安全属

性文件(java.security)中的"keystore.type"属性来确定。keystore类型定义了

keystore信息的存储和数据格式,用于保护keystore中的私有密钥和keystore完

整性的算法。SunMicrosystems支持的缺省类型为“JKS”。
  1.2"grant"记录
  在Policy文件中的每一个grant记录含有一个CodeSource(一个指定的代码)

及其permission(许可)。
  Policy文件中的每一条grant记录遵循下面的格式,以保留字“grant”开头,表

示一条新的记录的开始,“Permission”是另一个保留字,在记录中用来标记一个

新的许可的开始。每一个grant记录授予一个指定的代码(CodeBase)一套许可

(Permissions)。
  permission_class_name必须是一个合格并存在的类名,例如

java.io.FilePermission,不能使用缩写(例如,FilePermission)。
  target_name用来指定目标类的位置,action用于指定目标类拥有的权限。
  target_name可以直接指定类名(可以是绝对或相对路径),目录名,也可

以是下面的通配符:
directory/* 目录下的所有文件
*当前目录的所有文件
directory/-目录下的所有文件,包括子目录
- 当前目录下的所有文件,包括子目录
《ALL FILES》文件系统中的所有文件
对于java.io.FilePermission,action可以是:
read, write, delete和execute。
对于java.net.SocketPermission,action可以是:
listen,accept,connect,read,write。
  1.3 Policy文件中的属性扩展(Property Expansion)
  属性扩展与shell中使用的变量扩展类似,它的格式为:
"${some.property}"
实际使用的例子为:
permission java.io.FilePermission "${user.home}", "read"; "${user.home}"的值

为"d:\Project",
因此,下面的语句和上面的语句是一样的:
permission java.io.FilePermission " d:\Project ", "read";
三. 实例
  当初始化Policy时,首先装载系统Policy,然后再增加用户Policy,如果两者

都不存在,则使用缺省的Policy,即原始的沙箱模型。
  系统Policy文件的缺省位置为:
{java.home}/lib/security/java.policy (Solaris)
{java.home}\lib\security\java.policy (Windows)
用户Policy文件的缺省位置为:
{user.home}/.java.policy (Solaris)
{user.home}\.java.policy (Windows)
  其实,在实际使用中,我们可能不会象上面介绍的那么复杂,特别是在不使

用数字签名时。这时,我们完全可以借鉴JDK1.2提供给我们的现成的\jdk1.2

\jre\lib\security\java.policy文件,根据我们的需要作相应的修改,本文就针对不

使用数字签名情况详细说明安全策略文件的用法。
  下面,是一个完整的在Windows 95/98/NT下使用的.java.policy文件。在文

件中,分别使用注释的形式说明了每个“permission”记录的用途。
// For LanServerTalk.java and LanClientTalk.java
grant {
//对系统和用户目录“读”的权限
permission java.util.PropertyPermission "user.dir", "read";
permission java.util.PropertyPermission "user.home", "read";
permission java.util.PropertyPermission "java.home", "read";
permission java.util.PropertyPermission "java.class.path", "read";
permission java.util.PropertyPermission "user.name", "read";
//对线程和线程组的操作权限
permission java.lang.RuntimePermission "modifyThread";
permission java.lang.RuntimePermission "modifyThreadGroup";
//操作Socket端口的各种权限
permission java.net.SocketPermission "-", "listen";
permission java.net.SocketPermission "-", "accept";
permission java.net.SocketPermission "-", "connect";
permission java.net.SocketPermission "-", "read";
permission java.net.SocketPermission "-", "write";
//读写文件的权限
permission java.io.FilePermission "-", "read";
permission java.io.FilePermission "-", "write";
//退出系统的权限,例如System.exit(0)
permission java.lang.RuntimePermission "exitVM";
};
四. java.policy文件的使用
  对于windows 95/98/NT,使用.java.policy文件的方法主要有下面两种。
  1. 使用缺省目录
  我们可以简单地将编辑好的.java.policy文件拷贝到windows95/98/NT的

HOME目录,这时,所有的applet(或Java应用程序)可能都拥有某些相同的权限,

使用起来简单,但不灵活(例如:对于java.io.FilePermission,其目标类的

target_name必须使用绝对路径),如果不是在企业内部网中使用,还可能存在

一定安全隐患。
  2. 在命令行中指定
  在命令行,如果我们希望传递一个Policy文件给appletviewer,还可以使

用"-J-Djava.security.policy"参数来指定policy的位置:
appletviewer -J-Djava.security.
policy=pURL myApplet
  pURL为Policy文件的位置。下面,是一个实际的例子,以当前目录

的.java.policy文件所指定的安全策略运行当前目录的LanServerTalk.html(文件

中装载并运行LanServerTalk.java):
appletviewer -J-Djava.security.policy
=.java.policy LanServerTalk.html
  这种方法使用灵活,特别是作为一个软件包在企业内部网中发布时,安装、

设置和迁移软件,基本无须修改Policy文件的内容,使用起来相当简单,而且,

安全许可的范围控制较精细

五 SecurityManager示例
一般而言,Java程序启动时并不会自动启动安全管理器,可以通过以下两种方法

启动安全管理器:

①  一种是隐式,启动默认的安全管理器最简单的方法就是:直接在启动命令中

添加-Djava.security.manager参数即可。

②  一种是显式,实例化一个java.lang.SecurityManager或继承它的子类的对象

,然后通过System.setSecurityManager()来设置并启动一个安全管理器。

在启动安全管理器时可以通过-Djava.security.policy选项来指定安全策略文件。

如果没有指定策略文件的路径,那么安全管理器将使用默认的安全策略文件,它

位于%JAVA_HOME%/jre/lib/security目录下面的java.policy。需要说明一下的是

,=表示这个策略文件将和默认的策略文件一同发挥作用;==表示只使用这个策

略文件。policy文件包含了多个grant语句,每一个grant描述某些代码拥有某些操

作的权限。在启动安全管理器时会根据policy文件生成一个Policy对象,任何时候

一个应用程序只能有一个Policy对象。

那么如何才能实现自己的安全管理器,并且配置权限呢?下面将通过一个简单的

例子阐明实现步骤,一般可以分为以下两步:①创建一个SecurityManager子类

,并根据需要重写一些方法。②根据应用程序代码的权限需要配置策略文件。如

果使用默认安全管理器则省略第一步,下面用个例子说明安全管理器的使用:

public class SecurityManagerTest {

        public static void main(String[] args) throws FileNotFoundException {

         System.out.println("SecurityManager: " + System.getSecurityManager

());

         FileInputStreamfis = new FileInputStream("c:\\protect.txt");

         System.out.println(System.getProperty("file.encoding"));

        }

}

分下面几种情况运行程序:

(1) 假如不添加启动参数直接运行,则相当于没有启动安全管理器,

SecurityManager打印出来为null,且能正确读取protect.txt文件跟file.encoding属

性。

(2)  添加启动参数-Djava.security.manager-

Djava.security.policy=c:/protect.policy,俩参数分别代表启动默认安全管理器和

指明策略配置文件路径。此时SecurityManager打印出来为不为null,但由于此时

protect.policy里面并没有做任何授权,所以在读取文件的时就抛出

AccessControlExcepti on异常。

(3)  在protect.policy文件添加以下授权语句,

grant {

permissionjava.io.FilePermission "c:/protect.txt", "read";

};

此时SecurityManager不为空,并且有权限读取protect.txt文件,但最终还是会抛

一个AccessControlException异常,因为并没有权限读取file.encoding系统属性。

(4)  将protect.policy授权语句改为如下:

grant {

permissionjava.io.FilePermission "c:/protect.txt", "read";

permissionjava.util.PropertyPermission "file.encoding", "read";

};

这次读取文件跟读取系统属性的权限都有了,程序正常运行,不再抛出安全异常

由上面几种情况我们清晰了解安全管理器的使用,通过简单地配置策略文件能达

到应用安全的管理。Java的Permission类是用来定义类所拥有的权限,Java本身

包括了一些 Permission类,如下:

java.security.AllPermission
 所有权限的集合
 
java.util.PropertyPermission
 系统/环境属性权限
 
java.lang.RuntimePermission
 运行时权限
 
java.net.SocketPermission
 Socket权限
 
java.io.FilePermission
 文件权限,包括读写,删除,执行
 
java.io.SerializablePermission
 序列化权限
 
java.lang.reflect.ReflectPermission
 反射权限
 
java.security.UnresolvedPermission
 未解析的权限
 
java.net.NetPermission
 网络权限
 
java.awt.AWTPermission
 AWT权限
 
java.sql.SQLPermission
 数据库sql权限
 
java.security.SecurityPermission
 安全控制方面的权限
 
java.util.logging.LoggingPermission
 日志控制权限
 
javax.net.ssl.SSLPermission
 安全连接权限
 
javax.security.auth.AuthPermission
 认证权限
 
javax.sound.sampled.AudioPermission
 音频系统资源的访问权限
 


JAVA安全管理器简介


安全管理器是一个单独的对象,在java虚拟机中,它在访问控制-对于外部资源

的访问控制-起到中枢作用
     如果光看概念可能并不能很好的理解,或者说比较抽象,下面是ClassLoader

其中的一个构造函数,先简单的看看它在初始化ClassLoader之前会做一些什么

操作
protected ClassLoader(ClassLoader parent) {
 SecurityManager security = System.getSecurityManager();
 if (security != null) {
     security.checkCreateClassLoader();
 }
 this.parent = parent;
 initialized = true;
    }

这个构造函数的第一话(当然还有隐式调用)就是System.getSecurityManager();这

行代码返回的就是一个安全管理器对象security,这个对象所属的目录为

java.lang.SecurityManager。

这个构造函数先判断如果已经安装了安全管理器security(在前面类装载器的章节

,我们提到过,类装载器和安全管理器是可以由用户定制的,在这里有了体现吧

!!既然有System.getSecurityManager();你当然也应该猜到有

System.setSecurityManager();),也就是安全管理器不为空,那么就执行校验,

跳到checkCreateClassLoader();看看他做的是什么操作

  public void checkCreateClassLoader() {
 checkPermission

(SecurityConstants.CREATE_CLASSLOADER_PERMISSION);
    }

这里又调用了另外一个方法,从方法名字上,就可以猜到这个方法是用来校验权

限的,校验是否有创建ClassLoader的权限,再跳到checkPermisson方法里

   public static void checkPermission(Permission perm)
   throws AccessControlException
    {
 //System.err.println("checkPermission "+perm);
 //Thread.currentThread().dumpStack(); if (perm == null) {
     throw new NullPointerException("permission can't be null");
 } AccessControlContext stack = getStackAccessControlContext();
 // if context is null, we had privileged system code on the stack.
 if (stack == null) {
     Debug debug = AccessControlContext.getDebug();
     boolean dumpDebug = false;
     if (debug != null) {
  dumpDebug = !Debug.isOn("codebase=");
  dumpDebug &= !Debug.isOn("permission=") ||
      Debug.isOn("permission=" + perm.getClass().getCanonicalName());
     }     if (dumpDebug && Debug.isOn("stack")) {
  Thread.currentThread().dumpStack();
     }     if (dumpDebug && Debug.isOn("domain")) {
  debug.println("domain (context is null)");
     }     if (dumpDebug) {
  debug.println("access allowed "+perm);
     }
     return;
 } AccessControlContext acc = stack.optimize();
 acc.checkPermission(perm);
    }
}

上面的这个方法有些代码比较难以理解,我们不用每行都读懂(这个方法涉及的

东西比较多,它涉及到了代码签名认证,策略还有保护域,这些我们在后一节中

会详细的讲解,看不懂先跳过),看它的注解// if context is null, we had privileged

system code on the stack.意思就是如果当前的访问控制器上下文为空,在栈上

的系统代码将得到特权,找到acc.checkPermission(perm);再跳进去找到下面这

段代码

/*
  * iterate through the ProtectionDomains in the context.
  * Stop at the first one that doesn't allow the
  * requested permission (throwing an exception).
  *
  */ /* if ctxt is null, all we had on the stack were system domains,
    or the first domain was a Privileged system domain. This
    is to make the common case for system code very fast */ if (context ==

null)
     return; for (int i=0; i< context.length; i++) {
     if (context[i] != null &&  !context[i].implies(perm)) {
  if (dumpDebug) {
      debug.println("access denied " + perm);
  }  if (Debug.isOn("failure") && debug != null) {
      // Want to make sure this is always displayed for failure,
      // but do not want to display again if already displayed
      // above.
      if (!dumpDebug) {
   debug.println("access denied " + perm);
      }
      Thread.currentThread().dumpStack();
      final ProtectionDomain pd = context[i];
      final Debug db = debug;
      AccessController.doPrivileged (new PrivilegedAction() {
   public Object run() {
       db.println("domain that failed "+pd);
       return null;
   }
      });
  }
  throw new AccessControlException("access denied "+perm, perm);
     }
 }

什么都不用看,就看最上面的那段注解,意思是遍历上下文中的保护域,一旦发

现请求的权限不被允许,停止,抛出异常,到这里我们有一个比较清晰的概念了

,安全管理器就是用来控制执行权限的,而上面的这段代码中有一个很重要的类

AccessController,访问控制器,还有一个很重要的名词保护域(保护域我们在前

面一节也有简单的带过一下,是不是有点印象),这些可能现在听有点模糊,不

要担心,暂时不要管,后面一章节慢慢的会对他们进行讲解。


好了了解安全管理器是做什么的之后,接下来,来做一个下的实验,先来验证,

默认安全管理是没有被安装的,接着来试着把他安装上去。在我的环境中我是没

有安装默认的安全管理器的,也没有基于默认的安全管理器写自己的安全管理器

,如果需要打开的话,可以在程序显示的安装安全管理器,同样可以让它自动安

装默认的安全管理器(给jvm加上-Djava.security.manager就可以了。

下面我们用熟悉的ecplise写一个简单的demo来看看安装前后的区别,在下一节

中,会详细的来学习代码签名认证和策略,并写一个自己的安全管理器。

public static void main(String[] args) {
  System.out.println(System.getSecurityManager());
}

运行这个main函数,输出什么?是的输出null,这个时候我们没有安装默认的安

全管理器

重新换个方式运行,在ecplise里右键--Run As--Run Configuration--Arguments,

在VM arguments的栏目里输入

-Djava.security.manager。在点击Run,这个时候看到什么?

输出:securityManager的对象名。这个时候默认的安全管理器就被安装上了。

 

总结:

      在java虚拟机中,它在访问控制-对于外部资源的访问控制-起到中枢作用

 


JAVA类装载器


下面我们先来动态扩展一个类装载器,当然这只是一个比较小的demo,旨在让

大家有个比较形象的概念。

       第一步,首先定义自己的类装载器,从ClassLoader继承,重写它的

findClass方法,至于为什么要这么做,大家如果看过笔记三就知道,双亲委托模

式下,如果parent没办法loadClass,bootStrap也没把办法loadClass的时候,jvm

是会调用ClassLoader对象或者它子类对象的findClass来装载。

package com.yfq.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


public class MyClassLoader extends ClassLoader {

 @Override
 protected Class<?> findClass(String name) throws

ClassNotFoundException {
  byte[] data = getByteArray(name); 
        if (data == null) { 
            throw new ClassNotFoundException(); 
        } 
        return defineClass(name, data, 0, data.length);
 }
 
 
 private byte[] getByteArray(String name){
  String filePath =   name.replace(".", File.separator);
  byte[] buf = null;
  try {
   FileInputStream in = new FileInputStream

(filePath);
   buf = new byte[in.available()];
   in.read(buf);
  } catch (FileNotFoundException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
  return buf;
 }

}

第二步,定义一个类,专门用于被装载,这里我们定义了一个静态代码块,待会

用到它

package com.yfq.test;

public class TestBeLoader {
 static{
  System.out.println("TestBeLoader init");
 }
 public void sayHello(){
  System.out.println("hello");
 }
}

第三步,定义一个有main函数入口的public类来做验证
package com.yfq.test;

public class TestClassLoaderDemo {
 public static void main(String[] args) throws InstantiationException,

IllegalAccessException {
  Class thisCls = TestClassLoaderDemo.class;
  MyClassLoader myClassLoader = new MyClassLoader();
  System.out.println(thisCls.getClassLoader());
  System.out.println(myClassLoader.getParent());
  try {
   //用自定义的类装载器来装载类,这是动态扩展的一

种途径
   Class cls2 = myClassLoader.loadClass

("com.yfq.test.TestBeLoader");
   System.out.println(cls2.getClassLoader());
   TestBeLoader test=(TestBeLoader)

cls2.newInstance();
  } catch (ClassNotFoundException e) {
   e.printStackTrace();
  }
 }
}

第四步,查看运行结果

sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$AppClassLoader@19821f
TestBeLoader init
说明:

        第一个输出:装载TestClassLoaderDemo的类是AppClassLoder

       第二个输出:装载myClassLoader的装载器也是AppClassLoader,这里也验

证了我们笔记三讲的,在同个线程中,动态连接模式会运用当前线程的类加载器

来加载所需的class文件,因为第一个和第二个输出是同一个对象的对象名

       第三个输出:是TestBeLoader的类加载器,这个输出验证了,双亲委托模

式下的动态连接模式,由于myClassLoader是由AppClassLoader装载的,所以它

会委托自己的parent来装载com.yfq.test.TestBeLoader这个类,加载成功所以就

不再调用自己的findClass方法,这个我们在笔记三有做过简要的讨论。

       第四个输出:如果我们将TestBeLoader test=(TestBeLoader)

cls2.newInstance();这句话注掉,则不会有第四个输出,为什么?

                             类的装载大致分为三步,装载,连接,初始化。而初始化

这一步,是在我们第一次创建对象的时候才进行初始化分配内存,这一点需要注

意,并不是class被load内存后就立刻初始化。

**************
自定义的安全管理器
安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就

是checkPerssiom这个方法里又调用AccessController的checkPerssiom方法,访问

控制器AccessController的栈检查机制又遍历整个PerssiomCollection来判断具体

拥有什么权限一旦发现栈中一个权限不允许的时候抛出异常否则简单的返回,这

个过程实际上比我的描述要复杂得多,这里我只是简单的一句带过,因为这里涉

及到很多比较后面的知识点。

       下面来尝试一下写一个非常简单的demo,旨在让你有一个比较形象的思维

,不会在概念上打转。

       第一步,定义一个类继承自SecurityManger重写它的checkRead方(如果你

有兴趣可以先跳到super.checkRead(file, context);看看,当然不看也没有关系,

我们后面的章节会基于这个demo做扩展的时候也会讲到)。

package com.yfq.test;

public class MySecurityManager extends SecurityManager {

 @Override
 public void checkRead(String file) {
  //super.checkRead(file, context);
  if (file.endsWith("test")) 
     throw new SecurityException("你没有读取的本文件的权限"); 
 }
 
}

第二步,定义一个有main函数的public类来验证自己的安全管理器是不是器作用

了。
package com.yfq.test;

import java.io.FileInputStream;
import java.io.IOException;

public class TestMySecurityManager {
 public static void main(String[] args) {
  System.setSecurityManager(new MySecurityManager());
  try {
   FileInputStream fis = new FileInputStream

("test");
   System.out.println(fis.read());
  } catch (IOException e) {
   e.printStackTrace();
  }

 }
}

第三步,运行代码查看控制台输出

Exception in thread "main" java.lang.SecurityException: 你没有读取的本文件的

权限
 at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:9)
 at java.io.FileInputStream.<init>(FileInputStream.java:100)
 at java.io.FileInputStream.<init>(FileInputStream.java:66)
 at com.yfq.test.TestMySecurityManager.main(TestMySecurityManager.java:10)

 从上面的异常我们发现,安全管理器起作用了。读过笔记四的人应该会发现,

这里我们用到了一个笔记四提到的方法:System.setSecurityManager(new

MySecurityManager());这个是安装安全管理器的另外一种方法,笔记四中我们曾

经用-Djava.security.manager安装过默认的安全管理器,有印象不?

 

拓展内容:
好了,我们的安全管理器是怎么被执行的呢?如果你有兴趣可以继续往下看一下

,也可以跳过,这里只是简单的介绍一下,也是本人习惯的学习思路

直接跳到FileInputStream的构造函数里,下面贴出代码,简单阅读一下


  public FileInputStream(File file) throws FileNotFoundException {
 String name = (file != null ? file.getPath() : null);
 SecurityManager security = System.getSecurityManager();
 if (security != null) {
     security.checkRead(name);
 }
        if (name == null) {
            throw new NullPointerException();
        }
 fd = new FileDescriptor();
 open(name);
    }

发现没?它首先执行SecurityManager security = System.getSecurityManager();

,然后再调用security的checkRead方法,就是这么简单。

 

如果你还有兴趣那么继续往下读,在使用java的File时,你是否用过setWritable

(boolean, boolean),让你可以指定创建文件的权限,学习了安全管理器之后你

有没有有豁然开朗的感觉,它是怎么实现的,相信你已经猜到了,没有错就是安

全管理器设置权限啦。下面贴出它的代码,同时也引入一个新的概念Permission

    public boolean setWritable(boolean writable, boolean ownerOnly) {
 SecurityManager security = System.getSecurityManager();
 if (security != null) {
     security.checkWrite(path);
 }
 return fs.setPermission(this, FileSystem.ACCESS_WRITE, writable,

ownerOnly);
    }

Permisson就是权限的意思,它仅仅取出安全管理器然后将文件的权限设置了一

下而已,这个也是后面所有关于权限的一个不可或缺的类!


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值