java安全反射机制

java安全反射机制


反射

指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法。这种动态获取信息,以及动态调用对象方法的功能,叫做java语言的反射机制。


获取class对象

学生对象

package com.test;

public class Student {
public String name;
private String sex;
private int age;

public Student() {
System.out.println("hello");
}

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

private Student(String name, String sex) {
this.name = name;
this.sex = sex;
}

public String getName() {
System.out.println(111);
return name;
}

public void setName(String name) {
this.name = name;
System.out.println(name);
}

public String getSex() {
System.out.println(222);
return sex;
}

private void setSex(String sex) {
System.out.println(sex);
this.sex = sex;
}

public int getAge() {
System.out.println(333);
return age;
}

public void setAge(int age) {
System.out.println(age);
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}


获取对象的方法有三种:

第一种:通过对象获取,因为任何对象都必须和class对象关联

//第一种通过对象获取,任何对象都与class关联
Student student = new Student();
Class student1 = student.getClass();
System.out.println(student1.getName()); //com.test.Student

第二种:通过类对象直接获取

//第二种通过对象获取
Class student1 = Student.class;
System.out.println(student1.getName());

第三种:通过类加载器获得,因为类加载器读取class文件会返回class对象

//第三种通过对象获取
Class student1 = Class.forName("com.test.Student");
System.out.println(student1.getName());

实例化对象

newinstance()实例化对象,它生成的对象只能反射无参的构造函数

实例化无参构造方法

//实例化对象
//第一个表达式
Student.class.newInstance();
//第二个表达式
Class.forName("com.test.Student").newInstance();
//第三个表达式
Object o = Student.class.getConstructor().newInstance();
System.out.println(o);

实例化有参构造方法

配合获取到有参构造方法来实例

Constructor<Student> constructor = Student.class.getConstructor(new Class[]{String.class, String.class, int.class});
Object o = constructor.newInstance(new Object[]{"aming","boy",16});
System.out.println(o);

实例化有参构造方法,调用私有构造方法

Constructor constructor = Student.class.getDeclaredConstructor(new Class[]{String.class});
constructor.setAccessible(true);
Object o = constructor.newInstance("aming");
System.out.println(o);

获取所有构造函数

//实例化对象,
Student student = new Student();
Class c1 = student.getClass();
//获得类的所有公共构造函数
Constructor[] Constructors = c1.getConstructors();
for (int i = 0;i<Constructors.length;i++){
System.out.println(Constructors[i]);
}

调用对象方法

Method类,方法的对象,一个成员方法就是一个Method对象

Method getMethod(String name, Class[] params) – 使用特定的参数类型,获得命名的公共方法

Method[] getMethods() – 获得类的所有公共方法

Method getDeclaredMethod(String name, Class[] params) – 使用特写的参数类型,获得类声明的命名的方法

Method[] getDeclaredMethods() – 获得类声明的所有方法

invoke执行方法

如果这个方法是普通方法,那么第一个参数时类对象

如果这个方法是静态方法,那么第一个参数时类


实例化对象方法,调用有参方法

Student student = new Student();
Class c1 = student.getClass();
Method method = c1.getMethod("setName",String.class);
//如果这个方法是普通方法,那么第一个参数时类对象
method.invoke(c1.newInstance(),"xiaoming");

实例化对象方法,调用无参方法

Student student = new Student();
Class c1 = student.getClass();
Method method = c1.getMethod("getName");
//如果这个方法是普通方法,那么第一个参数时类对象
method.invoke(c1.newInstance());

实例化对象方法,调用私有方法

反射机制的默认行为受限于Java的访问控制,可通过 setAccessible 绕过控制

//实例化对象,有参
Student student = new Student();
Class c1 = student.getClass();
Method method = c1.getDeclaredMethod("setSex",String.class);
//设置对象数组可访问标志
method.setAccessible(true);
//如果这个方法是普通方法,那么第一个参数时类对象
method.invoke(c1.newInstance(),"boy");

获取所有方法,并且打印

//实例化对象,
Student student = new Student();
Class c1 = student.getClass();
//获取这个类中定义了的方法列表
Method[] methods = c1.getDeclaredMethods();
for (int i = 0;i<methods.length;i++){
System.out.println(methods[i]);
}

获取成员变量

getFields()方法获取的是所有的Public的成员变量的信息

getDeclaredFields()获取的是该类所有声明的成员变量信息

set(Object obj, Object value)指定对象变量上此 Field 对象表示的字段设置为指定的新值

get(Object obj)获取属性的值


获取所有的Public的成员变量

Student student = new Student();
Class c1 = student.getClass();
Field[] fields = c1.getFields();
for (Field field :fields){
String fieldName = field.getName();
System.out.println(fieldName);
}

获取所有成员变量

Student student = new Student();
Class c1 = student.getClass();
Field[] fields = c1.getDeclaredFields();
for (Field field :fields){
String fieldName = field.getName();
System.out.println(fieldName);
}

设置成员变量,并且获取成员变量

package com.test;

import java.lang.reflect.Field;

public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//new一个student类
Student student = new Student();
//获取私有的sex方法
Field field = student.getClass().getDeclaredField("sex");
//修改设置对象数组可访问标志
field.setAccessible(true);
//将指定对象变量上此 Field 对象表示的字段设置为指定的新值
field.set(student,"boy");
//取得对象的Field属性值
String sex = (String)field.get(student);
System.out.println(sex);
}
}

通过反射来执行系统命令

首先我们要开始学习Java的命令执行,了解整条调用链


单例模式

简单来说就是一个类只能构建一个对象的设计模式

public class Singleton {
private static Singleton instance; //单例对象
private Singleton (){} //私有构造函数

//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

我们为什么要这样写?


因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:

这样一来,显然instance被构建了两次


双重检测机制

package com.test;

public class Singleton {
private static Singleton instance; //单例对象
private Singleton (){} //私有构造函数

//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null){
instance = new Singleton(); //双重检测机制
}
}
}
return instance;
}
}

我们为什么怎么写?






像这样两次判空的机制叫做双重检测机制


存在的隐患

这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。



JVM编译器指令重排

java中简单的一句instance = new Singleton,会被编译器编译成如下JVM指令:

memory=allocate();    //1:分配对象的内存空间 
ctorInstance(memory); //2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate();    //1:分配对象的内存空间 
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。

此时如果线程2抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

为了避免这种情况,我们需要在instance对象前面增加一个修饰符volatile

volatile修饰符阻止变量访问前后指令重排,保证指令执行顺序

package com.test;

public class Singleton {
private volatile static Singleton instance; //单例对象
private Singleton (){} //私有构造函数

//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null){
instance = new Singleton(); //双重检测机制
}
}
}
return instance;
}
}

经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。



使用静态类实现单例模式

public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}

注意点:


利用反射打破单例

package com.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2)); //false
}
}

如何防止反射的构建方式

通过enum语法糖,jvm会阻止反射获取篇枚举类的私有构造方法

枚举类

package com.test;

public enum SingletonEnum {
INSTANCE;
public void printSomething() {
System.out.println("这是枚举创建实例");
}
}

调用枚举类

package com.test;

public class Main {
public static void main(String[] args) {
SingletonEnum.INSTANCE.printSomething();
}
}

尝试反射调用

package com.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获得构造器
Constructor con = SingletonEnum.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
SingletonEnum singleton1 = (SingletonEnum)con.newInstance();
SingletonEnum singleton2 = (SingletonEnum)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
}
}


枚举实现的优点:




执行命令调用链

通过Runtime类执行命令

package com.test;

import java.io.IOException;

public class Test {
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("calc");
}
}

这是我们一段Runtime类

public class Runtime {
private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}

private Runtime() {}


我们看下如何调用exec的,可以看到构造方法是private,通过public的getRuntime来调用获取到Runtime类调用exec方法

public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.isEmpty())
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}

exec方法

public Process exec(String command)-----在单独的进程中执行指定的字符串命令。
public Process exec(String [] cmdArray)---在单独的进程中执行指定命令和变量
public Process exec(String command, String [] envp)----在指定环境的独立进程中执行指定命令和变量
public Process exec(String [] cmdArray, String [] envp)----在指定环境的独立进程中执行指定的命令和变量
public Process exec(String command,String[] envp,File dir)----在有指定环境和工作目录的独立进程中执行指定的字符串命令
public Process exec(String[] cmdarray,String[] envp,File dir)----在指定环境和工作目录的独立进程中执行指定的命令和变量

我们具体看下调用链,下一个断点




走入到另外一个exec方法

返回了一个ProcessBuilder对象,调用了它的start方法,继续跟进:

产生了一个ProcessImpl对象,调用了它的start方法:

ProcessImple类的start方法,返回了一个ProcessImpl对象:

最终create方法执行了命令

调用链图:



exec获取返回值

通过read来获取流程和子流程的输入流进行输出

package com.test;

import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
InputStream in =Runtime.getRuntime().exec("whoami").getInputStream();
int out = 0;
while ((out = in.read()) != -1){
System.out.println((char)out);
}
in.close();
}
}

我们可以将数据读入缓冲区来读

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
InputStream inputStream =Runtime.getRuntime().exec("whoami").getInputStream();
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

exec方法返回的是一个Process对象,它有这些方法:

destroy() 
杀掉子进程。
exitValue()
返回子进程的出口值。
InputStream getErrorStream()
获得子进程的错误流。
InputStream getInputStream()
获得子进程的输入流。
OutputStream getOutputStream()
获得子进程的输出流。
waitFor()
导致当前线程等待,如果必要,一直要等到由该 Process 对象表示的进程已经终止。

通过反射来执行Runtime.exec方法

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
//获取class对象
Class clazz = Class.forName("java.lang.Runtime");
//获取到getRuntime方法
Method getRuntime = clazz.getMethod("getRuntime");
//获取runtime对象
Object runtime = getRuntime.invoke(Runtime.class);
//获取exec方法
Method exec = clazz.getMethod("exec", String.class);
//执行获取进程
Process process =(Process)exec.invoke(runtime,"whoami");
//获取输入流
InputStream inputStream = process.getInputStream();
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

通过构造方法来执行命令

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {
//获取class对象
Class clazz = Class.forName("java.lang.Runtime");
Constructor constructor = clazz.getDeclaredConstructor();
//Runtime类的构造方法是private
constructor.setAccessible(true);
//获取runtime对象
Object runtime = constructor.newInstance();
//获取exec方法
Method execMethod = clazz.getMethod("exec",String.class);
//获取进程,因为不是静态方法,第一个参数是对象实例
Process process = (Process) execMethod.invoke(runtime,"whoami");
//获取输入流,且打印
InputStream inputStream = process.getInputStream();
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

JDK9开始的新特性,开始对反射做出限制


绕过限制进行执行命令


因为对exec进行限制,我们就要找其他方法进行使用

我们可以使用另一个函数来解决限制问题

public Process exec(String [] cmdArray)---在单独的进程中执行指定命令和变量

在windows中,以cmd /c开头。

而在linux下,可以以/bin/sh -c开头:

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
String[] commond = {"cmd","/c","whoami"};
InputStream inputStream = Runtime.getRuntime().exec(commond).getInputStream();
//获取输入流,且打印
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

ProcessBuilder

我们前面有讲到底层ProcessBuilder调用了start方法造成了命令执行,所以我们可以尝试用ProcessBuilder来代替Runtime

我们可以看到下面两个构造方法

public final class ProcessBuilder
{
private List<String> command;
private File directory;
private Map<String,String> environment;
private boolean redirectErrorStream;
private Redirect[] redirects;

public ProcessBuilder(List<String> command) {
if (command == null)
throw new NullPointerException();
this.command = command;
}

public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}
}

这是start方法,省略一部分

public Process start() throws IOException {
......
return ProcessImpl.start(cmdarray,
environment,
dir,
redirects,
redirectErrorStream);
}

尝试使用ProcessBuilder进行执行

构造方法都是public这也省了不少事

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
InputStream inputStream = new ProcessBuilder("whoami").start().getInputStream();
//获取输入流,且打印
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

通过反射来调用

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
//获取class对象
Class clazz = Class.forName("java.lang.ProcessBuilder");
//获取构造方法
Constructor constructor = clazz.getConstructor(String[].class);
//获取start方法
Method start = clazz.getMethod("start");
//获取对象实例
Object processBuilder = constructor.newInstance(new String[][]{{"whoami"}});
//获取输入流,且打印
InputStream inputStream = ((Process) start.invoke(processBuilder)).getInputStream();
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

ProcessImpl

我们可以看到ProcessBuilder的start方法最终调用的是ProcessImpl的start方法,所以我们还可以用ProcessImpl方法来构造命令执行

此类并没有构造方法,我们只能通过反射其他方法来获取

final class ProcessImpl extends Process {
private static final sun.misc.JavaIOFileDescriptorAccess fdAccess
= sun.misc.SharedSecrets.getJavaIOFileDescriptorAccess();
}

这里有由于没有访问控制,默认都是default,只能在同包和当前类才能调用


这是ProcessImpl的start方法,是一个静态的default方法

// System-dependent portion of ProcessBuilder.start()
static Process start(String cmdarray[],
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
throws IOException
{
......
return new ProcessImpl(cmdarray, envblock, dir,
stdHandles, redirectErrorStream);
}

通过反射来执行

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
//获取class对象
Class clazz = Class.forName("java.lang.ProcessImpl");
//获取start方法
Method method = clazz.getDeclaredMethod("start",String[].class, Map.class,String.class,ProcessBuilder.Redirect[].class,boolean.class);
//修改设置对象数组可访问标志
method.setAccessible(true);
//执行方法,获取输入流,如果第一个参数是null代表这个method是个静态方法
InputStream inputStream = ((Process)method.invoke(null, new String[]{"whoami"}, null, null, null, false)).getInputStream();
//获取输入流,且打印
byte[] bcache = new byte[1024];
int readByte = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while ((readByte = inputStream.read(bcache)) != -1){
outputStream.write(bcache,0,readByte);
}
System.out.println(outputStream.toString());
}
}

参考:https://blog.csdn.net/rfrder/article/details/119541396

参考:https://www.jb51.net/article/217376.htm

参考:https://blog.csdn.net/bjweimengshu/article/details/78716839

上一篇

ClassLoader加载机制