前提

我自己的环境是jdk8,idea2023.3.6(比较基础请自行配置好环境)

  1. JAVA安装

  2. Maven安装

  3. MySQL

  4. Tomcat

  5. IDEA安装

  6. ChatGPT 的灵活使用

Java 基础语法一课通

一 基础知识

1 项目搭建

对于下面代码练习,我们需要先创建一个项目。

我个人习惯于直接基础的 SpringBoot 项目,在进行一些代码的调试。

创建方法很简单,在你的第一个 IDEA 项目中也讲到过了。

创建项目

点击下一步,选择依赖项页面,默认内容即可。

选择依赖

最后点击创建。

我们相关练习代码在src\main\java\com\example\javademo下右键选中该目录后,新建相关 Java Class 即可,如下图所示:

新建javaclass

2 Java 关键字

Java 关键字是对 Java编译器有特殊含义的字符串,是编译器和程序员的一个约定,程序员利用关键字来告诉编译器其声明的变量类型、类、方法特性等信息。

关键字 关键字 关键字 关键字
abstract assert boolean break
byte case catch char
class const continue default
do double else enum
extends final finally float
for goto if implements
import instanceof int interface
long native new package
private protected public return
short static strictfp super
switch synchronized this throw
throws transient try void
volatile while

3 修饰符

public(公开的):

  • 含义: 使用 public 修饰符的类是完全公开的,可以在任何地方访问。
  • 使用场景: 当你希望类的实例能够在任何地方都能被创建和访问时,或者当你编写的是一个库(library)的一部分,希望让其他开发者能够使用这个类。
1
2
3
public class MyClass {
// 类的定义
}

没有修饰符(default):

  • 含义: 如果没有使用任何修饰符,默认情况下,类的可见性为包级别,只能在同一个包中访问。
  • 使用场景: 当你希望类在同一个包中的其他类能够访问,但不希望被包外的类访问时使用。
1
2
3
class MyClass {
// 类的定义
}

protected(保护的):

  • 含义: 使用 protected 修饰符的类可以被同一包中的其他类访问,以及该类的子类(无论子类在哪个包中)。
  • 使用场景: 通常较少用于修饰类,更多地用于修饰成员变量和方法。当你希望子类能够访问类的成员,但其他类不能访问时使用。
1
2
3
protected class MyClass {
// 类的定义
}

private(私有的):

  • 含义: 使用 private 修饰符的类只能被同一类中的其他类访问,通常用于嵌套类或内部实现细节。
  • 使用场景: 当你希望将类的细节隐藏在同一类的内部,不让其他类直接访问时使用。
1
2
3
private class MyClass {
// 类的定义
}

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class OuterClass {
private int outerData;

// 构造函数
public OuterClass(int data) {
this.outerData = data;
}

// 内部类
private class InnerClass {
private int innerData;

// 构造函数
public InnerClass(int data) {
this.innerData = data;
}

public void display() {
System.out.println("Outer data: " + outerData);
System.out.println("Inner data: " + innerData);
}
}

// 外部类的方法
public void outerMethod() {
InnerClass innerObj = new InnerClass(20);
innerObj.display();
}

public static void main(String[] args) {
OuterClass Obj = new OuterClass(10);
outerObj.outerMethod();
}
}

4 数据类型

4.1 基础概念

在 Java 中,数据类型是用来定义变量的类型,以决定变量可以存储的数据种类。 Java 的数据类型主要分为两大类:基本数据类型和引用数据类型。基本数据类型包括整型(byte short int long) 浮点型(float double) 字符型(char)和布尔型(boolean)。

4.2 代码案例
1
2
3
4
5
6
7
8
9
10
11
// 整型示例
int integerVariable = 42;

// 浮点型示例
double doubleVariable = 3.14;

// 字符型示例
char charVariable = 'A';

// 布尔型示例
boolean booleanVariable = true;

5 变量

5.1 基础概念

变量是用于存储数据的内存空间的标识符。在 Java 中,变量必须先声明后使用,并指定其数据类型。变量的命名要符合 Java 的命名规范,遵循驼峰命名法。

5.2 代码案例
1
2
3
4
5
// 声明和初始化整型变量
int x = 10;

// 声明和初始化字符串变量
String name = "John";

6 基本运算符

6.1 基础概念

基本运算符包括算术运算符(+、-、*、/、%)、关系运算符(==、!=、<、>、<=、>=)、逻辑运算符(&&、||、!)等。它们用于执行常见的数学和逻辑运算。

6.2 代码案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 算术运算
int a = 5, b = 3;
int sum = a + b;
int difference = a - b;
int product = a * b;
double quotient = (double) a / b; // 注意类型转换
int remainder = a % b;

// 关系运算
boolean isEqual = (a == b);
boolean isGreaterThan = (a > b);

// 逻辑运算
boolean logicalAnd = (true && false);
boolean logicalOr = (true || false);
boolean logicalNot = !true;

7 顺序结构

7.1 基础概念

顺序结构是程序中最简单的结构,代码按照书写的顺序一行一行地执行。每一行代码都在前一行代码执行完毕后执行。

7.2 代码案例
1
2
3
4
5
// 顺序结构示例
int a = 5;
int b = 3;
int sum = a + b;
System.out.println("Sum: " + sum);

8 选择结构

8.1 基础概念

选择结构允许根据条件选择执行不同的代码块。在 Java 中,常见的选择结构有if语句、if-else语句和switch语句。

8.2 代码案例
1
2
3
4
5
6
7
// if语句示例
int number = 6;
if (number / 2 == 3) {
System.out.println("1");
} else {
System.out.println("2");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NestedIfExample {

public static void main(String[] args) {
int x = 5;
int y = 10;

if (x > 0) {
System.out.println("X 大于 0.");

if (y > 0) {
System.out.println("Y 也大于 0.");
} else {
System.out.println("y 小于 0.");
}
} else {
System.out.println("X 小于0.");
}
}
}

9 循环结构

9.1 基础概念

循环结构允许多次执行相同的代码块,直到满足退出条件。在 Java 中,常见的循环结构有 for 循环、while 循环和 do-while 循环。

9.2 代码案例
1
2
3
for (初始化; 循环条件; 循环迭代) {
// 循环体
}
1
2
3
4
// for循环示例
for (int i = 0; i < 5; i++) {
System.out.println("当前值为" + i);
}

10 数组

10.1 基础概念

数组是一种存储相同类型数据的集合。在 Java 中,数组是固定大小的,可以通过索引访问其中的元素。

10.2 代码案例
1
2
3
// 数组示例
int[] numbers = {1, 2, 3, 4, 5};
System.out.println("First element: " + numbers[0]);
1
2
3
4
int[] numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.length; i++) {
System.out.println("数组元素为:" + numbers[i]);
}

11 函数方法

在 Java 中,函数方法是一组执行特定任务的代码块。方法提供了程序的模块化和重用性。方法由方法名、参数列表、返回类型和方法体组成。 Java 中的方法可以分为普通方法和静态方法,普通方法需要实例化对象调用,而静态方法属于类,可以通过类名直接调用。

1
2
3
4
5
6
7
8
9
10
11
public class MyClass {
// 普通方法
public void printMessage(String message) {
System.out.println(message);
}

public static void main(String[] args) {
MyClass myObject = new MyClass();
myObject.printMessage("Hello, World!");
}
}
1
2
3
4
5
6
7
8
9
10
/** 返回两个整型变量数据的较大值 */
public static int max(int num1, int num2) {
int result;
if (num1 > num2)
result = num1;
else
result = num2;

return result;
}

如果我们想要使用,只需max(1,2)即可。

1
2
3
4
5
6
7
8
9
10
11
public class VoidMethodExample {
public static void main(String[] args) {
// 调用无返回值的方法
displayMessage();
}

// 一个无返回值的方法,用于显示一条简单的消息
public static void displayMessage() {
System.out.println("这是一个无返回值的方法代码案例。");
}
}
12 Java 异常处理

异常是指在程序运行时发生的意外情况,可能导致程序中断或产生不可预知的结果。Java 中的异常处理机制通过 try、catch、finally 块来捕获和处理异常。异常分为检查异常(Checked Exception)和非检查异常(Unchecked Exception)。

1
2
3
4
5
6
7
8
9
10
11
public class ExceptionHandling {
public static void main(String[] args) {
try {
int result = 10 / 0; // 会抛出ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero.");
} finally {
System.out.println("Finally block always executes.");
}
}
}

try块:try块内,包含可能引发异常的代码。在这里,int result = 10 / 0; 尝试进行除法运算,但由于除数是0,这会导致算术异常。

catch块: 如果在try块中发生了异常,控制流会跳转到对应的catch块。在这里,catch (ArithmeticException e) 捕获了ArithmeticException异常,表示发生了除以零的情况。catch块内的代码会被执行,输出提示信息:“Cannot divide by zero.”。

finally块: 无论是否发生异常,finally块中的代码总是会被执行。在这里,输出信息:“Finally block always executes.”。finally块通常用于确保资源的释放或清理工作,无论是否发生异常。

二 面向对象

面向对象(Object-Oriented,简称OOP)是一种编程思想和程序设计范式,它将程序中的数据和操作数据的方法组织成对象。在面向对象编程中,对象是程序的基本单元,每个对象可以包含数据(称为属性)和方法(称为行为)。

1 类

在编程中,类(Class)是一种抽象数据类型,用于描述具有相似属性和行为的对象集合。它是面向对象编程(Object-Oriented Programming,OOP)的核心概念之一,通过类可以创建对象,而对象则是类的实例。

我们初中生物讲过生物类,鸟类,鱼类等。这两个不同的类别,它们有着不同的特征和行为。同样,在编程中,类是一种将数据和方法组合在一起的结构,用于描述某种抽象概念或实体。

简单来说,类是一种用于描述对象的蓝图或模板。它定义了对象的属性(成员变量)和行为(方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 定义一个Bird类
public class Bird {
// 成员变量,描述鸟类的属性
private String feathers;
private String beakShape;

// 构造方法,用于初始化对象的属性
public Bird(String feathers, String beakShape) {
this.feathers = feathers;
this.beakShape = beakShape;
}

// 成员方法,描述鸟类的行为
public void fly() {
System.out.println("The bird is flying.");
}

// Getter方法,用于获取羽毛属性
public String getFeathers() {
return feathers;
}

// Setter方法,用于设置羽毛属性
public void setFeathers(String feathers) {
this.feathers = feathers;
}
}

// 在另一个类中使用Bird类
public class BirdExample {
public static void main(String[] args) {
// 创建一个鹰对象
Bird eagle = new Bird("brown", "hooked");

// 调用鹰对象的飞行方法
eagle.fly();

// 获取并输出鹰的羽毛属性
String feathers = eagle.getFeathers();
System.out.println("Feathers: " + feathers);

// 设置新的羽毛属性并输出
eagle.setFeathers("golden");
System.out.println("New Feathers: " + eagle.getFeathers());
}
}

2 对象

前面说到通过类可以创建对象,而对象则是类的实例。

实例化是为了创建对象,也就是我们使用类这个模板,以及可以进行自己所需的改动。

举个简单例子,我们从网上使用寻找 PPT 模板,最后下载使用,改成自己所需的内容。

这个过程和创建对象有些类似。

在 Java 中,实例化一个对象的过程通常包括使用 new 关键字来调用类的构造方法,并为对象分配内存空间。以下是一个简单的例子,演示如何在 Java 中实例化对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Dog {
// 类的属性
String name;
int age;

// 类的方法
public void bark() {
System.out.println("Woof! Woof!");
}
}

public class Main {
public static void main(String[] args) {
// 实例化一个 Dog 类的对象
Dog myDog = new Dog();

// 设置对象的属性值
myDog.name = "Buddy";
myDog.age = 3;

// 调用对象的方法
myDog.bark();

// 输出对象的属性值
System.out.println("Name: " + myDog.name);
System.out.println("Age: " + myDog.age);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 定义一个简单的类
class PPTTemplate {
// 成员变量
String templateName;

// 构造方法
public PPTTemplate(String name) {
this.templateName = name;
}

// 成员方法
public void customize(String content) {
System.out.println("Customizing PPT template '" + templateName + "' with content: " + content);
}
}

// 在另一个类中使用 PPTTemplate 类
public class PPTCreator {
public static void main(String[] args) {
// 实例化 PPTTemplate 类,创建一个对象
PPTTemplate myTemplate = new PPTTemplate("SimpleTemplate");

// 调用对象的成员方法
myTemplate.customize("This is my custom content.");

// 创建另一个对象
PPTTemplate anotherTemplate = new PPTTemplate("FancyTemplate");
anotherTemplate.customize("Adding some fancy graphics and animations.");

// ...
}
}

在这个例子中,我们定义了一个简单的 PPTTemplate 类,其中包含了一个构造方法用于初始化对象的成员变量 templateName,以及一个成员方法 customize 用于自定义模板内容。然后,在 PPTCreator 类中,我们实例化了两个不同的 PPTTemplate 对象,每个对象代表一个不同的PPT模板,并通过调用对象的方法来进行自定义。

关键步骤:

PPTTemplate 类的定义包括构造方法和成员方法。

② 在 PPTCreator 类的 main 方法中,通过 new PPTTemplate("SimpleTemplate") 实例化一个 PPTTemplate 对象,并指定模板名称为 “SimpleTemplate”。

③ 通过调用对象的成员方法 myTemplate.customize("This is my custom content."); 来自定义模板内容。

④ 同样地,创建另一个对象 anotherTemplate 并进行自定义。

实例化对象的过程涉及到为对象分配内存 调用构造方法进行初始化等步骤,这样就可以创建多个相互独立的对象,每个对象都有自己的状态(成员变量值)和行为(成员方法)。

3 继承

基础概念:

继承是面向对象编程中的概念,允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以继承父类的行为,并且可以通过添加新的属性和方法来扩展其功能。

解决的问题:

继承解决了代码重用和扩展的问题。通过继承,子类可以复用父类的代码,而不必重复实现相同的功能。同时,子类可以在保留父类功能的基础上,添加新的功能或修改部分功能,实现功能的扩展和定制。

代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父类
class Animal {
void eat() {
System.out.println("动物正在吃");
}
}

// 子类继承父类
class Dog extends Animal {
void bark() {
System.out.println("狗在叫");
}
}

public class Main {
public static void main(String[] args) {
// 创建子类对象
Dog myDog = new Dog();

// 调用继承自父类的方法
myDog.eat();

// 调用子类自己的方法
myDog.bark();
}
}

4 封装

基础概念:

封装是将对象的内部状态和实现细节隐藏起来,只对外提供访问接口。在 Java 中,通过访问修饰符(如private public)来实现封装。

解决的问题:

封装解决了对象的安全性和灵活性问题。通过将内部细节隐藏,防止外部直接访问对象的属性,从而保护对象的状态。同时,通过提供公共的方法,使得对象能够以受控制的方式被外部访问和修改。

代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Person {
// 私有属性
private String name;
private int age;

// Getter方法,用于获取name属性值
public String getName() {
return this.name;
}

// Setter方法,用于设置name属性值,并进行非空验证
public void setName(String newName) {
if (newName != null && !newName.isEmpty()) {
this.name = newName;
} else {
System.out.println("Name cannot be null or empty.");
}
}

// Getter方法,用于获取age属性值
public int getAge() {
return this.age;
}

// Setter方法,用于设置age属性值,并进行年龄范围验证
public void setAge(int newAge) {
if (newAge >= 0 && newAge <= 150) {
this.age = newAge;
} else {
System.out.println("Age must be between 0 and 150.");
}
}

public static void main(String[] args) {
// 创建一个Person对象
Person person = new Person();

// 使用setter设置属性值
person.setName("John");
person.setAge(25);

// 使用getter获取属性值并输出
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());

// 尝试设置无效值
person.setName(""); // 输出错误信息
person.setAge(200); // 输出错误信息
}
}

5 构造函数

基础概念:

构造函数是一个特殊的方法,与类同名,没有返回类型。它在对象被创建时调用,用于执行初始化操作。构造函数的主要目的是确保对象在被使用之前处于一个合理的状态。

构造函数可以显示的定义,也就是我们根据所需设置构造函数。

如果没有显示的提供构造函数,类仍然是可以被实例化的。

因为,如果你没有为类定义任何构造函数, Java 编译器会为你生成一个默认的无参数构造函数。这个构造函数会执行以下操作:

  • 将类的实例变量初始化为默认值(数值型为0,布尔型为false,对象型为null等)。
  • 如果类继承自其他类,会调用父类的无参数构造函数。

在构造函数中,又分为有参构造函数和无参构造函数。

都很好理解,就是是否需要传入参数。

如果你显式地提供了自定义的构造函数(无论是有参数的还是无参数的),并且没有提供任何无参数构造函数,那么默认的无参数构造函数就不再自动生成。

也就是说除非目标类显示的自定义了无参构造函数,否则如果目标类只定义了有参构造函数的话,那就不会默认生成无参构造函数了。

解决的问题:

构造函数解决了对象初始化的问题。通过构造函数,可以为对象的属性赋予初始值,执行必要的设置,使对象能够在创建时就具备正确的状态。

代码案例(无参构造函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Dog {
private String name;
private int age;

// 构造函数,初始值
public Dog() {
this.name = "juzi";
this.age = 3;
}

// 获取狗的名字
public String getName() {
return name;
}

// 获取狗的年龄
public int getAge() {
return age;
}

public static void main(String[] args) {
// 实例化一个Dog对象,使用初始值
Dog myDog = new Dog();

// 获取狗的信息并打印
System.out.println("Dog's name: " + myDog.getName() + ", Age: " + myDog.getAge());
}
}

代码案例(有参构造函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Dog {
private String name;
private int age;

// 无参构造函数,使用默认值
public Dog() {
this.name = "juzi";
this.age = 3;
}

// 有参构造函数,接受名字和年龄参数
public Dog(String name, int age) {
this.name = name;
this.age = age;
}

// 获取狗的名字
public String getName() {
return name;
}

// 获取狗的年龄
public int getAge() {
return age;
}

public static void main(String[] args) {
// 使用无参构造函数实例化一个Dog对象,使用默认值
Dog defaultDog = new Dog();
System.out.println("Default Dog's name: " + defaultDog.getName() + ", Age: " + defaultDog.getAge());

// 使用有参构造函数实例化一个Dog对象,提供自定义值
Dog customDog = new Dog("Buddy", 5);
System.out.println("Custom Dog's name: " + customDog.getName() + ", Age: " + customDog.getAge());
}
}
5.1 继承中的构造函数

默认情况:

  • 如果父类(基类)有一个无参数的构造函数,子类(派生类)会自动继承这个无参数构造函数。
  • 如果子类没有显式定义构造函数,编译器会默认生成一个无参数构造函数,并在其中调用父类的无参数构造函数。
1
2
3
4
5
6
7
class Animal {
// 父类有一个无参数构造函数
}

class Dog extends Animal {
// 子类没有显式定义构造函数,编译器默认生成一个无参数构造函数
}

父类有有参数构造函数:

  • 如果父类只提供了有参数的构造函数,子类需要显式定义构造函数,并通过super()调用适当的父类构造函数。此时 super()方法是必须的。
1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
public Animal(String name) {
// 父类有一个有参数构造函数
}
}

class Dog extends Animal {
public Dog(String name, String breed) {
super(name); // 调用父类的有参数构造函数
// 初始化子类特有的属性
}
}

子类提供无参数构造函数:

  • 如果父类没有提供无参数构造函数,但子类需要使用无参数构造函数,子类需要显式提供无参数构造函数,并通过super()调用适当的父类构造函数。此时 super()方法是必须的。
1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
public Animal(String name) {
// 父类有一个有参数构造函数
}
}

class Dog extends Animal {
public Dog() {
super("DefaultName"); // 调用父类的有参数构造函数
// 子类提供无参数构造函数,并在其中调用父类构造函数
}
}

总的来说,继承和构造函数的关系取决于父类的构造函数情况。在设计时,需要考虑如何在子类中正确地初始化父类的状态。

6 函数方法重载

函数方法的重载是指在同一个类中可以定义多个方法,它们具有相同的方法名但具有不同的参数列表。编译器根据方法的参数数量、类型或顺序来选择合适的方法。

下面计算器示例代码也是一个比较经典的案例了,可以使用重载来实现任意加减乘除的运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Calculator {
// 重载方法
public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}

public String add(String a, String b) {
return a + b;
}

public static void main(String[] args) {
Calculator myCalculator = new Calculator();
System.out.println(myCalculator.add(2, 3));
System.out.println(myCalculator.add(2.5, 3.5));
System.out.println(myCalculator.add("Hello", " World"));
}
}

7 构造函数中的重载

基础概念:

构造函数的重载是指在同一个类中定义多个构造函数,它们具有相同的名称但参数列表不同。通过构造函数的重载,可以提供多种初始化对象的方式。

解决的问题:

构造函数的重载解决了不同场景下对象初始化需求不同的问题。通过提供多个构造函数,使得用户能够选择适合自己需求的初始化方式。

代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person {
private String name;
private int age;

// 构造函数重载
public Person() {
this.name = "Unknown";
this.age = 0;
}

public Person(String name) {
this.name = name;
this.age = 0;
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 其他类成员和方法...
}

三 一些其他知识

1 HashMap

基础概念

HashMap 是 Java 中常用的集合类之一,用于存储Key-Value键值对的集合。它实现了 Map 接口,用于存储键值对。HashMap 的核心思想是通过散列算法将键映射到存储桶,提高查找效率。基本操作的时间复杂度为 O(1)。然而,需要注意 HashMap 不是线程安全的,如果在多线程环境中使用,可以考虑使用 ConcurrentHashMap

在 Java 中,键值对是一种常见的数据结构,通常用于表示关联关系。键值对包含两部分:键(key)和值(value)。键是唯一的,通过键可以访问对应的值。

在 HashMap 中,最常用的就是 get 和 put 了,通过名字也能知道,一个是获取键值,一个是存入键值。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.HashMap;
import java.util.Map;

public class HashMapExample {

public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();

// 添加键值对
hashMap.put("Java", 1);
hashMap.put("Python", 2);
hashMap.put("JavaScript", 3);

// 获取值
int javaValue = hashMap.get("Java");
System.out.println("Value for Java: " + javaValue);

// 遍历键值对
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}

2 StringBuilder

基础概念

StringBuilderjava.lang 包中的一个类,用于在单线程环境下对字符串进行可变操作,避免了使用 String 类时的不断创建新字符串的开销。它提供了一系列方法用于修改字符串内容,是一个可变的字符序列。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringBuilderExample {

public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("Hello");

// 追加字符串
stringBuilder.append(" World");

// 插入字符串
stringBuilder.insert(5, ", Java");

// 替换字符串
stringBuilder.replace(6, 11, "GPT");

// 删除字符串
stringBuilder.delete(12, 17);

// 输出结果
System.out.println(stringBuilder.toString());
}
}

3 StringBuffer

基础概念

StringBufferStringBuilder 类似,也是可变的字符序列。主要区别在于 StringBuffer 是线程安全的,因此在多线程环境中更适用。然而,由于同步的开销,StringBuilder 在单线程情况下可能更高效。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringBufferExample {

public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("Hello");

// 追加字符串
stringBuffer.append(" World");

// 插入字符串
stringBuffer.insert(5, ", Java");

// 替换字符串
stringBuffer.replace(6, 11, "GPT");

// 删除字符串
stringBuffer.delete(12, 17);

// 输出结果
System.out.println(stringBuffer.toString());
}
}

4 IO 流

基础概念

输入输出流(IO流)是 Java 中用于处理输入和输出的机制。它分为字节流和字符流,以及输入流和输出流。常见的 IO 类有 FileInputStreamFileOutputStreamBufferedReaderBufferedWriter 等。

可以进行文件读取,网络操作,缓冲操作读取字节流,对象序列化等操作,也是比较重要的。在第一阶段后面几个章节我们也会常接触的。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class IOExample {

public static void main(String[] args) {
try {
// 读取文件
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();

// 写入文件
FileWriter writer = new FileWriter("output.txt");
writer.write("Hello, IO!");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

5 Object

基础概念

Object 类是 Java 中所有类的根类,每个类都是 Object 类的子类。它定义了一些基本的方法,如 toStringequalshashCode。在 Java 中,所有对象都可以被赋值给 Object 类型的变量。

1
https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html

toString()方法:

  • 用于返回对象的字符串表示。默认情况下,toString()返回类名后跟对象的哈希码。

equals(Object obj)方法:

  • 用于比较两个对象是否相等。默认实现是比较对象的引用地址,但通常在子类中会被重写以根据业务逻辑判断对象是否相等。

hashCode()方法:

  • 返回对象的哈希码。哈希码用于在哈希表等数据结构中快速查找对象。

getClass()方法:

  • 返回对象的运行时类,即对象所属的类。

notify()notifyAll()wait()方法:

  • 用于线程间的协调和通信。这些方法通常与多线程编程有关,用于实现线程的等待和唤醒机制。

finalize()方法:

  • 在垃圾回收器清理对象之前调用。子类可以重写此方法以执行资源清理等操作。

clone()方法:

  • 创建并返回一个对象的副本。默认情况下,clone()方法执行的是浅拷贝,但可以在子类中重写以实现深拷贝。

getClass()方法:

  • 返回对象的运行时类,即对象所属的类。

wait(), notify(), notifyAll()

  • 用于线程间的协调和通信,通常与多线程编程相关。

Java Web,是用 Java 技术来解决相关 WEB 互联网领域的技术栈。

WEB 包括:WEB 服务端和 WEB 客户端两部分。Java 在客户端的应用有 Java Applet,不过使用得很少。

Java在服务器端的应用非常的丰富,比如 Servlet,JSP、SpringBoot 等等。

JavaWeb 架构演变过程大致分为以下几个阶段:

Java架构演变

下面,我们通过案例学习这些技术点。

Servlet 以及过滤器、监听器和拦截器

Servlet

1.1、什么是Servlet

Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

画个简易图,便于理解。

servlet流程简易图

上面是个简易图,用语言描述下流程。

比如,我们打开一个网站有个注册功能,在填写完信息后,点击提交,所填写的信息传输到后端,根据所指向的路径,来匹配对应的servlet,专门用于处理注册流程。

注册servlet

当然,我们还可以有登录Servlet,个人信息Servlet等等。

从代码上来说,Servlets 是 Java 类,服务于 HTTP 请求并实现了 javax.servlet.Servlet 接口。

1.2、Servlet 生命周期

Servlet 生命周期就是从创建到毁灭的过程。

大致是四个阶段。

  1. init():初始化阶段,只被调用一次,也就是在第一次创建 Servlet 时被调用。
  2. service():服务阶段,主要处理来自客户端的请求,并可以根据 HTTP 请求类型来调用对应的方法,比如doGet(),doPost(),doPut()等等。
  3. doGet(),doPost():处理阶段,将主要代码逻辑写在此处。根据不同 HTTP 请求对应不同方法。
  4. destroy():销毁阶段,该方法只会被调用一次,即在 Servlet 生命期结束时被调用。一般都是在关闭系统时执行。

画个简易图,大致流程如下图所示:

servlet生命周期简易图

1.3、你的第一个Servlet

光说不练假把式,我们动手写个 Servlet 案例,进一步理解它。

我们使用 Maven 创建一个 Servlet 案例,Maven在Java基础环境搭建中有讲到,可以回顾一下。

1.3.1、创建项目

①、双击启动 IDEA,点击Create New Project(如果默认进入某个项目,需要退出,点击左上角File -> Close Project ,左侧选择Maven项目,选中Create from archetype后选择maven-archetype-webapp,点击Next,如下图所示:

选择maven

选择archetype可以简单理解为选择模板,选择不同的模板会有各自的规范。

②、点击创建即可,稍等片刻,自行会自行加载相关依赖,如果有报错请尝试退出重进。

④、在pom.xml中引入 servlet 依赖,写在<dependencies>标签内,然后可以点击右上角加载文件改动的按钮(也就是重新加载安装依赖),如下图所示:

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>

填写servlet依赖

1.3.2、配置 Tomcat 服务器

①、访问Tomcat官网https://tomcat.apache.org/download-80.cgi,下载任意版本Tomcat,我选择的版本是8.5.81,如下图所示:

选择tomcat版本

选择Binary Distributions下的Core分类,这是 Tomcat 正式的二进制发布版本,一般学习和开发都用此版本。根据自己计算机系统选择下载项。

②、下载完成后,解压到一个固定常用的目录下。

③、进入 IDEA,配置 Tomcat。在右上侧选择Add Configuration...,在新的界面点击左上角的+按钮,滑到下面找到Tomcat Server点击选择Local,如下图所示:

配置添加tomcat

④、在 Server 标签栏下点击 Configure...,进入新的界面,在Tomcat Home处添加 Tomcat,最后点击 OK。如下图所示:

添加tomcat

⑤、配置部署方式,选择Deployment标签栏,在右侧点击+按钮,选择war exploded方式,如下图所示:

选择部署方式

war方式:是发布模式,先打包成 war 包,再发布。war exploded方式:常在开发的时候使用这种方式,它可以支持热部署,但需要设置。

跟本次案例调试无关,选择哪个都可以,对于我们练习影响不太大。

⑥、最后注意上图应用程序上下文出设置设置 URL 根路径后,一般就/ 即可,最后点击应用,再点击确定即可。如下图所示:

如果在配置发现没有工件,那就需要再配置下。

第一步,点击编辑工件,点左上角文件 - 项目结构 - 工件也可以,如下图所示:

点击这里编辑工件

第二步,点击加好,选择 WEB 应用程序,这两个看哪个有基于模块点击这个即可。

添加工件

选择模块

完成工件添加

然后点击应用确定即可,然后在转到部署工件处可成功部署。

最后,点击小三角运行,如果服务器没有报错,弹出浏览器输出了 hello world 及说明配成功,如下图所示:

成功运行

至此,IDEA 中配置 Tomcat 就完成了。

下面我们开始编写代码案例。

1.3.3、编写Servlet并运行

1.3.1、创建项目,在创建好后项目进行下面步骤。

①、鼠标右键点击main目录,选择New - Directory,创建名为java的主目录,后面我们的代码都写在此处。

②、鼠标右键点击java目录,选择New - Java Class,创建名为FirstServlet的class文件,在这里面编写我们的代码,最终如下图所示:

创建servlet的class

③、想要真正运行起来 Servlet 程序,我们需要继承HTTPServlet,重写部分方法,如下图所示:

继承HTTPServlet

④、编写代码,响应 Get 和 Post 请求内容,最终如下图所示:

servlet最终代码

⑤、在 servlet 中,需要根据 URL 路径匹配映射到对应的 servlet。即在web.xml中注册 servlet,如下图所示:

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>FirstServlet</servlet-name>
<servlet-class>FirstServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
</servlet-mapping>

设置映射路径

映射匹配流程:/FirstServlet路径绑定的Servlet-name为FirstServlet,而FirstServlet绑定的class是FirstServlet,最终访问/FirstServlet,调用的类也就是FirstServlet.class

多看两遍就明白了了。

servlet 注册有两种方式:一是通过上述web.xml进行注册,二是在 servlet3 以后可以通过@WebServlet()注解方式进行注册。

webservlet注解

⑥、启动项目,可以点击右上侧运行按钮,稍等片刻,看到控制台输出以下信息即表明启动成功,如下图所示:

启动成功

⑦、浏览器访问127.0.0.1:8080/FirstServlet,即可看到代码中输出的信息,如下图所示:

servlet成功输出

至此,你的第一个 Servlet 程序成功运行启动起来了。

现阶段,你只需要关注知道 Servlet 是什么就好,我相信这个流程走下来你能明白不少。

如果想进一步了解,我从网上找了几个 JSP + Servlet 的系统,可以进一步研究下。

1
2
https://github.com/Hui4401/StudentManager
https://github.com/czwbig/Tmall_JavaEE

二、过滤器、监听器和拦截器

**过滤器(Filter):**在 servlet 中,过滤器也就是Filter,它主要用于过滤字符编码,做一些统一的业务等等。是使用javax.servlet.Filter接口进行实现的。在代码安全中,他常被用于防止 XSS,防 SQL注入,防任意文件上传等。再配置了Filter之后,它可以统一过滤危险字符,省时省力。

监听器(Listener):在servlet中,监听器也就是Listener,它主要用于做一些初始化的内容。是使用javax.servlet.ServletContextListener接口进行实现的。如果同时有监听器和过滤器,监听器是在过滤器之前启动。

**拦截器(Interceptor):**依赖 WEB 框架,在 SrpingMvc 中就依赖 SpringMVC 框架。是属于面向切面变成的一种运用。

过滤器和拦截器的区别,分为以下五种:

  • 拦截器是基于 Java 的反射机制的,而过滤器是基于函数回调
  • 过滤器依赖与 servlet 容器,而拦截器不依赖与 servlet 容器
  • 拦截器只能对 action 请求起作用,而过滤器则可以对几乎所有的请求起作用
  • 拦截器可以访问 action 上下文、值栈里的对象,而过滤器不能
  • 在 action 的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次

我们现在主要关注一下过滤器。

2.1、过滤器代码

过滤器是使用javax.servlet.Filter接口进行实现的。需要使用doFilter()方法实现拦截。

1.3.3、编写Servlet并运行代码为基础,继续编写Filter代码。

①、在main/java右键点击后选择New - package,键入com.test.filter新建个filter,编写过滤层代码,如下图所示:

过滤器代码

我们重写了doFilter()方法,代码逻辑如下:

首先通过String requestURI = request.getRequestURI();获取URL路径。然后对路径进行判断,如果路径中包含/FirstServlet,则放行。否则就跳转到根路径下。

②、然后在 web.xml 中配置注册过滤器,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
<!--配置过滤器-->
<filter>
<filter-name>FilterTest</filter-name>
<filter-class>com.test.filter.FilterTest</filter-class>
</filter>
<!--映射过滤器-->
<filter-mapping>
<filter-name>FilterTest</filter-name>
<!--“/*”表示拦截所有的请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>

注册过滤器

过滤器标签需要要在servlet标签上面,程序会按照注册顺序进行执行。如果涉及多个过滤器,也是按照注册顺序进行执行的。

注册过滤器有两种方式:一是上面通过web.xml进行注册,另一种是通过@WebFilter()注解的方式进行注册。

③、运行项目,访问不通路径,观察不同效果。如下图所示:

servlet成功输出

过滤器拦截页面

JSP 基础

一、什么是 JSP

JSP 全称 Java Server Page,基于 Java 语言,是一种动态网页技术。

它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以<% code %>显示。

JSP 本质是简化版的 Servlet,JSP 在编译后就变成了 Servlet。JVM 只能识别 Java 的类,是无法识别 JSP 代码的。所以 WEB 服务器会将 JSP 编译成 JVM 能识别的 Java类。

JSP 跟 Servlet 区别在于,JSP 常用于动态页面显示,Servlet 常用于逻辑控制。在代码中常使用 JSP 做前端动态页面,在接收到用户输入后交给对应的 Servlet 进行处理。当然 JSP 也可以当做后端代码进行逻辑控制。

二、JSP 基础知识

以下相关概念了解即可。后续可再根据自己兴趣拓展学习。

1、JSP 文件后缀名为 *.jsp

2、JSP 代码需要写在指定的标签之中,比如:

1
2
3
4
常用:
<%
out.println("hellpo JSP!");
%>
1
2
3
<jsp:scriptlet>
代码片段
</jsp:scriptlet>

3、JSP 生命周期:编译阶段 -> 初始化阶段 -> 执行阶段 -> 销毁阶段,此处多了一个编译阶段,是将 JSP 编译成 Servlet 的阶段。

而这个阶段也是有三个步骤的:解析 JSP 文件 -> 将 JSP 文件转为 Servlet -> 编译 Servlet

4、JSP 指令:是用来设置 JSP 整个页面属性的。格式为:<%@ directive attribute="value" %>。JSP 中的三种指令标签:

指令 描述
<%@ page … %> 定义网页依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include … %> 包含其他文件
<%@ taglib … %> 引入标签库的定义

5、JSP的九大内置对象(隐式对象),这九个对象,可以不用声明直接使用。

名称 类型 描述
out javax.Servlet.jsp.JspWriter 页面输出
request javax.Servlet.http.HttpServletRequest 获得用户请求
response javax.Servlet.http.HttpServletResponse 服务器向客户端的回应信息
config javax.Servlet.ServletConfig 服务器配置,可以取得初始化参数
session javax.Servlet.http.HttpSession 保存用户的信息
application javax.Servlet.ServletContext 所有用户的共享信息
page java.lang.Object 指当前页面转换后的Servlet类的实例
pageContext javax.Servlet.jsp.PageContext JSP的页面容器
exception java.lang.Throwable 表示JSP页面所发生的异常,在错误页中才起作用

三、JSP程序运行

光说不练假把式。

通过创建 JSP 项目加上代码 demo 来进一步理解下吧。

2.1、创建项目

下面步骤讲解是基于 Maven 创建的 JSP 项目。

①、打开 IDEA 后,点击Create New Project,选择Mavan,勾选Create from archetype,选择的模板是maven-archetype-webapp,如下图所示:

创建项目选择maven

②、然后点击Next,给项目起个名字,其他默认即可。此处步骤和1.2.1 JavaWeb基础(一)Servlet以及过滤器、监听器和拦截器 中 1.3.1、创建项目相同,不再演示。

③、下面开始配置Tomcat。此处步骤和1.2.1 JavaWeb基础(一)Servlet以及过滤器、监听器和拦截器 中 1.3.2、环境配置Tomcat相同,不再演示。

④、配置完成后,点击运行,最终结果如下图所示:

成功运行jsp

2.2、代码 Demo

我们通过 JSP 模拟一下登录的过程。

首先引入一下依赖Jar包。点击左上角File - Project Structure - Libraries,点击+号,进入 Tomcat 的 lib 文件夹下,将依赖全部引入(对于我们这样最省事),最后点击 OK。如下图所示:

引入依赖

下面编写代码。

①、在webapp下面创建两个 JSP 文件,分别为login.jspdo_login.jsp,如下图所示:

创建两个JSP文件

②、在login.jsp键入以下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>User to Register Page!</title>
</head>
<body>
<hr><br>登录页面<br>
<form action="do_login.jsp" method="get">
<br>
<h1>Please input your message:</h1><br>
Name:<input type="text" name="username"><br>
Pswd:<input type="password" name="password"><br>
<br><br><br>
<input type="submit">&nbsp;&nbsp;&nbsp;&nbsp;<input type="reset"><br>
</body>
</html>

login的代码

③、在do_login.jsp键入以下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ page import="java.sql.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Server to do the register page!</title>
</head>
<body>
<%
String username=request.getParameter("username");
String password=request.getParameter("password");
%>

<%
//模拟登录成功与否
if(username.equals("admin") && password.equals("password")){
response.getWriter().write("Success!");
}else {
response.getWriter().write("Error!");
}
%>
</body>
</html>

dologin的代码

④、左上角运行代码,浏览器访问127.0.0.1:8080/login.jsp,分别输入正确和错误的账号密码,观察操作结果。

以上仅是 Demo 案例,用于演示理解 JSP 代码。

四、JSP 木马

前几年是 JSP 木马的鼎盛时期,由于技术的迭代。现在大型企业使用 SprinBoot 框架来开发的系统了,该框架默认是不引入 JSP 解析的,需要引入特定依赖才可以。

而且现在前端大多使用vue,thymeleaf,freemarker等等。因此JSP木马也算逐渐没落了。

但我们还是得学习了解,毕竟网站数量基数很大,难免会在授权的测试中遇见。

JSP木马也可以称作JSP Webshell,如果对方在上传文件或其他功能没有做防护的话,攻击者可以利用任意文件上传漏洞将恶意代码传到后端,继而攻击者可以达到操作目标网站的目的。

切勿对未授权的系统进行非法测试,这是违法行为。

推荐一些较为老派的 JSP 木马的 github 仓库:

1
https://github.com/theralfbrown/WebShell-2/tree/master/jsp

近两年主流 webshell 管理工具:冰蝎,哥斯拉,蚁剑…

1
2
3
冰蝎:https://github.com/rebeyond/Behinder
蚁剑:https://github.com/AntSwordProject
哥斯拉:https://github.com/BeichenDream/Godzilla

1、JSP 大马

JSP 大马,在代码中写入了更多功能,可以实现在网页中进行一些危险操作。

以下演示案例中的JSP大马用的是https://github.com/theralfbrown/WebShell-2/tree/master/jsp这个仓库中无密码的jsp大马.jsp

①、在webapp下新建个shell.jsp,在该文件中复制粘贴以上代码。如下图所示:

JSP大马

②、点击运行,;浏览器访问127.0.0.1:8080/shell.jsp,从中可以看到,我们可以进行的危险操作有上传文件、执行命令等等,如下图所示:

大马执行ping命令

建议各位动手操作吧,动手操作加深理解。

感兴趣的可以自己私下阅读学习源码。

2、Godzilla(哥斯拉)JSP木马操作

Godzilla(哥斯拉),主要用于管理Webshell的客户端工具。近两年比较主流的工具。哥斯拉内置了 3 种 Payload 以及 6 种加密器,6 种支持脚本后缀,20 个内置插件。

Github地址:

1
https://github.com/BeichenDream/Godzilla

下载地址:

1
https://github.com/BeichenDream/Godzilla/releases/tag/v4.0.1-godzilla

下面我们在代码文件中再新建一个名为shell1.jsp文件,配合哥斯拉的JSP木马进行操作。

①、下载完成后,启动哥斯拉,点击左上角管理,选择生成,此步骤是为了生成JSP木马代码,如下图所示:

哥斯拉生成木马

然后点击生成,suffix 选择为 JSP,点击确定后选择存放目录。

②、将已生成的代码复制粘贴到shell1.jsp中,然后启动项目。如下图所示:

复制粘贴到shell1jsp中

③、打开哥斯拉,点击左上角目标,选择添加,将shell1.jsp地址添加进去后点击添加,如下图所示:

添加目标

④、点击添加后,在管理页面即可看到该链接,右键选择进入,成功进入webshell管理页面,剩下的自己研究下吧。

shell执行whoami

JSP 章节到此结束了。关于以上代码建议大家动手操作,便于加深理解。

对于哥斯拉 JSP木马代码,在后续提升阶段可以进一步学习,目前理解即可。

感兴趣的可以私下进一步学习。

Spring 和 SpringMVC

一、什么是 Spring

Spring 是一个开源的框架,用于构建企业级 Java 应用程序。它提供了广泛的基础设施支持和一系列的解决方案,使开发者能够更轻松地构建和部署复杂的应用。

Spring 框架的核心特点包括:

轻量级: Spring 是一个轻量级框架,不需要大量的配置信息,使开发更加简单。

控制反转(IoC): Spring 实现了控制反转,即容器负责对象的创建、组装和管理。这样,开发者可以专注于业务逻辑而不是对象的创建和管理。

面向切面编程(AOP): Spring 支持面向切面编程,允许开发者定义横切关注点,并将其与主要业务逻辑分离。

容器: Spring 提供了一个容器,用于管理应用中的对象。这个容器负责对象的生命周期和配置。

数据访问: Spring 提供了对各种数据访问技术的支持,包括 JDBC、ORM(对象关系映射)等。

事务管理: Spring 支持声明式事务管理,简化了事务管理的配置。

模块化: Spring 框架被划分为多个模块,开发者可以选择使用特定的模块,以满足其特定需求。

Spring Framework官方介绍:

1
https://spring.io/projects/spring-framework

Spring 源码:

1
https://github.com/spring-projects/spring-framework

对于现阶段,大家理解 Spring 是一个 Java 开源框架接口,也是一个生态体系。其核心框架是 Spring Framework,在此基础上又衍生出来 Spring Boot、Spring Cloud、Spring Data、Spring Security 等一系列优秀项目。

spring生态项目

二、什么是 SpringMVC

SpringMVC 是 Spring 基础之上的一个 MVC 框架,基于模型-视图-控制器(Model-View-Controller,简称 MVC)设计模式进行 JavaWeb 系统的开发,它是 Spring 的一个模块,通过 Dispatcher Servlet, ModelAndView 和 View Resolver 等,让应用开发变得很容易。

SpringMVC 是基于 Spring 功能之上添加的 Web 框架,想用 SpringMVC 必须先依赖 Spring。

拓展学习:

1
2
Spring,Spring MVC及Spring Boot区别
https://www.jianshu.com/p/42620a0a2c33

三、什么是 MVC

MVC 模式是一种软件框架模式,被广泛应用在 JavaEE 项目的开发中。MVC 模式通过提供一种良好的组织结构和分工方式,帮助解决了软件开发中的耦合、可维护性、可测试性和可扩展性等方面的问题。这使得开发者能够更有效地构建和维护复杂的应用程序。

MVC 即模型(Model) 、视图(View)、控制器(Controller)。

  • 模型(Model)

模型是用于处理数据逻辑的部分。

所谓数据逻辑,也就是数据的映射以及对数据的增删改查,Bean、DAO(data access object,数据访问对象)等都属于模型部分。

  • 视图(View)

视图负责数据与其它信息的显示,也就是给用户看到的页面。

html、JSP 等页面都可以作为视图。

  • 控制器(controller)

控制器是模型与视图之间的桥梁,控制着数据与用户的交互。

控制器通常负责从视图读取数据,处理用户输入,并向模型发送数据,也可以从模型中读取数据,再发送给视图,由视图显示。

spring-springframework-mvc-4

3.1、核心架构流程

spring-springframework-mvc-5

在 Spring MVC 框架中,DispatcherServletHandlerMappingHandlerAdapter 是三个重要的组件,它们各自承担不同的角色,协同工作以处理客户端的请求并调度相应的处理程序。以下是它们的解释和作用:

DispatcherServlet(调度器Servlet):

  • 作用: DispatcherServlet 是 Spring MVC 中的前端控制器(Front Controller),负责接收客户端的所有请求并将其分派给适当的处理程序(Controller)。
  • 解释: 当客户端发送请求时,DispatcherServlet 接收请求,然后根据配置和规则找到合适的 HandlerMapping 来确定请求对应的处理程序。一旦找到处理程序,DispatcherServlet 将请求交给该处理程序进行处理。它还负责处理异常、视图解析和其他与请求生命周期相关的任务。
  • 源代码路径:
1
https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java

HandlerMapping(处理程序映射):

  • 作用: HandlerMapping 负责将请求映射到相应的处理程序(Controller)。它确定了客户端请求应该由哪个处理程序来处理。
  • 解释: 在 Spring MVC 中,可以有多个 HandlerMapping 实现,包括基于注解的映射、基于路径的映射等。HandlerMapping 将请求的 URL 映射到具体的控制器类和方法,以便 DispatcherServlet 可以将请求分发给正确的处理程序。
  • 源代码路径:
1
https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java

HandlerAdapter(处理程序适配器):

  • 作用: HandlerAdapter 负责调用实际的处理程序(Controller)来处理请求,并将处理程序的执行结果返回给 DispatcherServlet
  • 解释: 不同的处理程序可能有不同的接口,HandlerAdapter 的作用是适配各种不同类型的处理程序,使得它们能够被 DispatcherServlet 统一调用。它将请求传递给处理程序,处理程序执行后,HandlerAdapter 还负责处理返回的结果,如视图解析、数据绑定等。
  • 源代码路径:
1
https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerAdapter.java

这三个组件共同协作,实现了请求的分发和处理。DispatcherServlet 充当总管,HandlerMapping 负责找到处理程序,而 HandlerAdapter 则负责调用实际的处理程序执行业务逻辑。这种设计使得 Spring MVC 具有灵活性,允许通过配置来适应不同的业务需求和处理程序类型。

以上部分内容引用自下方链接,推荐大家拓展阅读

https://pdai.tech/md/spring/spring-x-framework-springmvc.html

https://pdai.tech/files/kaitao-springMVC.pdf

四、IDEA 创建 Spring 项目

1
2
https://juejin.cn/post/6996542590820548639
https://github.com/Laverrr/bookstore

①、打开IDEA,选择Create New Project

②、左侧选择Srping,并勾选相关选项,并且勾选Create empty spirng-config.xml,如下图所示:

新建项目spring

③、点击next,给项目起个名字,以及设置存放目录地址后点击Finish,稍等片刻,他会自动下载依赖的 JAR 包,如下图所示:

项目名称与加载

④、新建完成的项目结构如下图所示:

spring项目结构

⑤、右键选中src目录下新建两个Java Class分别为HelloSpringMain,如下图所示:

新建两个类

⑥、在HelloSpring文件中键入以下代码。

1
2
3
4
5
6
7
8
9
10
public class HelloSpring {

private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("Your Message : " + message);
}
}
hellospring代码

⑦、在Main文件中键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
HelloSpring obj = (HelloSpring) context.getBean("HelloSpring");
obj.getMessage();
}
}
MAIN代码

⑧、在spring-config.xml配置文件中增加以下内容。

1
2
3
<bean id="HelloSpring" class="HelloSpring">
<property name="message" value="i am groot!!"/>
</bean>

springconfigxml

⑨、最终点击运行,观察结果如下:

运行项目

是不是感觉不知所以然?在这基础上,给大家推荐一篇文章,在进一步理解一下,https://juejin.cn/post/6844903912034533383

扩展学习,推荐这本书:

1
2
Spring实战(第五版),这是网上翻译版,尽可能买原版书籍。
https://potoyang.gitbook.io/spring-in-action-v5/

是不是感觉配置繁琐,当然这还只是基础案例,在实际开发中配置会更加繁琐。

因此由于人类的【懒惰】,衍生出来了Spring Boot框架。

我们将在下一章节学习。

五、拓展 Spring + Spring MVC 的项目

对于学习代码审计,也许我们代码写的不是很溜,甚至不会写。

但拿到一个项目一定要会看。

推荐几个 Spring + Spring MVC 的项目进一步学习。

  • 某在线教育平台
1
https://github.com/impxiahuaxian/zaixianjiaoyu
  • 某网上书店
1
https://github.com/Laverrr/bookstore
  • 某在线课程学习网站
1
https://gitee.com/zhangjiazhao/online_course_learning_website
  • 某汽车租赁系统
1
https://gitee.com/huang_xiao_feng/carrentalsystem
  • 某人力资源管理系统
1
https://github.com/BradWenqiang/hr

SpingBoot 和 SpringCloud

一、什么是Srping Boot

SpringBoot 是一款基于 JAVA 的开源框架。目的是为了简化 Spring 应用搭建和开发流程。是目前比较流行,大中小型企业常用的框架。正因为极大简化了开发流程,才受到绝大开发人员的喜爱。

SpringBoot 核心原理是自动装配(自动配置)。

在这之前,开发一个JavaWeb,Spring 等项目要进行很多配置,使用了 SpringBoot 就不用在过多考虑这些方面。

并且在 SpringBoot 中还内置了 Tomcat。

官方介绍:

1
https://spring.io/projects/spring-boot

在 Spring Boot 中,自动装配是一项非常重要的功能,它帮助开发者简化了项目的配置和搭建过程。Spring Boot通过一系列的约定和默认配置,自动地将各种组件进行装配,使得开发者能够更专注于业务逻辑而不必过多关注框架的配置。

以下是 Spring Boot 自动装配体现的一些主要方面:

依赖管理: Spring Boot 通过 Maven 或 Gradle 等构建工具,引入了一系列默认的依赖项,以简化项目的搭建。例如,通过添加spring-boot-starter-web依赖,你就能够使用Spring MVC来构建Web应用。

类路径扫描: Spring Boot会自动扫描项目中的特定包及其子包,寻找标有特殊注解的类,比如@Controller@Service@Repository等,然后将它们注册为Spring的组件。

默认配置: Spring Boot 提供了大量的默认配置,当没有显式配置时,这些默认配置会自动应用。例如,如果项目中引入了数据库相关的依赖,Spring Boot 会根据类路径上的数据库驱动自动配置数据源。

条件化装配: Spring Boot 引入了条件化装配的概念,根据类路径上的特定条件来决定是否启用某些配置。这使得可以根据不同的环境或条件来自动装配不同的组件。

Spring Boot Starter: Spring Boot 提供了一系列的 Starter 依赖,这些 Starter 提供了一组常用的依赖项的整合,比如spring-boot-starter-data-jpaspring-boot-starter-security等,它们能够一次性地引入一组相关的依赖和配置,简化了开发者的工作。

自定义Starter: 除了 Spring Boot 提供的 Starter 之外,开发者也可以创建自己的 Starter,将一组相关的依赖和配置封装起来,使得它们能够在不同的项目中被重复使用。

自动化配置类: Spring Boot通过自动化配置类来实现自动装配。这些配置类使用@Configuration注解标记,并且通常包含有@ConditionalOn...注解,用于指定生效的条件。

总体来说,Spring Boot通过一系列约定、默认配置、条件化装配等方式,实现了自动装配,使得开发者能够更加方便地构建和配置项目。

二、Spring Boot 项目简介

通过经典 HelloWorld 程序,来看看 Springboot 项目搭建多么简便。

①、打开 IDEA,点击新建项目,选择Spring Initializer,配置信息如下图所示:

1

②、点击选择一些常用依赖,比如 Web 下的 Spring Web,如下图所示:

2

③、点击创建,稍等片刻,Maven 自动加载完所需依赖后,整体项目结构如下图所示:

helloworld项目结构

@SpringBootApplication注解表示这个类为 SpringBoot 的主配置类,SpringBoot 项目应运行这个类下面的 main 方法来启动 SpringBoot 应用。

也就是说这个 XXXApplication 是该项目的入口。

④、我们使用的是阿里云的脚手架创建的 SpringBoot 项目,可以看到在com\example\demo 目录下有个 demos\web 目录,里面是一些示例代码。如下图所示:

helloworld代码

⑦、点击右上方运行,或者进入 XXXApplication 中,点击左侧绿色小按钮,即刻运行项目,打开浏览器输入http://127.0.0.1:8080/hello,即可看到返回了一些内容,如下图所示:

返回helloworld

⑧、下面我们进入 demos.web 目录下,看看他的 Controller 代码,也就是返回上面内容的接口代码,是位于该目录下的 BasicController,拿上述接口距离,访问 hello 会返回你输入的名字,默认值是 unkonown user,如下图所示:

其他一些注解含义:

@Controller注解:标注该类为controller类,可以处理 http 请求。@Controller 一般要配合模版来使用。现在项目大多是前后端分离,后端处理请求,然后返回JSON格式数据即可,这样也就不需要模板了。

@ResponseBody注解:将该注解写在类的外面,表示这个类所有方法的返回的数据直接给浏览器。 @RestController 相当于 @ResponseBody 加上 @Controller

@RequestMapping注解:配置URL映射,可以作用于某个Controller类上,也可以作用于某Controller类下的具体方法中,说白了就是URL中请求路径会直接映射到具体方法中执行代码逻辑。

@PathVariable注解:接受请求URL路径中占位符的值,示例代码如下图所示:

1
2
3
4
5
6
7
8
9
@Controller
@ResponseBody
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/whoami/{name}/{sex}")
public String hello(@PathVariable("name") String name, @PathVariable("sex") String sex){
return "Hello" + name + sex;
}
}

@RequestParam注解:将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解),常用于POST请求处理表单。

综上演示,可以看到 Spring Boot 部署非常方便,并且在开发后端服务时也极大简化了各种配置,可以更专注于编写代码。这对于我们审计 Spring Boot 架构的系统也极为的便利。

三、什么是Spring Cloud

Spring Cloud 是一系列框架的有序集合。是一套基于 Spring Framework 的分布式系统开发工具,用于构建分布式应用程序中的各种模块化组件。

它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。

Spring Cloud 的诞生并不是为了解决微服务中的某一个问题,而是提供了一套解决微服务架构实施的综合性解决方案。

Spring Cloud 并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过 Spring Boot 风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

大家也听过微服务吧。那它和 Spring Cloud 什么关系呢?

首先,什么是微服务?

微服务(英语:Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关的API集相互通信。

简单来说,微服务就是将一个大型的应用拆分成很多个小的应用,这些应用之间一般通过基于HTTP的RESTful API进行通信协作,并且能够各自进行独立部署以及伸缩。由于微服务独立部署,可伸缩的特性,它能够迅速地大规模部署到云服务器上。

而使用 Spring Cloud 能够快速实现微服务架构。

7

官方介绍:

1
https://spring.io/projects/spring-cloud

Spring Cloud 提供了许多模块,以下是一些常用的模块及其主要作用:

Spring Cloud Config(配置中心):

  • 作用:用于集中管理配置信息,可以将配置信息存储在版本控制系统中,并在需要时动态刷新。

Spring Cloud Netflix(服务治理):

  • 作用:整合了Netflix开发的一些组件,包括Eureka(服务注册与发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Feign(声明式REST客户端)等,用于构建具有高可用性和弹性的微服务架构。

Spring Cloud Bus(消息总线):

  • 作用:通过消息总线,实现微服务架构中配置的动态刷新,使得配置的修改能够快速传播到各个微服务实例。

Spring Cloud Sleuth(分布式追踪):

  • 作用:用于跟踪分布式系统中的请求流程,生成跨服务的唯一标识,方便在分布式系统中进行日志跟踪和性能监控。

Spring Cloud Gateway(网关):

  • 作用:提供了一种简单而有效的方式来进行路由、过滤以及转发请求,用于构建微服务架构中的API网关。

Spring Cloud Stream(消息驱动):

  • 作用:简化了消息驱动的微服务开发,提供了一套统一的编程模型,支持多种消息中间件。

Spring Cloud Security(安全):

  • 作用:提供了一些安全工具和特性,用于保护分布式系统中的微服务。

Spring Cloud Data Flow(数据流):

  • 作用:用于构建和部署实时数据处理和分析的微服务,支持复杂的数据流操作。

Spring Cloud OpenFeign(声明式REST客户端):

  • 作用:简化了微服务之间的REST调用,通过声明式的方式定义和调用服务接口。

Spring Cloud Task(任务调度):

  • 作用:用于简化任务调度和执行,支持在分布式环境中进行批处理任务的调度和执行。

若依微服务版本:

1
https://gitee.com/y_project/RuoYi-Cloud

Java 分层思想与 MVC 模式

一、Java 分层思想

1、分层讲解

Java 分层思想是一种软件架构设计理念,旨在将一个复杂的系统划分为多个相对独立且互相关联的层次,每个层次负责不同的功能,以实现高内聚、低耦合的设计。这种思想有助于提高代码的可维护性、可扩展性,并使团队更容易协同工作。以下是Java分层思想的主要层次:

表现层(Presentation Layer): 主要负责与用户交互,处理用户界面和用户输入输出。在 Java 中,通常由 Servlet、JSP、或者更现代的框架如 Spring MVC 负责、或者 Springboot 下的 Controller 层。

业务层(Business Layer): 业务层包含应用程序的业务逻辑,处理业务规则和数据处理。这一层通常由 JavaBean、Service 等组成,负责执行具体的业务操作。

服务层(Service Layer): 服务层是业务层的一部分,提供业务逻辑的具体实现。在 Spring 框架中,使用 @Service 注解来表示服务层。

持久层(Persistence Layer): 持久层负责数据的持久化,通常与数据库交互。在 Java 中,常见的持久层技术包括 JDBC、Hibernate、MyBatis 等。

数据访问层(Data Access Layer): 这一层是持久层的一部分,负责封装数据访问细节,提供统一的接口给业务层。通常由 DAO(Data Access Object)组成。

Java分层

通过明确划分这些层次,开发人员可以更容易地理解、维护和扩展代码。这种分层思想还有助于实现模块化开发,每个层次都可以独立测试和替换,从而提高系统的可测试性和灵活性。在实际应用中,可以根据项目的规模和需求进行适度的调整和扩展。

1
https://www.cnblogs.com/java-123/p/9174547.html

2、代码案列

以后面实战阶段第一套系统举例说明,这是一个架构非常简单清晰的系统。

首先,src/main下面有两个目录,分别是javaresources

java目录中主要存放的是 Java 代码

resources目录中主要存放的是静态资源文件,比如:html、js、css等。

java目录下还有其他一些常见目录,具体含义整理如下:

2.1、java 目录

annotation:放置项目自定义注解

controller/: 存放控制器,接收从前端传来的参数,对访问控制进行转发、各类基本参数校验或者不复用的业务简单处理等。

dao/: 数据访问层,与数据库进行交互,负责数据库操作,在Mybaits框架中存放自定义的Mapper接口

entity/: 存放实体类

interceptor/: 拦截器

service/: 存放服务类,负责业务模块逻辑处理。Service层中有两种类,一是Service,用来声明接口;二是ServiceImpl,作为实现类实现接口中的方法。

utils/: 存放工具类

dto/: 存放数据传输对象(Data Transfer Object),如请求参数和返回结果

vo/: 视图对象(View Object)用于封装客户端请求的数据,防止部分数据泄漏,保证数据安全

constant/: 存放常量

filter/: 存放过滤器

2.2、resources 目录

mapper/: 存放Mybaits的mapper.xml文件

static/: 静态资源文件目录(Javascript、CSS、图片等),在这个目录中的所有文件可以被直接访问

templates/: 存放模版文件

application.propertiesapplication.yml: Spring Boot默认配置文件

二、Java MVC 模式

1、模式讲解

MVC 即模型(Model) 、视图(View)、控制器(Controller)。

MVC(Model-View-Controller)是一种软件架构模式,用于设计和组织代码。它将一个应用程序分为三个主要组件:模型(Model)、视图(View)和控制器(Controller)。每个组件有不同的责任,以实现代码的分离和模块化,以便更容易维护和扩展应用程序。

通俗来说,各司其职高效完成任务。

  • 模型(Model)

模型是用于处理数据逻辑的部分。

所谓数据逻辑,也就是数据的映射以及对数据的增删改查,Bean、DAO(data access object,数据访问对象)等都属于模型部分。

  • 视图(View)

视图负责数据与其它信息的显示,也就是给用户看到的页面。

html、JSP 等页面都可以作为视图。

  • 控制器(controller)

控制器是模型与视图之间的桥梁,控制着数据与用户的交互。

控制器通常负责从视图读取数据,处理用户输入,并向模型发送数据,也可以从模型中读取数据,再发送给视图,由视图显示。

2、代码案例

在前面我们简单学习了 Spring MVC,这就是基于 MVC 结合 Spring 形成的架构。

推荐几个 Spring + Spring MVC 的项目进一步学习。

  • 某在线教育平台
1
https://github.com/impxiahuaxian/zaixianjiaoyu
  • 某网上书店
1
https://github.com/Laverrr/bookstore
  • 某在线课程学习网站
1
https://gitee.com/zhangjiazhao/online_course_learning_website
  • 某汽车租赁系统
1
https://gitee.com/huang_xiao_feng/carrentalsystem
  • 某人力资源管理系统
1
https://github.com/BradWenqiang/hr

对于 Java 代码审计来说,分层思想也好,MVC 模式也罢,我们主要关注的是整个请求流程的走向,以便更好地正向追踪,或是逆向追踪整个代码请求流程,实现 Java 代码审计闭环。

三、其他

推荐了解《阿里巴巴Java开发手册》:

《阿里巴巴Java开发手册》以Java开发者为中心视角,划分为编程规约、异常日志、单元测试、安全规约、MySQL 数据库、工程结构、设计规约七个维度,再根据内容特征,细分成若干二级子目录。

一份倡导性的 Java 项目开发规范文档,对于我们来说,从开发者角度了解开发规范,可以让我们在代码审计各个项目时对于细节更加的了解。

1
https://00fly.online/upload/alibaba/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E6%B3%B0%E5%B1%B1%E7%89%88%EF%BC%89.pdf

Java 文件操作之文件上传

此章节我们关注学习两种文件上传方式:Multipartfile方式文件上传和ServletFileUpload方式文件上传。

两种方式给出代码案例,从不同侧来学习Java中文件上传方式。

以下给出的文件上传代码均为示例代码,无安全防护,目前仅是为了让大家学习理解这几种上传方式。

提前铺垫下一阶段任意文件上传漏洞前置知识。

当然还有其他方式和组件的文件上传,比如文件流方式,smartupload组件等等。

本次仅讲述常见的两种。不论用那种方式都换汤不换药,方式的不同对于我们后面进行任意文件上传代码审计影响不会太大。到时再具体问题具体分析。

一、Multipartfile 方式文件上传

1、简介

MultipartFile 是 Spring 框架中的一个接口,它提供了一种方便的方式来处理文件上传。 它可以处理从表单中上传的文件,并提供了访问文件名称、大小、类型以及内容等信息的方法。 MultipartFile 接口还提供了一些实用的方法,如将文件保存到本地磁盘、将文件转换为字节数组等。

2、环境搭建

①、创建 SpringBoot 项目,打开 IDEA,点击新建项目,选择 Spring Initializr,使用以下配置后点击Next,如下图所示:

创建项目

②、依赖选择添加 Web -> Spring Web,最后点击创建即可。如下图所示:

新建2

③、完善下项目结构。在 main 目录下新建webapp目录,然后在 webapp 目录下新建WEB-INF目录。如下图所示:

右键选择创建目录

创建webapp目录

备注:WEB-INF 目录为 JAVA WEB 中安全目录,该目录仅允许服务端访问,客户端无法访问。该目录下有 web.xml 文件。

④、在pom.xml文件中的<dependencies>...</dependencies>标签内添加 JSP 依赖,并重载 maven 为了下载所添加的依赖。如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- servlet 依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!-- tomcat 的支持.-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

添加jsp依赖

⑤、在application.properties添加一些配置信息,如下图所示:

1
2
3
4
5
server.port=7089
#视图前缀
spring.mvc.view.prefix=/jsp/
#视图后缀
spring.mvc.view.suffix=.jsp

修改配置信息

⑧、在 webapp 文件下创建 jsp 目录,并新建一个 index.jsp 文件,键入以下信息,如下图所示:

创建jsp目录和indexjsp

⑨、启动运行,如成功搭建,响应如下图所示:

成功启动index

3、代码示例

①、在 jsp 文件夹下新建一个名为multipartfileUpload.jsp的文件,键入以下代码。如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>文件上传页面</title>
</head>
<body>
<h1>文件上传页面</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
选择要上传的文件:<input type="file" name="file"><br>
<hr>
<input type="submit" value="提交">
</form>
</body>
</html>

multipartfileUpload-JSP

②、在com.example.demo目录下右键新建一个class名为multipartfileController,键入以下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

@Controller
public class multipartfileController {

@Value("${file.upload.path}")
private String path;

@GetMapping("/")
public String uploadPage() {
return "upload";
}

@PostMapping("/upload")
@ResponseBody
public String create(@RequestPart MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
String filePath = path + fileName;

File dest = new File(filePath);
Files.copy(file.getInputStream(), dest.toPath());
return "Upload file success : " + dest.getAbsolutePath();
}

}

multipartfileController代码

③、在application.properties配置文件中添加一条配置信息file.upload.path=C:/Users/powerful/Desktop/,是上传文件存放的地址,如下图所示:

存放地址

④、运行启动项目,访问127.0.0.1/jsp/multipartfileUpload.jsp,上传文件观察结果(是否将上传的图片存放到了桌面处)。

存放结果

此案例非常简单,使用了multipartfile方式进行文件上传,配合文件流保存上传内容。

二、ServletFileUpload 方式文件上传

1、简介

ServletFileUpload 方式文件上传依赖 commons-fileupload 组件。

对于 commons-fileupload 组件介绍:FileUpload依据规范RFC1867中”基于表单的 HTML 文件上载”对上传的文件数据进行解析,解析出来的每个项目对应一个 FileItem 对象。
每个 FileItem 都有我们可能所需的属性:获取contentType,获取原本的文件名,获取文件大小,获取FiledName(如果是表单域上传),判断是否在内存中,判断是否属于表单域等。
FileUpload使用FileItemFactory创建新的FileItem。该工厂可以控制每个项目的创建方式。目前提供的工厂实现可以将项目的数据存储临时存储在内存或磁盘上,具体取决于项目的大小(即数据字节,在指定的大小内时,存在内存中,超出范围,存在磁盘上)。

FileUpload 又依赖于 Commons IO。

官方网站:

1
https://commons.apache.org/proper/commons-fileupload/

常用的一些方法:

方法 描述
FileItemFactory 表单项工厂接口
ServletFileUpload 文件上传类,用于解析上传的数据
FileItem 表单项类,表示每一个表单项
public List parseRequest(HttpServletRequest request) 解析上传的数据,返回包含 表单项的 List 集合
String FileItem.getFieldName() 获取表单项的 name 属性值
String FileItem.getString() 获取当前表单项的值;
String FileItem.getName() 获取上传的文件名
void FileItem.write(file) 将上传的文件写到 参数 file 所指向存取的硬盘位置
1
2
3
4
5
6
7
8
9
10
11
FileItemFactory      表单项工厂接口
ServletFileUpload 文件上传类,用于解析上传的数据
FileItem 表单项类,表示每一个表单项
boolean ServletFileUpload.isMultipartContent(HttpServletRequest request) 判断当前上传的数据格式是否是多段的格式,只有是多段数据,才能使用该方式
public List<FileItem> parseRequest(HttpServletRequest request) 解析上传的数据,返回包含 表单项的 List 集合
boolean FileItem.isFormField()       判断当前这个表单项,是否是普通的表单项,还是上传的文件类型,true 表示普通类型的表单项;false 表示上传的文件类型
String FileItem.getFieldName()       获取表单项的 name 属性值
String FileItem.getString()     获取当前表单项的值;

String FileItem.getName()         获取上传的文件名
void FileItem.write( file )        将上传的文件写到 参数 file 所指向存取的硬盘位置

2、创建项目

①、启动 IDEA,新建个名为 servletDemo 的项目,选择 maven-webapp 模板,如下图所示:

servlet新建

②、在pom.xml中引入 servlet 的依赖,写在<dependencies>标签内,然后可以点击右上角加载文件改动的按钮(也就是重新加载安装依赖),如下图所示:

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>

填写servlet依赖

3、环境配置Tomcat

①、访问Tomcat官网https://tomcat.apache.org/download-80.cgi,下载任意版本Tomcat,我选择的版本是8.5.81,如下图所示:

选择tomcat版本

选择Binary Distributions下的Core分类,这是Tomcat正式的二进制发布版本,一般学习和开发都用此版本。根据自己计算机系统选择下载项。

②、下载完成后,先解压到桌面备用。

③、进入IDEA,配置Tomcat。在右上侧选择Add Configuration...,在新的界面点击左上角的+按钮,滑到下面找到Tomcat Server点击选择Local,如下图所示:

配置添加tomcat

④、在Server标签栏下点击Configure...,进入新的界面,在Tomcat Home处添加Tomcat,最后点击OK。如下图所示:

添加tomcat

⑤、配置部署方式,选择Deployment标签栏,在右侧点击+按钮,选择war exploded方式,如下图所示:

选择部署方式

war方式:是发布模式,先打包成war包,再发布。war exploded方式:常在开发的时候使用这种方式,它可以支持热部署,但需要设置。跟本次案例调试无关,仅是给大家简单讲讲。

⑥、设置URL根路径后,点击Apply,左后点击OK。如下图所示:

设置路径后apply

至此,完成了在IDEA中配置Tomcat。

4、代码示例

下文代码示例使用的https://www.cnblogs.com/liuyangv/p/8298997.html文章中的代码。作者给出了详细的注解,非常适合学习。

配合上面项目搭建,调试一下吧。

①、首先在 pom.xml 中添加所需依赖后重新加载 maven,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

commons-fileupload依赖

②、在index.jsp文件中键入以下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html" pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<form action="FileUploadServlet" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="name"><br>
文件1:<input type="file" name="f1"><br>
文件2:<input type="file" name="f2"><br>
<input type="submit" value="提交">
</form>
</body>
</html>

INDEXjsp代码

③、右键main目录新建一个名为java的目录,并在该目录下创建一个class,名为FileUploadServlet,并键入一下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;


/**
* @author powerful
*/
public class FileUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
//得到上传文件的保存目录。 将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
String realPath = this.getServletContext().getRealPath("/upload");// /WEB-INF/files
System.out.println("文件存放位置:"+realPath);
//设置临时目录。 上传文件大于缓冲区则先放于临时目录中
String tempPath = "C:\\Users\\powerful\\Desktop";
System.out.println("临时文件存放位置:"+tempPath);


//判断存放上传文件的目录是否存在(不存在则创建)
File f = new File(realPath);
if(!f.exists()&&!f.isDirectory()){
System.out.println("目录或文件不存在! 创建目标目录。");
f.mkdir();
}
//判断临时目录是否存在(不存在则创建)
File f1 = new File(tempPath);
if(!f1.isDirectory()){
System.out.println("临时文件目录不存在! 创建临时文件目录");
f1.mkdir();
}

/**
* 使用Apache文件上传组件处理文件上传步骤:
*
* */
//1、设置环境:创建一个DiskFileItemFactory工厂
DiskFileItemFactory factory = new DiskFileItemFactory();

//设置上传文件的临时目录
factory.setRepository(f1);

//2、核心操作类:创建一个文件上传解析器。
ServletFileUpload upload = new ServletFileUpload(factory);
//解决上传"文件名"的中文乱码
upload.setHeaderEncoding("UTF-8");

//3、判断enctype:判断提交上来的数据是否是上传表单的数据
if(!ServletFileUpload.isMultipartContent(req)){
System.out.println("不是上传文件,终止");
//按照传统方式获取数据
return;
}

//==获取输入项==
// //限制单个上传文件大小(5M)
// upload.setFileSizeMax(1024*1024*4);
// //限制总上传文件大小(10M)
// upload.setSizeMax(1024*1024*6);

//4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
List<FileItem> items =upload.parseRequest(req);
for(FileItem item:items){
//如果fileitem中封装的是普通输入项的数据(输出名、值)
if(item.isFormField()){
String filedName = item.getFieldName();//普通输入项数据的名
//解决普通输入项的数据的中文乱码问题
String filedValue = item.getString("UTF-8");//普通输入项的值
System.out.println("普通字段:"+filedName+"=="+filedValue);
}else{
//如果fileitem中封装的是上传文件,得到上传的文件名称,
String fileName = item.getName();//上传文件的名
//多个文件上传输入框有空 的 异常处理
if(fileName==null||"".equals(fileName.trim())){ //去空格是否为空
continue;// 为空,跳过当次循环, 第一个没输入则跳过可以继续输入第二个
}

//注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
//处理上传文件的文件名的路径,截取字符串只保留文件名部分。//截取留最后一个"\"之后,+1截取向右移一位("\a.txt"-->"a.txt")
fileName = fileName.substring(fileName.lastIndexOf("\\")+1);
//拼接上传路径。存放路径+上传的文件名
String filePath = realPath+"\\"+fileName;
//构建输入输出流
InputStream in = item.getInputStream(); //获取item中的上传文件的输入流
OutputStream out = new FileOutputStream(filePath); //创建一个文件输出流

//创建一个缓冲区
byte b[] = new byte[1024];
//判断输入流中的数据是否已经读完的标识
int len = -1;
//循环将输入流读入到缓冲区当中,(len=in.read(buffer))!=-1就表示in里面还有数据
while((len=in.read(b))!=-1){ //没数据了返回-1
//使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath+"\\"+filename)当中
out.write(b, 0, len);
}
//关闭流
out.close();
in.close();
//删除临时文件
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
item.delete();//删除处理文件上传时生成的临时文件
System.out.println("文件上传成功");
}
}
} catch (FileUploadException e) {
//e.printStackTrace();
throw new RuntimeException("服务器繁忙,文件上传失败");
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}

fileuploadservlet

④、运行项目,上传文件观察结果。

至此,两种上传方式均已演示完毕。大家调试下代码学习一下吧。

三、拓展文件上传项目学习

1、SpringBoot 集成上传文件,该项目多是使用的 Multipartfile 方式进行的文件上传。但其中有一块是使用了文件流方式,位于/src/main/java/com/example/springbootupload/controller/FileUploadController.java文件内,大家可以调试一下。

1
https://github.com/xiaonongOne/springboot-upload

2、MyUploader-Backend 项目实现了单文件上传,多文件上传,大文件上传,断点续传,文件秒传,图片上传。完整项目建议学习。

1
https://github.com/gaoyuyue/MyUploader-Backend

Java 文件操作之文件读取与下载

我先抛出一个问题,读取文件与下载文件在代码审计有何区别呢?

先简单说下答案,实际上在后端大多是通过读取文件方式获得目标文件内容,这个不难理解。最后将文件流内容传给浏览器,并在 header 头中添加浏览器解析方式和文件名,比如:文件下载到本地实现方法可以使用响应头Content-disposition来控制,也就是说下载这个动作是交给浏览器去操作的。

Content-Disposition 响应头:指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

但从漏洞挖掘角度来看,我们使用下载或读取功能的目的是获得目标敏感文件中的数据。

一、新建 Java 基础工程

使用IDEA创建一个基础项目工程,用于下面几种文件读取/下载实现方法的调试。

①、打开 IDEA 创建一个项目,注意选择 maven webapp 模板,如下图所示:

新建项目1

②、点击创建即可。

③、在桌面随便创建一个 txt 文本文件,里面键入任意内容,目的是用于下面读取文件的。

到此项目创建完成,下面根据每一个实现方法编写对应实现代码。

二、文件读取/下载实现方法

下面介绍几种我学到的 Java读取/下载文件的几种实现方法。

方法一:使用java.nio.file.Files读取文本

1、简述

使用Files类将文件的所有内容读入字节数组。Files类还有一个方法可以读取所有行到字符串列表。Files类是在Java 7中引入的,如果想加载所有文件内容,使用这个类是比较适合的。只有在处理小文件并且需要加载所有文件内容到内存中时才应使用此方法。

2、实现代码

①、在刚才创建的工程下的src/main/java目录下创建一个名为ReadFiles的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

/**
* 编号7089
*/
public class ReadFiles {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\power\\Desktop\\test.txt";
//使用Java 7中的Files类处理小文件,获取完整的文件数据
readUsingFiles(fileName);
}
private static void readUsingFiles(String fileName) throws IOException {
Path path = Paths.get(fileName);
System.out.println("使用File类字节数组读取文件.........");
//将文件读取到字节数组
byte[] bytes = Files.readAllBytes(path);
System.out.println(new String(bytes));
System.out.println("使用File类字读取文件字符串列表.........");
@SuppressWarnings("unused")
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println(new String(allLines.toString()));
}
}

②、启动运行项目,观察结果,如下图所示:

使用files类读取文件.jpg

方法二:使用java.io.FileReader类读取文本

1、简述

可以使用FileReader获取BufferedReader,然后逐行读取文件。FileReader不支持编码并使用系统默认编码,因此它不是一种java中读取文本文件的非常有效的方法。

2、实现代码

①、在刚才创建的工程下的src/main/java目录下创建一个名为ReadFileReader的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class ReadFileReader {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\powerful\\Desktop\\test.txt";
//使用FileReader读取,没有编码支持,效率不高
readUsingFileReader(fileName);
}
private static void readUsingFileReader(String fileName) throws IOException {
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
System.out.println("使用FileReader读取文本文件......");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
br.close();
fr.close();
}
}

②、启动运行项目,观察结果,如下图所示:

使用filereader读取文件

方法三:使用java.io.BufferedReader读取文本

1、简述

如果想逐行读取文件并对它们进行处理,那么BufferedReader是非常合适的。它适用于处理大文件,也支持编码。

BufferedReader是同步的,因此可以安全地从多个线程完成对BufferedReader的读取操作。BufferedReader的默认缓冲区大小为:8KB

2、实现代码

①、在刚才创建的工程下的src/main/java目录下创建一个名为ReadBufferedReader的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class ReadBufferedReader {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\powerful\\Desktop\\test.txt";
//使用BufferedReader读取,逐行读取,并设置编码为UTF_8
readUsingBufferedReader(fileName, StandardCharsets.UTF_8);
}
private static void readUsingBufferedReader(String fileName, Charset cs) throws IOException {
File file = new File(fileName);
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, cs);
BufferedReader br = new BufferedReader(isr);
String line;
System.out.println("使用BufferedReader读取文本文件......");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
br.close();
}

}

②、启动运行项目,观察结果,如下图所示:

使用bufferreader读取文件

方法四:使用 Scanner 读取文本

1、简述

如果要逐行读取文件或基于某些java正则表达式读取文件,则可使用Scanner类。

Scanner类使用分隔符模式将其输入分解为标记,分隔符模式默认匹配空格。然后可以使用各种下一种方法将得到的标记转换成不同类型的值。Scanner类不同步,因此不是线程安全的。

2、实现代码

①、在刚才创建的工程下的src/main/java目录下创建一个名为ReadScanner的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;

public class ReadScanner {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\powerful\\Desktop\\test.txt";
//使用Scanner类来处理大文件,逐行读取
readUsingScanner(fileName);
}
private static void readUsingScanner(String fileName) throws IOException {
Path path = Paths.get(fileName);
Scanner scanner = new Scanner(path);
System.out.println("使用Scanner读取文本文件.....");
//逐行读取
while(scanner.hasNextLine()){
//逐行处理
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
}
}

②、启动运行项目,观察结果,如下图所示:

使用scanner读取文件

方法五:使用RandomAccessFile断点续传读取文本

1、简述

随机流(RandomAccessFile)不属于IO流,支持对文件的读取和写入随机访问。

首先把随机访问的文件对象看作存储在文件系统中的一个大型 byte 数组,然后通过指向该 byte 数组的光标或索引(即:文件指针 FilePointer)在该数组任意位置读取或写入任意数据。

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。

断点续传实现原理:

  1. 下载断开的时候,记录文件断点的位置position;
  2. 继续下载的时候,通过RandomAccessFile找到之前的position位置开始下载
2、实现代码

①、在刚才创建的工程下的src/main/java目录下创建一个名为ReadRandomAccessFile的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.IOException;
import java.io.RandomAccessFile;

public class ReadRandomAccessFile {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\powerful\\Desktop\\test.txt";
//使用RandomAccessFile来实现断点续传读取/下载文件
readUsingRandomAccessFile(fileName);
}
private static void readUsingRandomAccessFile(String fileName) throws IOException {
RandomAccessFile file = new RandomAccessFile(fileName, "r");
String str;
while ((str = file.readLine()) != null) {
System.out.println("使用RandomAccessFile来实现断点续传读取/下载文件......");
System.out.println(str);
}
file.close();
}
}

②、启动运行项目,观察结果,如下图所示:

RandomAccessFile读取文件

方式六:使用 commons-io读取文本

1、简述

Commons IO 是 Apache 软件基金会的一个项目,它提供了一组用于处理 I/O 操作的实用工具类。这个库旨在简化 Java 编程中常见的文件和流操作,使开发者能够更轻松地处理文件读写、流操作、文件过滤和目录操作等。

一些 Commons IO 提供的主要功能包括:

文件操作: 提供了一系列用于文件复制、移动、删除和重命名的方法,还包括了获取文件大小、检查文件是否存在等常见操作。

流操作: 提供了用于处理输入和输出流的实用方法,如关闭流、复制流、转换流等。

文件过滤: 提供了一些实用的文件过滤器,可以根据文件名、文件大小、最后修改时间等条件对文件进行过滤。

目录操作: 提供了一些方便的方法来处理目录,如创建目录、列出目录内容等。

文件内容操作: 提供了一些用于读取、写入和比较文件内容的方法。

下面使用 Commons-io 库一行代码文件来实现读取文件。

2、实现代码

①、首先要在pom.xml中引入Commons-io依赖后重载maven。如下图所示:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

引入commonsio依赖

②、在刚才创建的工程下的src/main/java目录下创建一个名为ReadCommonsIo的Java Class。并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ReadCommonsIo {
public static void main(String[] args) throws IOException {
String fileName = "C:\\Users\\powerful\\Desktop\\test.txt";
readUsingCommonsIo(fileName);
}
private static void readUsingCommonsIo(String fileName) throws IOException {
File file = new File(fileName);
System.out.println("使用Commons-io读取文件......");
System.out.println(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
}
}

③、启动运行项目,观察结果,如下图所示:

使用Commonsio读取文件

方法七:使用 Files.readString 读取文本

①、简述

Java 11添加了readString()方法来读取小文件String。官方介绍:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/Files.html#readString(java.nio.file.Path,java.nio.charset.Charset)

②、实现代码

1
String content = Files.readString(path, StandardCharsets.US_ASCII);

三、文件读取/下载 JavaWeb 工程

我们创建一个基于SpringBoot的读取文件的JavaWeb工程,这回我们从WEB角度调试上面几种方法。

①、打开 IDEA 创建个名为 webreadfile 的 SpringBoot 项目工程,如下图所示:

创建工程1

②、下一步,选择 Spring web 依赖,如下图所示:

选择springweb依赖

③、在pom.xml中引入 Commons-Io 依赖后重载Maven,便于后面使用。如下图所示:

webreadfile引入commonsio依赖

④、在src/main/java/com/example/demo目录下创建一个名为ReadFilesController的Java Class,并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.example.demo;

import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Scanner;

/**
* 编号7089
*/
@Controller
@ResponseBody
public class ReadFilesController {


@RequestMapping("/readUsingFiles")
public String readUsingFiles(String fileName, HttpServletResponse response) throws IOException {
//使用Java 7中的Files类处理小文件,获取完整的文件数据
Path path = Paths.get(fileName);
//将文件读取到字节数组
byte[] bytes = Files.readAllBytes(path);
System.out.println("使用File类读取文件.........");
@SuppressWarnings("unused")
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
//response.reset();
//response.setContentType("application/octet-stream");
//response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));

System.out.println(new String(bytes));
return new String(bytes);

}

@RequestMapping("/readUsingFileReader")
public void readUsingFileReader(String fileName, HttpServletResponse response) throws IOException {
//使用FileReader读取,没有编码支持,效率不高
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
System.out.println("使用FileReader读取文本文件......");
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
//response.reset();
//response.setContentType("application/octet-stream");
//response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
PrintWriter out = response.getWriter();
while ((line = br.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
br.close();
fr.close();
}

@RequestMapping("/ReadBufferedReader")
public void readBufferedReader(String fileName, HttpServletResponse response) throws IOException{
File file = new File(fileName);
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
//response.reset();
//response.setContentType("application/octet-stream");
//response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
PrintWriter out = response.getWriter();
System.out.println("使用BufferedReader读取文本文件......");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
out.print(line);
}
br.close();
}

@RequestMapping("/readScanner")
public void readScanner(String fileName, HttpServletResponse response) throws IOException{
Path path = Paths.get(fileName);
Scanner scanner = new Scanner(path);
System.out.println("使用Scanner读取文本文件.....");
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
//response.reset();
//response.setContentType("application/octet-stream");
//response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
PrintWriter out = response.getWriter();
//逐行读取
while(scanner.hasNextLine()){
//逐行处理
String line = scanner.nextLine();
System.out.println(line);
out.print(line);
}
scanner.close();
}


@RequestMapping("/readUsingRandomAccessFile")
public void readUsingRandomAccessFile(String fileName, HttpServletResponse response) throws IOException{
RandomAccessFile file = new RandomAccessFile(fileName, "r");
String str;
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
//response.reset();
//response.setContentType("application/octet-stream");
//response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
PrintWriter out = response.getWriter();
while ((str = file.readLine()) != null) {
System.out.println("使用RandomAccessFile来实现断点续传读取/下载文件......");
System.out.println(str);
out.print(str);
}
file.close();
}

@RequestMapping("/readUsingCommonsIo")
public String readUsingCommonsIo(String fileName,HttpServletResponse response) throws IOException{
File file = new File(fileName);
//将注释去掉,重新运行启动项目,在浏览器键入要读取的文件地址,观察下效果有什么不一样。
response.reset();
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
System.out.println("使用Commons-io读取文件......");
System.out.println(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
return FileUtils.readFileToString(file, StandardCharsets.UTF_8);
}

}

⑤、启动运行项目,打开浏览器,访问其中一个读取/下载文件的接口,如下图所示:

1
http://127.0.0.1:8080/readUsingRandomAccessFile?fileName=C:/Users/powerful/Desktop/test.txt

选择任意接口读取文件

⑥、在代码文件中,每个接口我都留了相同的注释,我们选择一个接口将注释去除掉,如下图所示:

选择任意接口去掉注释

⑦、再次启动运行,访问去掉注释的这个接口,我们可以看到将文件下载到了本地,如下图所示:

1
http://127.0.0.1:8080/readUsingFiles?fileName=C:/Users/powerful/Desktop/test.txt

去除注释接口下载文件到本地

至此,文件读取/下载的几种方式讲完了。

一定要动手实践调试。

Java 命令执行

零、前言

在 Java 中可用于执行系统命令的方式有三种,分别是java.lang.Runtimejava.lang.ProcessBuilder以及java.lang.UNIXProcess/ProcessImpl

这三种方式都是 JDK 原生提供的,并不需要再额外引入。

下面我们通过代码示例来进一步理解这几种方式是如何执行系统命令的。

在我们针对 Java 系统进行代码审计以及渗透测试时,目标系统如果存在执行命令的功能,大概率是这三种方法之一。

一、创建Demo工程

使用 IDEA 创建一个基础项目工程,用于下面几种命令执行方法的调试。

①、双击 IDEA 启动,点击Create New Project

②、左侧选择 Maven,模板就不用选择了,点击 Next。如下图所示:

maven默认

③、随便起个名字,最后点击Finish即可。如下图所示:

项目名字

到此项目创建完成,下面根据每一个实现方式编写对应的示例代码。

二、java.lang.Runtime执行命令

1、使用说明

java.lang提供了一系列对 Java 编程至关重要的类。具体提供的类可看:https://docs.oracle.com/javase/7/docs/api/java/lang/package-summary.html

其中 Runtime 是 java.lang 中的一个类,主要是与操作系统交互执行操作命令。

而在java.lang.Runtime中我们主要关注exec()方法,使用该方法执行具体的命令,而执行exec()方法有以下六种重载形式,可以传入不同的数据类型的参数,如下图所示:

exec六种执行方式

方法 英文释义 中文释义(非标准)
exec(String[] cmdarray) Executes the specified command and arguments in a separate process. 在单独的进程中执行指定的命令和参数。
exec(String command) Executes the specified string command in a separate process. 在单独的进程中执行指定的字符串命令。
exec(String command, String[] envp, File dir) Executes the specified string command in a separate process with the specified environment and working directory. 在具有指定环境和工作目录的单独进程中执行指定的字符串命令。
exec(String command, String[] envp) Executes the specified string command in a separate process with the specified environment. 在具有指定环境的单独进程中执行指定的字符串命令。
exec(String[] cmdarray, String[] envp) Executes the specified command and arguments in a separate process with the specified environment. 在具有指定环境的单独进程中执行指定的命令和参数。
exec(String[] cmdarray, String[] envp, File dir) Executes the specified command and arguments in a separate process with the specified environment and working directory. 在具有指定环境和工作目录的单独进程中执行指定的命令和参数。

java.lang.Runtime 的 API 文档https://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html

目前我们只先关注exec(String command)exec(String[] cmdarray)这两种执行方式。

下面通过示例代码来理解。

1.1、exec(String command)

在单独的进程中执行指定的字符串命令。

简单来说就是直接执行字符串命令。

1
2
3
//简易示例代码
String command = "whoami";
Runtime.getRuntime().exec(command)
1.2、exec(String[] cmdarray)

在单独的进程中执行指定的命令和参数。

简单来说就是以数组的形式接收多个字符串然后执行。

1
2
3
//简易示例代码
String[] command = { "cmd", "/c", "whoami" };
Runtime.getRuntime().exec(command)

2、代码示例

①、在src/main/java目录下新建一个Java Class,名叫ExecRuntime,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.*;
import java.nio.charset.Charset;

public class ExecRuntime {
public static void main(String[] args) throws Exception {
String command1 = "cmd /c whoami";
//windows环境下执行
String[] command2 = {"cmd","/c","ping www.baidu.com"};
//String[] command2 = {"cmd","/c", "py","-3", "C:\\Users\\powerful\\Desktop\\dirsearch-0.4.2\\dirsearch.py", "-u", "https://www.baidu.com", "-e *"};
//Linux环境下执行,也可以是bash,
//String[] command3 = {"/bin/sh", "/root/xxx.sh", "xxx.sh所需的参数"};
//exec执行字符串命令
cmdstring(command1);
//exec以数组方式接受多个参数并执行
cmdarray(command2);
}
//exec执行字符串命令
private static void cmdstring(String command) throws IOException {
String line = null;
Process process = Runtime.getRuntime().exec(command);
//使用BufferedReader设置编码解决执行命令响应中文乱码问题
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
//exec以数组方式接受多个参数并执行
private static void cmdarray(String[] command) throws IOException {
String line = null;
Process process = Runtime.getRuntime().exec(command);
//使用BufferedReader设置编码解决执行命令响应中文乱码问题
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
}

execruntime执行结果

②、在代码注释中直接以字符串形式执行命令和以字符串数组形式执行命令给出了几个样例,大家可以关闭注释调试下。

③、为了便于理解字符串数组形式执行命令,我举了个执行python脚本的例子,命令如下:String[] command2 = {"cmd","/c", "py","-3", "C:\\Users\\powerful\\Desktop\\dirsearch-0.4.2\\dirsearch.py", "-u", "https://www.baidu.com", "-e *"};,大家可以关闭该注释可以启动调试一下,观察下结果。

执行dirsearch探测

④、我们的执行命令样例都是在Windows环境下执行的,等到练习命令执行注入漏洞时,会进一步讲解在Linux系统下遇见的各种问题。现在先熟悉下这些函数方法吧。

三、java.lang.ProcessBuilder执行命令

1、使用说明

在上面介绍了java.lang。java.lang.Runtime是其中的一个API。而java.lang.ProcessBuilder也是java.lang中的一个API。该类主要用于创建操作系统进程。具体介绍可查看:https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html。

java.lang.ProcessBuilder中我们要关注command()方法,可通过该方法设置要执行的命令参数。以及start()方法,简单来说使用该方法可以执行命令。以及

1.1、command()方法

官方原文介绍:https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html#command()

command()方法主要用于设置要执行的命令。

command()方法传参有两种方式,一种是可变的字符串(简单说就是可以传入普通字符串,或者字符串数组),另一种是字符串列表,如下图所示:

command两种方式

1
2
3
//简易示例代码
ProcessBuilder p = new ProcessBuilder();
p.command("calc");
1.2、start()方法

官方原文介绍:https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html#start()

使用start()方法可以创建一个新的具有命令,或环境,或工作目录,或输入来源,或标准输出和标准错误输出的目标,或redirectErrorStream属性的进程。
新进程中调用的命令和参数有command()方法设置,工作目录将由directory()方法设置,进程环境将由environment()设置。

在使用command()方法设置执行命令参数后,然后由start()方法创建一个新的进程进而在系统中执行了我们设置的命令。

这么看来java.lang.ProcessBuilder#start()Runtime.exec(String[] cmdarray, String[] envp, File dir)有些相似。

1
2
//简易示例代码
Process cmd = new ProcessBuilder(command).start();

2、代码示例

①、在src/main/java目录下新建一个Java Class,名叫ExecProcessBuilder,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;


public class ExecProcessBuilder {
public static void main(String[] args) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder();
// 在Windows上运行这个命令,cmd, /c 参数是在运行后终止
processBuilder.command("cmd.exe", "/c", "ping www.baidu.com");
// 在Linux上运行
//processBuilder.command("bash", "-c", "ping baidu.com");
Process process = processBuilder.start();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
}

processbuilder执行命令结果

②、建议大家私下多试一些命令进行调试,天马行空的想法都可以尝试一下,另外在代码中还给出了linux系统下运行的命令方法。

四、java.lang.UNIXProcess/ProcessImpl执行命令

1、使用说明

首先对于UNIXProcessProcessImpl可以理解本就是一个东西,因为在 JDK9 的时候把UNIXProcess合并到了ProcessImpl当中了。具体可查看:https://hg.openjdk.java.net/jdk-updates/jdk9u/jdk/rev/98eb910c9a97

UNIXProcessProcessImpl最终都是调用native执行系统命令的类,这个类提供了一个叫forkAndExec的native方法,如方法名所述主要是通过fork&exec来执行本地系统命令。

UNIXProcess类是*nix系统在 java 程序中的体现,可以使用该类创建新进程,实现与”fork”类似的功能(对于Windows系统,使用的是java.lang.ProcessImpl类)

ProcessImpl 是更为底层的实现,Runtime和ProcessBuilder执行命令实际上也是调用了ProcessImpl这个类。

对于 ProcessImpl 类,我们不能直接调用需要配合使用反射。因为 java.lang.ProcessImpl 代码都被 private 封装起来了。并没有设置公共的 API 接口。

2、代码示例

①、在src/main/java目录下新建一个Java Class,名叫ExecProcessImpl,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.Map;

public class ExecProcessImpl {
public static void main(String[] args) throws Exception {
String[] cmds = new String[]{"whoami"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);
String line = null;
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
}

processimpl反射执行命令

②、由于ProcessImpl类不能直接调用,需要配合反射来进行命令执行,因此此部分中的反射知识点可先自行学习。到后面Java反射章节会在进一步讲解。

五、执行命令Java Web项目

1、创建javawebexec工程

我们创建一个基于SpringBoot的执行系统命令的JavaWeb工程,这回我们从WEB视角调试上面几种方法。

①、双击IDEA启动,点击Create New Project

②、左侧选择Spring Initializr,内容默认即可,点击Next,如下图所示:

选择spring初始化

③、稍等片刻, Srping Initializr Project Settings页面配置内容将Java Version选择为8,,其他默认即可。如下图所示:

spring配置

④、点击Next,进入依赖项选择页面,我们选择Web -> Spring Web这一个即可,如下图所示:

web-springweb

⑤、起个项目名称为javawebexec,最后点击Finish,稍等片刻进入项目工程。

⑥、在pom.xml中引入servlet依赖,并重载maven,如下图所示:

引入依赖并重载

2、示例代码

①、在src/main/java/com/example/demo目录下创建一个名为ExecController的Java Class,并键入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.Charset;

@Controller
@ResponseBody
public class ExecController {

@RequestMapping("/execRuntimeString")
public void execRuntimeString(String command, HttpServletResponse response) throws IOException {
String line = null;
Process process = Runtime.getRuntime().exec(command);
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}
@RequestMapping("/execRuntimeArray")
public void execRuntimeArray(String command, HttpServletResponse response) throws IOException {
String line = null;
String[] commandarray ={"cmd","/c",command};
Process process = Runtime.getRuntime().exec(commandarray);
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("GBK")));
PrintWriter out = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
//逐行读取
System.out.println(line);
out.print(line);
}
bufferedReader.close();
}

}

execcontroller执行结果

②、在这个Controller里面我只写了java.lang.Runtime下执行命令的两种方式。在此留作业,请各位将java.lang.ProcessBuilderjava.lang.ProcessImpl根据上面形式写出对应的Controller,并成功运行。

本节讲述两种 Java 数据库操作的方式,一是比较原生繁琐的 JDBC 方式,二是现在比较简便主流的 Mybatis 方式。

由于 Mybatis 在做数据库操作时更加简便,成为了比较主流之一的方式,值得我们关注学习。

但也需要了解原生的 JDBC 方式,毕竟这是基础,主流的框架也大多由此衍生。

Java 数据库操作

一、Java数据库操作之 JDBC

1、简介

Java 数据库连接,(Java Database Connectivity,简称 JDBC)是Java 语言中用来规范客户端程序如何来访问数据库的应用程序接口(位于 jdk 的 java.sql 中)。我们通常说的 JDBC 是面向关系型数据库的,提供了诸如查询、更新、删除、增加数据库中数据的方法。在使用时候需要导入具体的jar包,不同数据库需要导入的jar包不同。

JDBC与MySQL进行连接交互,通常为以下6个流程:

  • 1:注册驱动 (仅仅做一次)
  • 2:建立连接(Connection)
  • 3:构建运行 SQL 的语句(Statement)
  • 4:运行语句
  • 5:处理运行结果(ResultSet)
  • 6:释放资源

JDBC中常用的API,推荐看这里:https://book.itheima.net/course/1265899443273850881/1272721284588904449/1272772917125455877

2、代码示例

下面通过一个简单的代码示例来理解JDBC链接Mysql,并查询数据的过程。

JDBC 与 Mysql 交互需要在本机安装 Mysql,我选择的是 PHPStudy自带的 Mysql,版本为MySQL 5.7.26,点击启动,即可使用,如下图所示:

phpstudy启动mysql

①、首先,需要创建练习所需的数据库,同时创建数据表,以及添加对应的数据,分别执行以下三个部分的代码,最终执行结果如下图所示:

1
CREATE DATABASE jdbcdemo;
1
2
3
4
5
6
7
USE jdbcdemo;
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
`username` varchar(25) NOT NULL COMMENT '用户名',
`password` varchar(25) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
1
2
3
INSERT INTO user(id,username,password) VALUES (1, "power7089", "power7089");
INSERT INTO user(id,username,password) VALUES (2, "root", "root");
INSERT INTO user(id,username,password) VALUES (3, "admin", "admin@123");

jdbc数据库

②、下面创建一个项目工程,名为jdbcdemo,打开IDEA,选择Create New Porject

③、在Spring Initializr Project Settings页面,将Java Version设置为8,其他配置项默认即可,点击Next。

④、在依赖选项界面,我们选择SQL -> JDBC APISQL -> Mysql Driver ,注意,一共需要勾选两个依赖。如下图所示:

jdbc选择依赖

⑤、点击Next,起个项目名称为jdbcdemo,最后点击Finish。

⑥、在src.main.java.com.example.demo的文件下新建一个名为JdbcDemo的Java Class,并键入以下代码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class JdbcDemo {
public static void main(String[] args) throws Exception {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
String url = "jdbc:mysql://localhost:3306/jdbcdemo?useUnicode=true&characterEncoding=utf8";
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet rs = stmt.executeQuery(sql);
while(rs.next()) {
System.out.println("==================");
System.out.println(rs.getInt("id"));
System.out.println(rs.getString("username"));
System.out.println(rs.getString("password"));
}
// 回收资源
rs.close();
stmt.close();
conn.close();
}
}

jdbc示例代码

⑦、点击运行JdbcDemo类,观察运行响应结果,可以看到正确连接到数据库,并查询回来了所需数据,如下图所示:

运行jdbc代码

一个简单的JDBC链接Mysql并查询数据的示例代码。

二、Java数据库操作之Mybatis

1、简述

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

Mybatis中文文档:https://mybatis.org/mybatis-3/zh/index.html

2、代码示例

下面,我们通过SpringBoot整合Mybatis,来模拟一个增删改查的场景。

务必按顺序跟着下面每一个步骤进行练习。

2.1、创建项目工程

先创建一个名为mybatis-springboot的工程项目,用于后面编写示例代码。

①、打开IDEA,选择Create New Porject

②、左侧选择Spring Initializr,配置默认即可,点击Next。

③、在Spring Initializr Project Settings页面,将Java Version设置为8,其他配置项默认即可,点击Next。

④、在依赖选项界面,我们选择web -> Spring WebSQL -> JDBC APISQL -> Mybatis Framework SQL -> Mysql Driver ,注意,一共需要勾选四个依赖。如下图所示:

配置页面选择依赖

⑤、点击Next,这一步给项目起个名字,就叫mybatis-springboot吧。其他默认即可。最后点击Finish。

2.2、创建数据库

在编写代码之前,我们需要对项目做一些前置工作。

首先需要创建一个数据库并添加一些数据。我使用的是PHPStudy下自带的Mysql,版本为Mysql 5.7.26

①、启动Mysql后,我使用的是命令行方式进入Mysql。创建一个名为mybatisdemo的数据库,如下图所示:

1
CREATE DATABASE mybatisdemo;

创建数据库

②、先切换使用mybatisdemo数据库。然后创建user数据表,如下图所示:

1
2
3
4
5
6
7
USE mybatisdemo;
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
`username` varchar(25) NOT NULL COMMENT '用户名',
`password` varchar(25) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

添加数据表

③、向info数据表中添加具体数据,如下图所示:

1
2
3
INSERT INTO user(id,username,password) VALUES (1, "power7089", "power7089");
INSERT INTO user(id,username,password) VALUES (2, "root", "root");
INSERT INTO user(id,username,password) VALUES (3, "admin", "admin@123");

info表中添加数据

2.3、编写代码

下面我们编写 SpringBoot 整合 Mybatis 实现增删改查的场景。

这个场景案例非常有意义,比较提盒真实的SpringBoot项目。

并且通过示例代码,可以直观的了解到JavaWeb从接口到数据库的请求过程。

①、首先我们针对目录结构进行改动,根据不同作用类创建对应的代码包,在src.main.java.com.example.demo下分别创建controllerserviceentitymapperservice->impl,以及src.main.resources下创建mapper,最终目录结构如下图所示:

1
2
3
4
5
controller:Controller层
service:业务逻辑层
service/impl:service的实现
mapper:数据操作层 DAO
entity:实体类,作用一般是和数据表做映射。

最终项目目录结构

②、在src.main.java.com.example.demo.entiy包下新建一个Java Class,名为User,这是一个实体类,和数据表做下映射,键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class User {

private int id;
private String username;
private String password;

public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String name) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", password=" + password + "]";
}
}

user实体类

③、在src.main.java.com.example.demo.mapper文件下新建一个名为UserMapper的Java Interface,键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;

@Mapper
public interface UserMapper {

/**
* 新增用户
* @param user
* @return
*/
public boolean addUser(User user);

/**
* 删除用户
* @param userName
* @return
*/
public boolean delUser(String userName);

/**
* 修改用户
*@param user
*@return
* */
public boolean updateUser(User user);

/**
* 单个查询
* @param id
* @return
*/
public User getUser(int id);

/**
* 查询全部
* @return
*/
public List<User> getUsers();
}

usermapper代码

④、在src.main.resources.mapper文件下新建一个名为UserMapper.xml文件,与dao层的UserMapper做好映射绑定,键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<!-- 添加用户 -->
<insert id="addUser" parameterType="com.example.demo.entity.User">
insert into user(id, username, password) values(#{id}, #{username}, #{password})
</insert>
<!-- 删除用户 -->
<delete id="delUser" parameterType="String">
delete from user WHERE username = #{username}
</delete>
<!-- 修改用户 -->
<update id="updateUser" parameterType="com.example.demo.entity.User">
update user set username = #{username}, password = #{password},
where id = #{id}
</update>
<!-- 根据主键查询用户 -->
<select id="getUser" resultType="com.example.demo.entity.User" parameterType="int">
select * from user where id=#{id}
</select>
<!-- 查询全部数据 -->
<select id="getUsers" resultType="com.example.demo.entity.User">
select * from user
</select>
</mapper>

usermapperxml文件

⑤、在src.main.java.com.example.demo.service文件下新建一个名为UserService的Java interface,键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo.service;

import com.example.demo.entity.User;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public interface UserService {
// 添加数据
public boolean addUser(User user);
// 删除数据
public boolean delUserByName(String userName);
// 修改数据
public boolean updateUserByUserId(User user);
// 根据id查询数据
public User getUser(int id);
// 查询全部数据
public List<User> getUsers();
}

service代码

⑤、在src.main.java.com.example.demo.service.impl文件下新建一个名为UserServiceImpl的Java class,这是UserService实现方法,继承UserService并重写方法。键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.demo.service.impl;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

/**
* 添加用户
* */
@Override
public boolean addUser(User user) {
boolean flag;// 默认值是false
flag = userMapper.addUser(user);
return flag;
}

/**
* 根据id删除用户
* */
@Override
public boolean delUserByName(String userName) {
boolean flag;// 默认值是false
flag = userMapper.delUser(userName);
return flag;
}

/**
* 修改用戶
* */
@Override
public boolean updateUserByUserId(User user) {
boolean flag;// 默认值是fals
flag = userMapper.updateUser(user);
return flag;
}

/**
* 根据id获取用户
* */
@Override
public User getUser(int id) {
User user = userMapper.getUser(id);
return user;
}

/**
* 查询全部数据
* */
@Override
public List<User> getUsers() {
List<User> users = userMapper.getUsers();
return users;
}
}

userserviceimpl代码1

userserviceimpl代码2

⑥、在src.main.java.com.example.demo.controller文件下新建一个名为UserController的Java class,键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@Controller
@RestController
public class UserController {

@Autowired
private UserService service;

/**
* 新增用户
* @param user
* @return
*/
@RequestMapping(value="/add", method=RequestMethod.POST)
@ResponseBody
public String addUser(User user){
boolean flag = service.addUser(user);
if (flag) {
return "success";
} else {
return "faile";
}
}

/**
* 删除用户
*
* @param name
* @return
* */
@RequestMapping(value="/del", method=RequestMethod.POST)
@ResponseBody
public String delUserByName(@RequestParam("name") String userName) {

boolean flag = service.delUserByName(userName);
if (flag) {
return "success";
} else {
return "faile";
}
}

/**
* 修改用户
*
* @param User
* @return
* */
@RequestMapping(value="/updata", method=RequestMethod.POST)
@ResponseBody
public String updateUserByName(User user) {
boolean flag = service.updateUserByUserId(user);
if (flag) {
return "success";
} else {
return "faile";
}
}

/**
* 单个查询
* @param id
* @return
*/
@RequestMapping(value="/get/{id}", method= RequestMethod.GET)
public User getUser(@PathVariable("id") int id){
User user = service.getUser(id);
return user;
}

/**
* 查询全部
* @return
*/
@RequestMapping(value="/getUser/list", method=RequestMethod.GET)
@ResponseBody // @ResponseBody - 返回json字符串
public List<User> getUsers(){
List<User> users = service.getUsers();
return users;
}
}

usercontroller代码

⑦、在DemoApplication启动类中添加注解@MapperScan(),如下图所示:

@MapperScan 注解:扫描 com.example.demo.dao 下的所有的类作为 Mapper 映射文件

添加mapperscan注解

⑧、最后在src.main.resources -> application.properties文件中添加以下配置,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
server.port=7089
## 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/mybatisdemo?useUnicode=true&characterEncoding=utf8
## 下面是数据库连接账户密码,设置成自己的
spring.datasource.username=root
spring.datasource.password=rootroot
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

## Mybatis 配置
## mybatis.typeAliasesPackage 配置为com.example.demo.dao,指向实体类包路径。
mybatis.typeAliasesPackage=com.example.demo.dao
## mybatis.mapperLocations 配置为 classpath 路径下 mapper 包下,* 代表会扫描所有 xml 文件。
mybatis.mapperLocations=classpath:mapper/*.xml

application配置文件

⑨、启动项目,访问我们在UserController中定义的接口,比如:http://127.0.0.1:7089/get/1http://127.0.0.1:7089/getUser/list,最终响应如下图所示:

访问get接口

getuser接口

还剩几个接口,大家自行调试下吧。有问题不要怕,迎难而上解决问题。

注意:

在实际项目开发中,每个人的风格不同,实现代码的方式因此也各有千秋。我上面举例的代码仅为最基础,便于理解。在我们以后代码审计中,肯定会遇见各种风格代码,以后遇见再说吧,毕竟以后我们的重心还是在代码审计上面。

Java 反射基础

一、Java反射基础

其实,在 Java 命令执行和 Java 数据库操作章节,我们就已经简单接触到 Java 反射了。

一是Java命令执行章节中,使用java.lang.ProcessImpl的话需要配合反射机制来执行命令。

1
2
3
4
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);

二是Java数据库操作章节中,使用反射机制来注册Mysql驱动。

1
2
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

本节我们就学习下Java反射机制的基础知识吧。

1、什么是反射?

Java 的反射(reflection)机制是指在程序运行中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。 这种动态获取程序信息以及动态调用对象的功能称为 Java语言的反射机制 。

反射机制允许我们在 Java程序运行时检查,操作或者说获取任何类、接口、构造函数、方法和字段。还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等操作。

在 Java代码审计中学习反射机制,我们目的是可以利用反射机制操作目标方法执行系统命令。比如我们想要反射调用java.lang.runtime去执行系统命令。这个下面会讲到。

下面,通过代码案例来学习反射API,进一步理解反射机制。

推荐一篇文章,关于”java的反射到底是有什么用处?怎么用?“:

1
https://www.zhihu.com/question/377483107

2、创建练习项目工程

老规矩,先创建一个名为reflectdemo的项目工程,用于下面示例代码的练习。

①、打开IDEA,点击Create New Project,创建新的工程。

②、左侧选择Maven,配置默认即可,不选择任何模板,点击Next。

③、起个项目名称为reflectdemo,其他默认即可,点击Finish。

④、在Java目录下创建名为com.exampl.demo的包,并在demo包下再创建一个名为entity的包,最终目录结构如下图所示:

目录结构

下面创建个用于练习的类。

①、我们在com.example.demo.entity下创建个User类,代码如下:

1
2
3
4
5
6
7
8
9
public class User {
public String name = "power7089";
public String getName() {
return name;
}
public void setName(String testStr) {
this.name = name;
}
}

User实体类

3、获取 Class 对象

获取 Class 对象的方式有下面几种,:

  • 根据类名:类名.class
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全路径类名)
  • 通过类加载器获得class对象:ClassLoader.getSystemClassLoader().loadClass(“com.example.xxx”);

我们在com.example.demo先创建一个名为GetClass的类,用于演示获取User Class对象几种方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo;
import com.example.demo.entity.User;

public class GetClass {
public static void main(String[] args) throws ClassNotFoundException {
//1.通过类名.class
Class c1 = User.class;

//2.通过对象的getClass()方法
User user = new User();
Class c2 = user.getClass();

//3.通过 Class.forName()获得Class对象;
Class c3 = Class.forName("com.example.demo.entity.User");

//4.通过类加载器获得class对象
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class c4 = classLoader.loadClass("com.example.demo.entity.User");

System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
System.out.println(c4);
}
}

getclass类代码

动手操作调试,观察运行结果,并加以思考。

那他们几个有什么需要注意的呢?

  • 类名.class:需要导入类的包。
  • 对象.getClass():初始化对象后,其实不需要再使用反射了。
  • Class.forName(全路径类名):需要知道类的完整全路径,这是我们常使用的方法。
  • 通过类加载器获得class对象:ClassLoader.getSystemClassLoader().loadClass(“com.example.xxx”);

Class.forName() 获取 class 对象方法是常用的一种方式,下面所有示例代码我们都使用Class.forName()这个方法来获取Class对象。

在获取到目标Class对象后,我们可以做的事就多了,下面我们通过示例代码进一步演示。

4、Java 反射 API

Java 提供了一套反射 API,该API由Class类与java.lang.reflect类库组成。

该类库包含了FieldMethodConstructor等类。

java.lang.reflect官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html

(这部分的官方文档我都是给的 java8 的,大家可以将路径中的数字 8 改为7,9,10等,这几个版本会有不同的地方大家可自行比对学习)


:warning:在进行下面练习前,首先我们需要在com.example.demo下新建一个名为reflectdemo的包,并新建一个名为UserInfo的Java Class,并键入一下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.demo.reflectdemo;

public class UserInfo {
private String name;
public int age;

public UserInfo() { }

private UserInfo(String name) {
this.name = name;
}

public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

private String introduce() {
return "我叫" + name + ",今年" + age + "岁了!";
}

public String sayHello() {
return "Hello!我叫[" + name + "]";
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

userinfo类代码

4.1、java.lang.Class

用来描述类的内部信息,Class的实例可以获取类的包、注解、修饰符、名称、超类、接口等。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

方法名 释义
getPackage() 获取该类的包
getDeclaredAnnotations() 获取该类上所有注解
getModifiers() 获取该类上的修饰符
getName() 获取类名称
getSimpleName() 获取简单类名称
getGenericSuperclass() 获取直属超类
getGenericInterfaces() 获取直属实现的接口
newInstance() 根据构造函数创建一个实例
更多方法可查看官方文档…
4.1.1、示例代码

①、在创建com.example.demo.reflectdemo下创建一个名为ClassDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demo.reflectdemo;
import java.lang.reflect.Modifier;

/**
* 编号7089
*/
public class ClassDemo {

public static void main(String[] args) throws ClassNotFoundException {
Class clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");
// 获取该类所在包路径
Package aPackage = clazz.getPackage();
System.out.println("getPackage运行结果:" + aPackage);

// 获取类上的修饰符
int modifiers = clazz.getModifiers();
String modifier = Modifier.toString(modifiers);
System.out.println("getModifiers运行结果:" + modifier);

// 获取类名称
String name = clazz.getName();
System.out.println("getName运行结果:" + name);
// 获取简单类名
String simpleName = clazz.getSimpleName();
System.out.println("getSimpleName运行结果:" + simpleName);
}
}

classdemo代码

运行结果如下图所示:

classdemo运行结果

4.2、获取属性信息

java.lang.reflect.Field 提供了类的属性信息。可以获取属性(字段)上的注解、修饰符、属性类型、属性名等。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html

方法名 释义
getField(“xxx”) 获取指定名称的公有字段(即使用 public 修饰的字段)的声明信息
getFields() 获取该类以及其父类中所有的公有字段的声明信息
getDeclaredField(“xxx”) 获取指定名称的任意字段的声明信息,无论其访问权限是公有的还是私有的。
getDeclaredFields() 获取该类中所有声明的字段的声明信息,包括公有的、受保护的、默认访问权限的和私有的字段,但不包括父类中的字段
getName() 返回字段的名称。
getType() 返回字段的类型
get() 返回指定对象上该字段的值
set() 将指定对象上的该字段设置为指定的新值
更多方法可查看官方文档…
4.2.1、示例代码

①、在创建com.example.demo.reflectdemo下创建一个名为FieldDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo.reflectdemo;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class FieldDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");

// 获取一个该类或父类中声明为 public 的属性
Field field1 = clazz.getField("age");
System.out.println("getField运行结果:" + field1);

// 获取该类及父类中所有声明为 public 的属性
Field[] fieldArray1 = clazz.getFields();
for (Field field : fieldArray1) {
System.out.println("getFields运行结果:" + field);
}

// 获取一个该类中声明的属性
Field field2 = clazz.getDeclaredField("name");
System.out.println("getDeclaredField运行结果:" + field2);

// 获取某个属性的修饰符(该示例为获取上面name属性的修饰符)
String modifier = Modifier.toString(field2.getModifiers());
System.out.println("getModifiers运行结果: " + modifier);

// 获取该类中所有声明的属性
Field[] fieldArray2 = clazz.getDeclaredFields();
for (Field field : fieldArray2) {
System.out.println("getDeclaredFields运行结果:" + field);
}
}
}

fielddemo代码

运行结果如下图所示:

fielddemo运行结果

4.3、获取方法信息

java.lang.reflect.Method 提供了类的方法信息。可以获取方法上的注解、修饰符、返回值类型、方法名称、所有参数。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html

一些获取方法的函数(包括 java.lang.class 部分),

方法名 释义
getDeclaredMethods() 获取所有在该类中声明的方法,无论其访问修饰符是什么。这个方法返回一个包含 Method 对象的数组
getDeclaredMethod() 获取一个在该类中声明的方法,无论其访问修饰符是什么
getMethod(“setAge”, String.class) 获取目标类及父类中声明为 public 的方法,需要指定方法的入参类型
getMethods() 获取该类及父类中所有声明为 public 的方法
getParameters() 获取所有传参
更多方法可查看官方文档…
4.3.1、示例代码

①、在创建com.example.demo.reflectdemo下创建一个名为MethodDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demo.reflectdemo;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class MethodDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");

// 获取一个该类及父类中声明为 public 的方法,需要指定方法的入参类型
Method method = clazz.getMethod("setName", String.class);
System.out.println("01-getMethod运行结果:" + method);

// 获取所有入参
Parameter[] parameters = method.getParameters();
for (Parameter temp : parameters) {
System.out.println("getParameters运行结果 " + temp);
}

// 获取该类及父类中所有声明为 public 的方法
Method[] methods = clazz.getMethods();
for (Method temp : methods) {
System.out.println("02-getMethods运行结果:" + temp);
}

// 获取一个在该类中声明的方法
Method declaredMethod = clazz.getDeclaredMethod("getName");
System.out.println("03-getDeclaredMethod运行结果:" + declaredMethod);

// 获取所有在该类中声明的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method temp : declaredMethods) {
System.out.println("04-getDeclaredMethods运行结果:" + temp);
}
}
}

methoddemo代码

运行结果如下图所示:

methoddemo运行结果

4.4、获取修饰信息

java.lang.reflect.Modifier 提供了访问修饰符信息。通过ClassFieldMethodConstructor等对象都可以获取修饰符,这个访问修饰符是一个整数,可以通过Modifier.toString方法来查看修饰符描述。并且该类提供了一些静态方法和常量来解码访问修饰符。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Modifier.html

方法名 释义
getModifiers() 获取类的修饰符值
getDeclaredField(“username”).getModifiers() 获取属性的修饰符值
更多方法可查看官方文档…
4.4.1、示例代码

①、在创建com.example.demo.reflectdemo下创建一个名为ModifierDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.demo.reflectdemo;
import java.lang.reflect.Modifier;

/**
* 编号7089
*/
public class ModifierDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");

// 获取类的修饰符值
int modifiers1 = clazz.getModifiers();
System.out.println("获取类的修饰符值getModifiers运行结果:" + modifiers1);

// 获取属性的修饰符值
int modifiers2 = clazz.getDeclaredField("name").getModifiers();
System.out.println("获取属性的修饰符值getModifiers运行结果:" + modifiers2);

// 获取方法的修饰符值
int modifiers4 = clazz.getDeclaredMethod("setName", String.class).getModifiers();
System.out.println("获取方法的修饰符值getModifiers运行结果:" + modifiers4);

// 根据修饰符值,获取修饰符标志的字符串
String modifier = Modifier.toString(modifiers1);
System.out.println("获取类的修饰符值的字符串结果:" + modifier);
System.out.println("获取属性的修饰符值字符串结果:" + Modifier.toString(modifiers2));
}
}

Modifierdemo代码

运行结果如下图所示:

Modifierdemo运行结果

4.5、获取构造函数信息

java.lang.reflect.Constructor 提供了类的构造函数信息。可以获取构造函数上的注解信息、参数类型等。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Constructor.html

方法名 释义
getConstructor() 获取一个声明为 public 构造函数实例
getConstructors() 获取所有声明为 public 构造函数实例
getDeclaredConstructor() 获取一个声明的所有修饰符的构造函数实例
getDeclaredConstructors() 获取所有声明的所有修饰符的构造函数实例
更多方法可查看官方文档…
4.5.1、示例代码

①、在创建com.example.demo.reflectdemo下创建一个名为ConstructorDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo.reflectdemo;
import java.lang.reflect.Constructor;
public class ConstructorDemo {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("com.example.demo.reflectdemo.UserInfo");

// 获取一个声明为 public 构造函数实例
Constructor<?> constructor1 = clazz.getConstructor(String.class,int.class);
System.out.println("1-getConstructor运行结果:" + constructor1);
// 根据构造函数创建一个实例
Object c1 = constructor1.newInstance("power7089",18);
System.out.println("2-newInstance运行结果: " + c1);

// 获取所有声明为 public 构造函数实例
Constructor<?>[] constructorArray1 = clazz.getConstructors();
for (Constructor<?> constructor : constructorArray1) {
System.out.println("3-getConstructors运行结果:" + constructor);
}
// 获取一个声明的构造函数实例
Constructor<?> constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("4-getDeclaredConstructor运行结果:" + constructor2);
// 将构造函数的可访问标志设为 true 后,可以通过私有构造函数创建实例
constructor2.setAccessible(true);
Object o2 = constructor2.newInstance("Power7089666");
System.out.println("5-newInstance运行结果:" + o2);

// 获取所有声明的构造函数实例
Constructor<?>[] constructorArray2 = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructorArray2) {
System.out.println("6-getDeclaredConstructors运行结果:" + constructor);
}
}
}

ConstructorDemo代码

运行结果如下图所示:

ConstructorDemo代码运行结果

4.6、获取参数信息

java.lang.reflect.Parameter 提供了方法的参数信息。可以获取方法上的注解、参数名称、参数类型等。

官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Parameter.html

方法名 释义
getParameters() 获取构造函数/方法的参数
更多方法可查看官方文档…
4.7、绕过私有限制

java.lang.reflect.AccessibleObject 是FieldMethodConstructor类的超类。该类提供了对类、方法、构造函数的访问控制检查的能力(如:私有方法只允许当前类访问)。

访问检查在设置/获取属性、调用方法、创建/初始化类的实例时执行。

方法名 释义
setAccessible() 将可访问标志设为true(默认为false),会关闭访问检查。这样即使是私有的属性、方法或构造函数,也可以访问。
4.7.1、示例代码

可以看ConstructorDemo类代码,涉及到了setAccessible(),如下:

1
2
3
4
5
6
7
// 获取一个声明的构造函数实例
Constructor<?> constructor2 = clazz.getDeclaredConstructor(String.class);
System.out.println("4-getDeclaredConstructor运行结果:" + constructor2);
// 将构造函数的可访问标志设为 true 后,可以通过私有构造函数创建实例
constructor2.setAccessible(true);
Object o2 = constructor2.newInstance("Power7089666");
System.out.println("5-newInstance运行结果:" + o2);

5、常用方法整理

1、getMethod()

getMethod()方法获取的是当前类中所有公共(public)方法。包括从父类里继承来的方法。

2、getDeclaredMethod()

getDeclaredMethod()系列方法获取的是当前类中“声明”的方法,包括private,protected和public,不包含从父类继承来的方法。

3、getConstructor()

getConstructor()方法获取的是当前类声明为公共(public)构造函数实例。

4、getDeclaredConstructor()

getDeclaredConstructor() 方法获取的是当前类声明的构造函数实例,包括private,protected和public。

5、setAccessible()

在获取到私有方法或构造方法后,使用setAccessible(true);,改变其作用域,这样即使是私有的属性,方法,构造函数也都可以访问调用了。

6、newInstance()

将获取到的对象实例化。调用的是这个类的无参构造函数,或者有参构造函数需要设定传参。

他和 new 关键字去实例化对象相似,而 newInstance() 是个函数方法,而 new 是个关键字,这是有区别的。

使用 newInstance 不成功的话可能是因为:①、你使用的类没有无参构造函数,②、你使用的类构造函数是私有的。

当然了,调用无参构造函数使用方法是,Class.newInstance()

调用带参数的构造函数,则是通过 Class 类获取 Constructor,最后调用 Constructor 中的 newInstance(Object … initarges) 方法,这里面 newInstance 中需要传入所需的参数。

7、invoke()

调用包装在当前Method对象中的方法。

invoke传参如下图所示:

invoke传参

二、Java 反射到命令执行

学习ava反射机制,其实我们更关心如何利用Java反射实现命令执行。下面举例讲解下Java反射命令执行的几种情况。

:warning: 首先在com.example.demo下新建一个名为codeexec的包。用于命令执行示例代码的编写调试。最终目录如下图所示:

codeexec包

1、反射之Java.lang.Runtime

下面是两种通过反射java.lang.Runtime来达到命令执行的方式。

1.1、方式一:通过getMethod

由于java.lang.Runtime类的构造函数是私有的,因此不能直接使用newInstance() 创建一个实例。

那为什么这个类的构造函数会是私有的呢?

这涉及到一个“单例模式”的概念。举个例子:我们在链接数据库时只有最开始链接一次,而不是用一次链接一次,如果这样的话,资源消耗太大了。

因此可以将类的构造函数设为私有,再通过静态方法来获取。

由于 java.lang.Runtime 使用了单例模式,我们可以通过 Runtime.getRuntime() 来获取 Runtime 对象。

先看一段代码:

1
2
3
4
5
Class<?> clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(null);
execMethod.invoke(runtime, "calc.exe");

简单解读:

首先通过 Class.forName 获取 java.lang.Runtime。

接下来通过 getMethod() 方法获 exec 方法,在 java 命令执行章节中我们了解到,exec()方法有六种调用方式(重载),我们选择最简单的 String 方式,则 getMethod 方法我们设定的入参方式为String.class

然后获取 getRuntime 方法后,使用 invoke 执行方法。

最后在通过 invoke 方法调用 runtime 对象执行命令。

将上述代码可以简化如下,简化前后有什么区别?大家可自行调试一下,观察不同。

1
2
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

①、在com.example.demo.codeexec下新建以及各名为RuntimeGetMethod的Java Class,并键入以上代码,最终如下图所示:

RuntimeGetMethod

上述两种方式代码,自行调试运行观察结果。

1.2、方式二:通过 getDeclaredConstructor

如果方法或构造函数是私有的,我们可以使用getDeclaredMethodgetDeclaredConstructor来获取执行。

在这里,java.lang.Runtime 的构造函数为私有的,因此我们可以使用 getDeclaredConstructor方法获取java.lang.Runtime并执行。

先看一段代码:

1
2
3
4
5
Class<?> clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
Method c1 = clazz.getMethod("exec", String.class);
c1.invoke(m.newInstance(), "calc.exe");

简单解读:

首先通过Class.forName获取java.lang.Runtime。

接下来通过getDeclaredConstructor获取构造函数。

通过setAccessible(true)设置改变作用域,让我们可以调用他的私有构造函数。

调用exec方法,入参设置为String.class

最后使用Invoke执行方法。

①、在com.example.demo.codeexec下新建以及各名为RuntimeGetDeclaredConstructor的Java Class,并键入以上代码,最终如下图所示:

RuntimeGetDeclaredConstructor代码

自行调试运行观察结果。

2、反射之java.lang.ProcessBuilder

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们可以通过getConstructor()方法实例化该类。当然也可以使用getDeclaredConstructor()方法。

java.lang.ProcessBuilder 有两个构造函数,构造函数也是支持重载的。如下图所示:

1
2
ProcessBuilder(List<String> command)
ProcessBuilder(String... command)

两个构造函数

在Java命令执行章节,我们了解到java.lang.ProcessBuilder使用start()方法执行命令。

我们以ProcessBuilder(List<String> command)为例。进行讲解。

①、在com.example.demo.codeexec下新建以及各名为ProcessBuilderGetConstructor的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.codeexec;
import java.util.Arrays;
import java.util.List;

public class ProcessBuilderGetConstructor {
public static void main(String[] args) throws Exception{
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
Object object = clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"));
clazz.getMethod("start").invoke(object,null);
}
}

ProcessBuilderGetConstructor代码

写法:

1
2
3
4
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)
clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).star
t();
1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(
Arrays.asList("calc.exe")));

大家自行运行观察分析结果。

:warning:注意:

在开头我们介绍引入的是反射调用 java.lang.ProcessImpl,代码如下。留个作业,大家自行调试。

反射这节基础还是比较重要的,希望大家能够积极练习,并记录笔记提交到对应的作业处。

1
2
3
4
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process process = (Process) method.invoke(null, cmds, null, ".", null, true);

至此,Java反射机制知识点到这就结束了。

本章节提到了一些特殊场景以及解决方法。

当然在实际中,我们会遇见各种情况,加以分析,再配合掌握的这些函数方法后,可以更好的解决应对。

Java 序列化与反序列化基础

反序列化漏洞在 Java 代码审计中是非常重要的基础之一。对于基础薄弱的朋友一定要好好学习,刚开始学也不必过多纠结,只需要记住反序列化的方法就好,后面跟着实战一点点领悟其中的奥妙。

一、序列化与反序列化

在了解反序列化之前,一定要先明白序列化。

通过下面一张图,先简单理解序列化与反序列化的关系。

序列化与反序列化

1.1、什么是序列化

序列化是指把 Java 对象转换为字节序列的过程,目的是便于保存在内存、文件、数据库中。

ObjectOutputStream类的writeObject() 方法可以实现序列化。

writeObject()方法:将指定的对象写入 ObjectOutputStream 中。

1
2
public final void writeObject(Object obj)
throws IOException

官方文档详细说明:https://docs.oracle.com/javase/8/docs/api/java/io/ObjectOutputStream.html#writeObject-java.lang.Object-

一个类的对象要想序列化成功,必须满足两个条件:

  • 该类必须实现 java.io.Serializable 接口。
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

1.2、什么是反序列化

反序列化是指把字节序列恢复为 Java 对象的过程。

ObjectInputStream 类的readObject()方法可实现反序列化。

readObject()方法:从ObjectInputStream读取一个对象。

1
2
3
public final Object readObject()
throws IOException,
ClassNotFoundException

官方文档详细说明:https://docs.oracle.com/javase/8/docs/api/java/io/ObjectInputStream.html#readObject--

1.3、用途

此部分引用自:https://xz.aliyun.com/t/12667

1、当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个 Java 进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个 Java 对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出 Java 对象。

2、所以 Java 序列化一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

① 、想把内存中的对象保存到一个文件中或者数据库中时候;
② 、想用套接字在网络上传送对象的时候;
③ 、想通过RMI传输对象的时候

一些应用场景,涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。

1.4、示例代码

老规矩,先新建一个名为serdemo的项目工程,用于下面示例代码的练习。

①、打开IDEA,点击Create New Project,创建新的工程。如果打开IDEA后进入之前项目,则需在左上角点击File —> New -> Porject...即可。

②、左侧选择Maven,配置默认即可,不用选择任何模板,点击Next。

③、起个项目名称为serdemo,其他默认即可,点击Finish。

④、最终目录结构如下图所示:

目录结构

1.3.1、代码 Demo - 1

①、在java目录下创建一个名为HackInfo的Java Class,并键入以下代码,最终如下图所示:

前面提到,一个类的对象要想序列化成功,必须满足两个条件:

①、该类必须显式实现java.io.Serializable接口,以标记其对象是可序列化的。

②、类的所有属性必须是可序列化的。如果有任何属性不是可序列化的,必须将其声明为transient,这样在序列化过程中,这些属性将被忽略,不会被序列化到输出流中。

1
2
3
4
public class HackInfo implements java.io.Serializable{
public String id;
public String team;
}

序列化代码

②、在java目录下创建一个名为SerializeDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String [] args) throws IOException {
HackInfo hack = new HackInfo();
hack.id = "Power7089";
hack.team = "闪石星曜CyberSecurity";

//将序列化后的字节序列写到serializedata.txt文件中
FileOutputStream fileOut = new FileOutputStream("C:\\Users\\powerful\\Desktop\\serializedata.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(hack);
out.close();
fileOut.close();
System.out.println("序列化的数据已经保存在了serializedata.txt文件中");
}
}

序列化代码

③、我们可以使用Notepad++打开serializedata.txt文件,使用Hex-Editor插件以二进制形式查看数据,如下图所示:

序列化txt文件

注意:序列化的数据会有明显的特征,都是以aced 0005 7372开头的。

④、下面们通过反序列化操作,将字节序列还原成对象。在java目录下创建一个名为DeserializeDemo的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
* 编号7089
*/
public class DeserializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {

HackInfo hack = null;
FileInputStream fileIn = new FileInputStream("C:\\Users\\powerful\\Desktop\\serializedata.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
hack = (HackInfo) in.readObject();
in.close();
fileIn.close();

System.out.println("Deserialized Employee...");
System.out.println("Name: " + hack.id);
System.out.println("Address: " + hack.team);
}
}

DeserializeDemo

先从一个简简单单的代码例子理解序列化与反序列化,大家一定要动手调试一下。

1.3.2、代码 Demo - 2
1.3.2.1、transient 关键字

在 Java 中,transient 是一个关键字,用于修饰实例变量。当一个对象被序列化时,transient 修饰的变量的值不会被保存。这个关键字的作用是告诉 JVM 忽略被修饰变量的序列化,使得在反序列化时这些变量的值为其默认值。

一些情况下,你可能不想将某些字段序列化,例如临时计算的值或者不需要持久化的敏感信息,这时你可以使用 transient 关键字来标记这些字段。

1
2
3
class MyClass implements Serializable {
transient int sensitiveData; // 这个字段不会被序列化
}
1.3.2.2、序列化 ID

序列化 ID 是一个与序列化相关的特殊标识符,它在序列化对象时被用来验证发送方和接收方是否加载了与序列化对象兼容的类。当你序列化一个对象时,Java会根据对象的类自动生成一个序列化 ID,这个ID用来标识类的版本信息。在反序列化时,JVM会使用这个序列化 ID 来检查序列化对象和当前加载的类是否兼容。

如果类的结构发生了改变(比如新增了字段、修改了字段类型等),序列化 ID 将会发生变化,如果接收方的类与发送方的类不兼容,反序列化就会失败,抛出 InvalidClassException 异常。

你也可以显式地指定序列化 ID,通过给类添加 serialVersionUID 字段。这样做可以确保即使类的结构发生了变化,序列化 ID 也保持不变,避免了反序列化失败。

1
private static final long serialVersionUID = 123456789L;

当然了,如果没有显示的定义序列化 ID,Java 会根据类的结构自动生成一个序列化 ID。在序列化和反序列化过程中,JVM 会根据序列化数据中记录的序列化 ID 与当前加载的类的序列化 ID 进行比较。即使没有显式设置 serialVersionUID,JVM 仍然会进行校验,确保序列化数据和当前加载的类的版本是兼容的。

1.3.2.3、案例代码

在下面这个代码案例中,设置了 transient 关键字和序列化 ID。

HackInfo2 代码,用于被序列化的代码。

1
2
3
4
5
6
7
8
9
10
import java.io.Serializable;

public class HackInfo2 implements Serializable {
private static final long serialVersionUID = -8619751142754444841L;

public String id;
public String team;
public transient String password; // 不需要序列化的字段

}

SerializeDemo2 代码,用于序列化对象的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class SerializeDemo2 {
public static void main(String [] args) throws IOException {
HackInfo2 hack2 = new HackInfo2();
hack2.id = "Power7089";
hack2.team = "闪石星曜CyberSecurity";
hack2.password = "6666";

//将序列化后的字节序列写到serializedata.txt文件中
FileOutputStream fileOut = new FileOutputStream("C:\\Users\\power\\Desktop\\serializedata.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
System.out.println(out);
out.writeObject(hack2);
out.close();
fileOut.close();
System.out.println("序列化的数据已经保存在了serializedata.txt文件中");
}
}

DeserializeDemo2 代码,用于反序列化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeDemo2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HackInfo2 hack2 = null;
FileInputStream fileIn = new FileInputStream("C:\\Users\\power\\Desktop\\serializedata.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
hack2 = (HackInfo2) in.readObject();
in.close();
fileIn.close();

System.out.println("反序列化恢复字节序列为对象...");
System.out.println("Name: " + hack2.id);
System.out.println("Address: " + hack2.team);
System.out.println("password: " + hack2.password);

}
}
1.3.3、代码 Demo - 3
1.3.3.1、反序列化漏洞简述

在 Java 中有这么一种情况,我们都知道如果一个类实现了 Serializable 接口,那么它的对象可以被序列化和反序列化。

但如果类中定义了 private void readObject(ObjectInputStream ois) 方法,在反序列化过程中,这个方法会在默认的反序列化机制执行之前被调用,允许你在对象反序列化时执行一些自定义的逻辑。

如果你在你的类中重写了 readObject 方法,那么在反序列化这个类的对象时,JVM 会调用你重写的 readObject 方法而不是默认的反序列化机制。这样可以让你在反序列化过程中自定义对象的初始化或者执行其他逻辑。

但如果你想要执行完重写的 readObject 反序列化动作后,还想继续使用原反序列化操作,那就需要使用到defaultReadObject方法,不然执行完自定义的动作后就停止了。也就是说我们重写的 readObject 执行顺序在默认反序列化操作之前。

既然,我们可以在要序列化的类中重写 readObject,这也就意味着给了我们一个攻击的入口,可以实现在服务器上运行代码的,执行命令等能力。

当然了,不会有人这么去写代码。下面我们先了解下重写 readObject 的方式,具体反序列化漏洞,我们在后面阶段配合案例再进一步理解。

反序列化漏洞两个基础条件:

  • 实现了java.io.Serializable接口。
  • 重写了 readObject 方法。
1.3.3.2、漏洞代码

HackInfo3代码,用于被序列化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package demo3;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class HackInfo3 implements Serializable {
private static final long serialVersionUID = -8619751142754444841L;
public String id;
public String team;

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//in.defaultReadObject();

Runtime.getRuntime().exec("calc.exe");

}
}

SerializeDemo3 代码,用于序列化对象的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo3;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeDemo3 {
public static void main(String [] args) throws IOException {
HackInfo3 hack3 = new HackInfo3();
hack3.id = "Power7089";
hack3.team = "闪石星曜CyberSecurity";

//将序列化后的字节序列写到serializedata.txt文件中
FileOutputStream fileOut = new FileOutputStream("C:\\Users\\power\\Desktop\\serializedata.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
System.out.println(out);
out.writeObject(hack3);
out.close();
fileOut.close();
System.out.println("序列化的数据已经保存在了serializedata.txt文件中");
}
}

DeserializeDemo3 代码,用于反序列化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package demo3;

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

public class DeserializeDemo3 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HackInfo3 hack3 = null;
FileInputStream fileIn = new FileInputStream("C:\\Users\\power\\Desktop\\serializedata.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
hack3 = (HackInfo3) in.readObject();
in.close();
fileIn.close();

System.out.println("反序列化恢复字节序列为对象...");
System.out.println("Name: " + hack3.id);
System.out.println("Address: " + hack3.team);

}
}

RMI 基础

1.1、什么是 RMI

RMI 是 Java 中的一种技术,全称为远程方法调用(Remote Method Invocation)。它的作用是允许你在不同的 Java 虚拟机(JVM)之间进行通信,就像在同一个 JVM 中调用方法一样,调用远程方法。这些虚拟机可以在不同的主机上、也可以在同一个主机上。

RMI 是 Java 的一组拥护开发分布式应用程序的 API。RMI 使用 Java 语言接口定义了远程对象,它集合了 Java 序列化和 Java远程方法协议(Java Remote Method Protocol)。

简单地说,原先的程序仅能在同一操作系统的方法调用,通过 RMI 可以变成在不同操作系统之间对程序中方法的调用。

RMI 依赖的通信协议是 JRMP。JRMP: Java 远程方法协议(Java Remote Method Protocol,JRMP),是特定于 Java 技术的通信协议,主要是用于查找和引用远程对象的协议。

RMI 对象是通过序列化方式进行传输的。

Java RMI 的出现可以是客户端运行的程序调用远程服务器上的方法,对于调用的这个过程,我们只需关注对象方法的本身,而不用在关注网络协议的各种问题。

有这么一段描述,讲述了 RMI 的一种使用场景,便于大家去理解。

https://paper.seebug.org/1012/

假设A公司是某个行业的翘楚,开发了一系列行业上领先的软件。B公司想利用A公司的行业优势进行一些数据上的交换和处理。但A公司不可能把其全部软件都部署到B公司,也不能给B公司全部数据的访问权限。于是A公司在现有的软件结构体系不变的前提下开发了一些RMI方法。B公司调用A公司的RMI方法来实现对A公司数据的访问和操作,而所有数据和权限都在A公司的控制范围内,不用担心B公司窃取其数据或者商业机密。

对于开发者来说,远程方法调用就像我们本地调用一个对象的方法一样,他们很多时候不需要关心内部如何实现,只关心传递相应的参数并获取结果就行了。但是对于攻击者来说,要执行攻击还是需要了解一些细节的。

1.2、RMI 中的三个角色

RMI中涉及到三个角色,它们分别为服务端(Server),注册中心(Registry)和客户端(Client),下面是他们的作用。

服务端(Server):负责将远程对象绑定至注册中心。

注册中心(Registry):服务端会将远程对象绑定至此。客户端会向注册中心查询绑定的远程对象。

客户端(Client):与注册中心和服务端交互。

插入一张来自互联网的图片,直观展示了他们的关系。

rmi关系图

存根/桩(Stub):客户端侧的代理,每个远程对象都包含一个代理对象stub,当运行在本地 Java 虚拟机上的程序调用运行在远程 Java 虚拟机上的对象方法时,它首先在本地创建该对象的代理对象 stub, 然后调用代理对象上匹配的方法。

骨架(Skeleton):服务端侧的代理,用于读取 stub 传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。

:warning:注意:在低版本的 JDK 中,Server 与 Registry 是可以不在一台服务器上的,而在高版本的 JDK 中,Server 与 Registry 只能在一台服务器上,否则无法注册成功。比如 Jdk8u121 这个分界线。

1.2.1、存根和骨架

存根和骨架是远程对象实现中的重要组成部分。当您在远程对象上调用方法(可能位于不同的主机上)时,实际上是在调用一些本地代码,它充当该对象的代理。这就是存根。(称其为存根是因为它类似于对象的截断占位符。)而骨架则是另一个代理,它与真实对象一起存在于其原始主机上。它接收来自存根的远程方法调用,并将其传递给对象。

创建存根和骨架后,您无需直接操作它们;它们对您是隐藏的(可以说是藏在壁橱里)。您可以通过运行rmic(RMI编译器)实用程序来为您的远程对象创建存根和骨架。在通常编译Java源文件后,作为第二步,您需要对远程对象类运行rmic。这一过程非常简单;我们将在下面的示例中向您展示具体操作方法。

https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

1.3、示例代码1

老规矩,先创建一个名为rmidemo的工程文件。并在java目录下创建三个包,分别名为methodrmiclientrmiserver。最终目录结构如下图所示:

rmidemo项目目录结构

RMI 代码编写步骤如下:

  1. 创建远程接口及声明远程方法(SayHello.java)
  2. 实现远程接口及远程方法(继承UnicastRemoteObject)(SayHelloImpl.java)
  3. 启动 RMI 注册服务,并注册远程对象(RmiServer.java)
  4. 客户端查找远程对象,并调用远程方法(RmiClient.java)
  5. 执行程序:启动服务端RmiServer;运行客户端RmiClient进行调用

我们对上面流程进行拆解,编写对应的代码。

rmi关系图

①、创建远程接口及声明远程方法

一定要先创建声明一个远程方法,才能用于后续的远程方法调用。在这里我们以经典的Hello World为例。远程方法中的接口均要继承Remote,实际上Remote类中没有任何代码,继承也仅是为了说明该是接口使用于远程方法。

src.main.java.method目录下新建一个名为SayHello的Java Interface,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
package method;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface SayHello extends Remote {
public String sayhello(String name) throws RemoteException;
}

rmi远程接口方法代码

②、实现远程接口及远程方法

此步骤编写远程方法中具体实现代码。需要注意的是,它必须继承UnicastRemoteObject类,表明其可以作为远程对象,并可以被注册到注册中心,最终可以让客户端远程调用。

src.main.java.method目录下新建一个名为SayHelloImpl的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package method;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class SayHelloImpl extends UnicastRemoteObject implements SayHello{

public SayHelloImpl() throws RemoteException {
super();
}

@Override
public String sayhello(String name) throws RemoteException {
return "Hello,i am " + name;
}
}

sayhellimpl代码

③、启动RMI注册服务,并注册远程对象

这个步骤我们是编写RMI服务端代码,需要将上面编写的远程方法注册到注册中心去。

src.main.java.rmiserver目录下新建一个名为RmiServer的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package rmiserver;

import method.SayHello;
import method.SayHelloImpl;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiServer {
public static void main(String[] args) throws RemoteException {
System.out.println("远程方法创建等待调用ing......");
//创建远程对象
SayHello sayhello = new SayHelloImpl();
//创建注册表
Registry registry = LocateRegistry.createRegistry(1099);
//将远程对象注册到注册表里面,并且取名为sayhello
registry.rebind("sayhello",sayhello);

}
}

rmiserver代码

④、客户端查找远程对象,并调用远程方法

这个步骤我们是编写RMI客户端代码,主要是获取到注册中心代理,查询具体注册的远程方法并调用。

src.main.java.rmiclient目录下新建一个名为RmiClient的Java Class,并键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package rmiclient;

import method.SayHello;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 编号7089
*/
public class RmiClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//获取到注册中心的代理
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
//利用注册中心的代理去查询远程注册表中名为sayhello的对象
SayHello sayhello = (SayHello) registry.lookup("sayhello");
//调用远程方法
System.out.println(sayhello.sayhello("POWER7089"));
}
}

rmiclient代码

⑤、执行程序

这个步骤我们运行程序观察结果。

首先,启动服务端RmiServer,如下图所示:

启动rmiserver

最后,启动客户端RmiClient,如下图所示:

启动rmiclient

大家动手调试下吧。

1.4、示例代码2

Calculator 声明远程接口,如下所示:

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Calculator extends Remote {
int add(int a, int b) throws RemoteException;
}

Calculator 接口的具体实现方法 - CalculatorImpl,实现了一个基础的加法计算的方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class CalculatorImpl extends UnicastRemoteObject implements Calculator {
public CalculatorImpl() throws RemoteException {
super();
}

public int add(int a, int b) throws RemoteException {
return a + b;
}
}

RmiServerCalc,RMI 服务器,注册远程对象,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import method.Calculator;
import method.CalculatorImpl;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiServerCalc {
public static void main(String[] args) {
try {
// 创建远程对象实例
Calculator calculator = new CalculatorImpl();
// 将远程对象注册到RMI注册表中
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("CalculatorService", calculator);
System.out.println("Calculator 服务已就绪.");
} catch (Exception e) {
e.printStackTrace();
}
}
}

RmiClientCalc,客户端访问远程对象,调用相关方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import method.Calculator;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiClientCalc {
public static void main(String[] args) {
try {
// 查找远程对象
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
Calculator calculator = (Calculator) registry.lookup("CalculatorService");
// 调用远程方法
int result = calculator.add(5, 3);
System.out.println("5 + 3 = " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

拓展阅读:

1
2
3
4
5
https://paper.seebug.org/1091/
https://y4er.com/posts/java-rmi/
https://goodapple.top/archives/321
https://paper.seebug.org/1251/
https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

JNDI 基础

一、JNDI 介绍

1、什么是 JNDI

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一个Java API,允许 Java 应用程序与命名和目录服务进行交互。这些服务可以是本地的,也可以是分布式的,可以基于各种协议,如LDAP(轻量级目录访问协议)、DNS(域名系统)、NIS(网络信息服务)等。

JNDI 的主要目的是为了统一不同命名和目录服务的访问方式,使得Java应用程序能够以一种统一的方式来访问这些服务,而不必关心底层的实现细节。通过 JNDI,开发人员可以编写与特定命名服务提供者无关的代码,从而实现应用程序的可移植性和灵活性。

JNDI 可以访问的目录及服务,比如:DNS、LDAP、CORBA对象服务、RMI等等。

简单理解:在前面一节我们学到了 RMI,比如 RMI 对外提供了服务,那么 JNDI 可以通过相关 API 链接处理这些服务。

jndi结构

1.1、命名服务 Naming Service

命名服务提供了将名称与对象关联映射的机制。在 Java 中,这些对象通常是网络资源、Java对象、文件、服务等。命名服务允许开发人员使用简单的名称来访问这些对象,而不需要知道其底层的物理位置或其他详细信息。例如,一个Web应用程序可能需要连接到一个数据库,而不需要知道数据库的确切位置。通过命名服务,开发人员可以为数据库分配一个简单的名称,并在需要时通过该名称访问它。

1.2、目录服务 Directory Service

目录服务扩展了命名服务的概念,提供了一种更加结构化和查询友好的方式来组织和管理对象。目录服务通常被用来存储和检索关于用户、组织、网络资源等信息的数据。与命名服务类似,目录服务也使用名称来引用对象,但它们提供了更丰富的查询功能,使得可以根据各种属性进行搜索和过滤。

2、JNDI 的五个包

在 JNDI 中提供了五个作用不同的包。

  • javax.naming
  • javax.naming.directory
  • javax.naming.ldap
  • javax.naming.event
  • javax.naming.spi

2.1、javax.naming

它包含了命名服务的类和接口。比如其中定义了Context接口,可以用于查找、绑定/解除绑定、重命名对象以及创建和销毁子上下文等操作。这个也是我们比较关注的一个包。

  • 查找

最常用的操作是lookup()。你向lookup()提供你想要查找的对象的名称,它返回与该名称绑定的对象。

  • 绑定

listBindings()返回一个名字到对象的绑定的枚举。绑定是一个元组,包含绑定对象的名称、对象的类的名称和对象本身。

  • 列表

list()listBindings()类似,只是它返回一个包含对象名称和对象类名称的名称枚举。list()对于诸如浏览器等想要发现上下文中绑定的对象的信息但又不需要所有实际对象的应用程序来说非常有用。

  • 引用

在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储。

2.1.1、InitialContext类

构造方法:

1
2
3
4
5
6
InitialContext() 
//构建一个初始上下文。
InitialContext(boolean lazy)
//构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment)
//使用提供的环境构建初始上下文。

常用方法:

1
2
3
4
5
bind(Name name, Object obj) //将名称绑定到对象。 
list(String name) //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name) //检索命名对象。
rebind(String name, Object obj) //将名称绑定到对象,覆盖任何现有绑定。
unbind(String name) //取消绑定命名对象。
2.1.2、Reference类

构造方法:

1
2
3
4
5
6
7
8
Reference(String className) 
//为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void add(int posn, RefAddr addr) 
//将地址添加到索引posn的地址列表中。
void add(RefAddr addr)
//将地址添加到地址列表的末尾。
void clear()
//从此引用中删除所有地址。
RefAddr get(int posn)
//检索索引posn上的地址。
RefAddr get(String addrType)
//检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll()
//检索本参考文献中地址的列举。
String getClassName()
//检索引用引用的对象的类名。
String getFactoryClassLocation()
//检索此引用引用的对象的工厂位置。
String getFactoryClassName()
//检索此引用引用对象的工厂的类名。
Object remove(int posn)
//从地址列表中删除索引posn上的地址。
int size()
//检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式。

官方详细介绍:

1
https://docs.oracle.com/javase/tutorial/jndi/overview/naming.html
2.2、javax.naming.directory

继承了 javax.naming,提供了除命名服务外访问目录服务的功能。

官方详细介绍:

1
https://docs.oracle.com/javase/tutorial/jndi/overview/dir.html
2.3、javax.naming.ldap

继承了 javax.naming,提供了访问 LDAP 的能力。

官方详细介绍:

1
https://docs.oracle.com/javase/tutorial/jndi/overview/dir.html
2.4、javax.naming.event

包含了用于支持命名和目录服务中的事件通知的类和接口。

官方详细介绍:

1
https://docs.oracle.com/javase/tutorial/jndi/overview/event.html
2.5、javax.naming.spi

允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过 JNDI 可以访问相关服务。

官方详细介绍:

1
https://docs.oracle.com/javase/tutorial/jndi/overview/event.html

推荐拓展学习:

https://www.docs4dev.com/docs/zh/java/java8/tutorials/jndi-overview-index.html

https://docs.oracle.com/javase/tutorial/jndi/overview/index.html

二、JNDI 操作 RMI

1、创建工程

我们在RMI基础的代码中进行一些修改。

打开rmidemo项目,在src.main.java下新建一个名为jndi的目录,并在该目录下分别新建两个名为JndiClientJndiServer的Java Class。如下图所示:

新建jndi

2、JndiServer 代码

JndiServer中键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
package jndi;

import method.SayHelloImpl;
import javax.naming.InitialContext;

public class JndiServer {
public static void main(String[] args)throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/sayhello",new SayHelloImpl());
System.out.println("启动成功...");
}
}

jndiserver

3、JndiClient 代码

JndiClient中键入以下代码,最终如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package jndi;

import method.SayHello;
import javax.naming.InitialContext;


public class JndiClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
SayHello sayHello = (SayHello)initialContext.lookup("rmi://127.0.0.1:1099/sayhello");
System.out.println(sayHello.sayhello("Power7089"));
}
}

jndiclient

4、运行项目

①、先启动RmiServer

②、再启动JndiServer

③、最后启动JndiClient

观察运行结果。

三、JNDI 操作 DNS

使用 JNDI 向 DNS 服务查询某域名 IP 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;

public class JNDIDNSLookup {
public static void main(String[] args) {
try {
// 设置JNDI属性
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");

// DNS服务器地址
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

// 创建JNDI上下文对象
DirContext ctx = new InitialDirContext(env);

// 要查找的DNS名称
String dnsName = "github.com";

// 获取DNS记录的属性集合,只获取IPv4地址(A记录)
Attributes res = ctx.getAttributes(dnsName, new String[]{"A"});

// 获取IPv4地址属性
Attribute attr = res.get("A");
if (attr != null) {
// 遍历并输出所有IPv4地址
NamingEnumeration<?> ips = attr.getAll();
while (ips.hasMore()) {
System.out.println("IPv4 Address for " + dnsName + ": " + ips.next());
}
} else {
System.out.println("No IPv4 Address found for " + dnsName);
}

// 关闭上下文
ctx.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
}