零、前言
Java代码审计自然需要Java语言的基本功底,这方面网上有很多Java教程,也有大量的书可以参考。理论上有其他语言的基础,学习Java也不是很困难,但是Java尤其是框架有一些比较“有意思”、“很强大”而又“不太好理解”的特性,这里尝试对其进行讲解,为Java代码审计做一个知识铺垫。
一、知识特性
1、泛型
一个类的成员变量,一个函数中的参数,都具有一种数据类型,可以为基本数据类型(如int类型)或者引用类型(如Interger类型)。假设我们想描述平面坐标的一个点,那么我们创建一个Point类,这个类要包含表示X坐标和Y坐标的成员变量:
public class Point<T> { private T x; private T y; public T getX() { return x; } public void setX(T x) { this.x = x; } public T getY() { return y; } public void setY(T y) { this.y = y; } }
如果我们发现int类型描述平面坐标上的点精度不够,希望换做double类型,那么代码应该大部分是类似的,可是上面的代码我们却无法复用。我们只能再定义一个非常类似的DoublePoint重新定义为:
public class DoublePoint{ private double x; private double y; public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } }
能否只定义一个类就能满足坐标既可能为int类型,也可能为double类型的情况呢?如果可以的话将可以让代码更加通用,减少大量的重复代码。答案是肯定的,这个时候你需要泛型。在使用泛型时,我们可以把类型作为参数传入到泛型类中。类似于把参数传入到方法中一样。我们来实现一个通用的泛型Point类:
public class Point<T> { private T x; private T y; public T getX() { return x; } public void setX(T x) { this.x = x; } public T getY() { return y; } public void setY(T y) { this.y = y; } }
此时Point成为了一个泛型类,T是则是类型参数,T具体是什么类型那要看程序运行的时候我们传入什么类型给他。因此你可以直观的理解“泛型”两个字,“泛”即是指一切,定义好之后它可以充当所有类型。使用泛型类时,注意实际传入的类型参数不能是原生类型,必须是引用类型,因此如果希望传入int类型的话,那么需要传入int对应的包装类Interger。对应地,double类型则要传入包装类Double。
public class Test{ public static void main(String[] args){ // 坐标为int类型,把int类型的包装类Integer作为参数传入泛型类中 Point<Integer> point1 = new Point<Integer>(); point1.setX(1); point1.setY(1); // 坐标为double类型,把double类型的包装类Double作为参数传入泛型类中 Point<Double> point2 = new Point<Double>(); point2.setX(3.456); point2.setY(4.789); } }
泛型应用的典型的例子就是容器类,也叫集合类。以下面这个例子为例,我们定义一个容器类Container,这个容器中可以存放各种类型的对象,可以使用泛型类实现这一特性。
public class Container<T> { private T variable; public Container () { variable = null; } public Container (T variable) { this.variable = variable; } public T get() { return variable; } public void set(T variable) { this.variable = variable; } public static void main(String[] args) { Container<String> stringContainer = new Container<String>(); stringContainer.set("this is a string"); } }
我们实例化Container对象时,只需设置它使用的类型,如:
Container<String> stringContainer = new Container<String>(); stringContainer.set("this is a string");
Java已经帮我们内置很多原生的集合类,比如ArrayList、LinkedList(List接口的实现)和HashMap(Map接口的实现),当我们声明一个集合时:
ArrayList<Integer> nums = new ArrayList<Integer>(); Map<Long, String> posts = new HashMap<Long, String>();
2、反射
反射是一种动态获取信息以及动态调用对象方法的机制。在程序运行状态中,通过反射能够知道某个类具有哪些属性和方法;能够访问某一个对象的方法和属性。具体来说,反射机制主要提供了以下功能:
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的方法;
- 生成动态代理。
Java中用class
这个关键字来声明一个类,同时Java中也有一个类对象叫 Class
,它表示类本身这个概念,每一个类都可以获取一个代表自身Class
对象,有以下三种方式进行获取
public class ReflectionTest { public static void main(String[] args) throws Exception { Class<?> class1 = null; Class<?> class2 = null; Class<?> class3 = null; // 第一种方式 class1 = Class.forName("com.java.ReflectionTest"); // 第二种方式 class2 = new ReflectionTest().getClass(); // 第三种方式 class3 = ReflectionTest.class; System.out.println("类名称 " + class1.getName()); System.out.println("类名称 " + class2.getName()); System.out.println("类名称 " + class3.getName()); } }
获得一个类的Class对象后我们可以完全操纵这个类,访问它的所有方法和属性(包括private限定符下的),以调用类方法为例
public class ReflectionTest { public static void main(String[] args) throws Exception { Class<?> clazz = Class.forName("com.java.ReflectionTest"); // 通过反射获取方法并调用方法 Method method = clazz.getMethod("reflect"); method.invoke(clazz.newInstance()); // 通过反射获取方法,并进行传参调用方法 method = clazz.getMethod("reflect", int.class, String.class); method.invoke(clazz.newInstance(), 20, "张三"); } public void reflect() { System.out.println("Java 反射机制 - 调用无参方法"); } public void reflect(int age, String name) { System.out.println("Java 反射机制 - 调用有参数方法"); System.out.println("age -> " + age + ". name -> " + name); } }
主要就是获得方法和执行方法两个步骤,在上一篇博客中介绍的Strust2反序列化漏洞就是通过连续反射去调用Runtime.getRuntime().exec(cmd)执行命令的。
3、控制反转(IOC)和依赖注入(DI)
控制反转(Inversion of Control)本质上是一种思想而不是具体的技术。它想要做的事情或者说达到的目的正如它的字面意思一样,将控制反转过来!那么我们要先明白,在一般情况下是谁控制谁。在传统的编程思想中,我们使用面向对象的编程方法,将想要表示的事务用类表达,逻辑实现的过程实际上是对类对象的声明和调用,整个过程都是由程序员去控制,这是一种很自然的做法。但是慢慢地,这种方式会显露出很多弊端,比如为了进行功能调用会在一个类中实例化其他类对象,或者一个类继承另外一个类,这些操作会造成各个模块代码的耦合度很高,而且类的实例化和赋值等操作全由程序员管理,使得代码很难维护和修改。
控制反转即是将类的实例化、赋值等操作全由容器来完成,也就是说将类的控制权从程序员手中转交到容器,此为反转。那么我们只要代码中指定的位置进行“标注”,容器会自动将我们需要类提供给我们,而我们只需要去使用它,专心于业务逻辑即可。
那么什么是依赖注入(Dependency Injection)呢,前面我们说控制反转是一种思想,依赖注入就是实现这种思想的具体方法。容器将上层代码需要的对象注入到需要的位置(这里一般通过配置文件说明或者使用注解标注),那么注入是如何实现的呢?使用的方法就是我们上文说的反射,反射能够动态的获取类对象,调用类属性和方法。下面有两个更好的解释:
一个形象化的解释:https://zhuanlan.zhihu.com/p/33492169
一个更形象化的解释:https://www.zhihu.com/question/23277575
4、注解
注解(Annotation)就像一种注释,不过注释是给人看的,而注解是给程序自己看的。它能够告诉程序当某中注解下的代码代表了什么含义,在执行的时候按照某种规约去执行。以上文的IOC和DI为例
@Component public class UserDao { ... }
这里定义了一个UserDao对象,使用@Component注解,它是Spring的一个注解,定义一个Spring Bean。启动了自动扫描后,被@Component标注的类都自动会注册为Spring Beans。
@Component public class UserService { @Autowired private UserDao userDao; ... }
然后在使用UserDao对象时,我们使用@Autowired注解,Spring框架会自动将这个我们需要的类对象装配(注入)到userDao变量中,我们不需要显示的声明。在学习注解时,不需要记住每个注解的含义,见到陌生的查一下即可,一般熟悉几个常用的即可。
一个形象的解释:https://blog.csdn.net/briblue/article/details/73824058
5、面向切面编程(AOP)
面向切面编程(Aspect Oriented Program)是一种很经典的编程思想,并不是什么新的事物,我们在开发程序时往往需要写各种各样的功能,这其中有一些是核心功能,有一些是通用功能。为了能够方便对核心/通用进行分离和组合,便促生了面向切面编程,这里的切面便是指“通用”。比如现在成熟的系统中一定会有日志记录功能,这并不是核心功能,但每个主要的功能或者程序运行的节点都要进行日志记录,如果写在一起势必会造成代码臃肿和耦合。根据切面的思想,就可以将日志功能单独分离出来,重点是如何将功能组合起来,这里就涉及AOP的底层技术啦,有三种方式:
- 编译时织入:在代码编译时,把切面代码融合进来,生成完整功能的Java字节码,这就需要特殊的Java编译器了,AspectJ属于这一类
- 类加载时织入:在Java字节码加载时,把切面的字节码融合进来,这就需要特殊的类加载器,AspectJ和AspectWerkz实现了类加载时织入
- 运行时织入:在运行时,通过动态代理的方式,调用切面代码增强业务功能,Spring采用的正是这种方式。动态代理会有性能上的开销,但是好处就是不需要特殊的编译器和类加载器啦,按照写普通Java程序的方式来就行了!
6、拦截器和过滤器
我们用Struts2的运行流程图来解释:
上图为Struts2的官方流程图,不同的颜色代表了不同的任务分工,下面从从一个请求的生命周期来完整的分析。
FilterDispacher
请求会首先经过Servlet Filters,一般最后一个FilterDispacher,它是必须存在的,负责将请求转发到Struts2。
ActionMapper
它会将请求转发给ActionMapper,ActionMapper会检查这个请求是否需要Struts2处理。
ActionProxy
当请求进入的时候会生成一个ActionProxy对象,它并不是某个具体的Action,它会通过ConfigurationManager去检查struts.xml文件,通过请求的url的匹配特定的Action
ActionInvocation
ActionInvocation会做真正的逻辑处理部分,它会首先执行Action之前所有的Interceptor,然后执行Action类中具体的方法,再根据返回结果调用具体的前端文件,然后反向执行之前的Interceptor,构造Response返回给浏览器。
一个很好的解释:https://www.jianshu.com/p/3e6433ead5c3
二、参考文献
这是两个很棒的Java基础和框架的入门教程: