【问题标题】:Disable/Avoid an advice execution in AspectJ在 AspectJ 中禁用/避免建议执行
【发布时间】:2012-04-17 16:11:26
【问题描述】:

假设我有一个方面

public aspect Hack {

pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

boolean around(String user, String pass): authHack(user,pass) {
    out("$$$ " + user + ":" + pass + " $$$");
    return false;
}

}

Authenticator.authenticate 方法很重要。黑客拦截了对该方法的调用。

是否可以编写第二个方面来取消/禁用 Hack 方面的 authHack 建议?

我可以捕捉到around authHack 建议的执行,但如果我想继续进行身份验证,我需要再次调用Authenticator.authenticate,这会创建一个无限循环..

【问题讨论】:

  • 好问题。对于那些阅读,Yaneeve 给出了一个很好的答案,但不要止步于此。 kreigaex 给出的答案让它变得真实,并更进一步。

标签: java aop aspectj


【解决方案1】:

为了模拟你的情况,我写了下面的Authenticator代码:

public class Authenticator {

    public boolean authenticate(String user, String pass) {
        System.out.println("User: '" + user + "', pass: '" + pass + "'");
        return true;
    }

}

这是我的主课:

public class Main {

    public static void main(String[] args) {

        Authenticator authenticator = new Authenticator();

        boolean status = authenticator.authenticate("Yaneeve", "12345");
        System.out.println("Status: '" + status + "'");
    }

}

输出是:

User: 'Yaneeve', pass: '12345'
Status: 'true'

我添加了你的 Hack 方面:

public aspect Hack {

    pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

    boolean around(String user, String pass): authHack(user,pass) {
        System.out.println("$$$ " + user + ":" + pass + " $$$");
        return false;
    }
}

现在的输出是:

$$$ Yaneeve:12345 $$$
Status: 'false'

现在解决方法:

我创建了以下 HackTheHack 方面:

public aspect HackTheHack {

    declare precedence: "HackTheHack", "Hack";

    pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

    boolean around(String user, String pass): authHack(user,pass) {
        boolean status = false;
        try {
            Class<?> klass = Class.forName("Authenticator");
            Object newInstance = klass.newInstance();
            Method authMethod = klass.getDeclaredMethod("authenticate", String.class, String.class);
            status = (Boolean) authMethod.invoke(newInstance, user, pass);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return status;
    }
}

又是输出:

User: 'Yaneeve', pass: '12345'
Status: 'true'

这只有在 Hack 方面的原始切入点是“调用”而不是“执行”时才有效,因为执行实际上捕获了反射。

解释:

我使用 Aspect 优先级在 Hack 之前调用 HackTheHack:

declare precedence: "HackTheHack", "Hack";

然后我使用反射(注意可以并且应该优化以减少方法的重复查找)来简单地调用原始方法而无需 Hack around 建议。由于两件事,这成为可能:

  1. authHack 切入点:pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) &amp;&amp; args(user,pass); 使用(在两个方面)call() 而不是execution()
  2. 我没有在 HackTheHack 中调用 proceed()

我想将您推荐给Manning's AspectJ in Action, Second Edition,这让我走上了正轨:

6.3.1 建议的排序

正如您刚刚看到的,一个系统中存在多个方面,不同方面的建议可以 通常适用于单个连接点。发生这种情况时,AspectJ 使用 以下优先规则来确定其中的顺序 建议被应用。稍后,您将看到如何控制优先级:

1 具有较高优先级的切面在连接上执行其之前的建议 点在优先级较低的方面之前。

2 优先级较高的切面在连接点之后执行其后通知 优先级较低的方面。

3 较高优先级方面的环绕通知将环绕通知包含在 优先级较低的方面。这种安排允许更高的 优先方面来控制较低优先级的建议是否会 通过控制对proceed()的调用来运行。如果更高的优先级 aspect 不会在其通知正文中调用proceed(),不仅 优先级较低的方面不执行,但建议的连接点也执行 不会执行。

【讨论】:

  • 很好的答案。不过,我有一些问题:1) declare precedence: "HackTheHack", "Hack"; 是否必要 2) 我可以将 HackTheHack 设置为先于所有方面吗? 3)如果黑客使用execution怎么办?
  • @elmes 谢谢 :)。回答你的问题。 1) 是的。 2)我想是的......以下声明对我有用,试试吧:声明优先级:“HackTheHack”,“*”; 3)它不会工作。如果 HackTheHack 也使用 execution() 那么你会得到一个无限的调用循环,如果 HackTheHack 使用 call()(和 Hack execution()),那么它只会调用 Hack 已经编织的代码——这不是你想要的。 ..
  • 所以,基本上,AspectJ 有一些限制。例如只需将Hack 更改为使用execution 即可使其不朽 .. ;)
【解决方案2】:

实际上用户@Yaneeve 提出了一个不错的解决方案,但它有一些缺点,例如它

  • 仅适用于call(),不适用于execution()
  • 需要反思,
  • 需要declare precedence,
  • 需要事先知道 hack 的类和包名(好的,这可以通过在优先声明中使用 * 来规避)。

我有一个更稳定的解决方案给你。我已将源代码修改为更真实一点:

验证者:

身份验证器有一个用户数据库(为简单起见硬编码),并且实际上比较用户和密码。

package de.scrum_master.app;

import java.util.HashMap;
import java.util.Map;

public class Authenticator {
    private static final Map<String, String> userDB = new HashMap<>();

    static {
        userDB.put("alice", "aaa");
        userDB.put("bob", "bbb");
        userDB.put("dave", "ddd");
        userDB.put("erin", "eee");
    }

    public boolean authenticate(String user, String pass) {
        return userDB.containsKey(user) && userDB.get(user).equals(pass);
    }
}

应用:

应用程序有一个入口点并尝试验证一些用户,并打印结果:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        Authenticator authenticator = new Authenticator();
        System.out.println("Status: " + authenticator.authenticate("alice",  "aaa"));
        System.out.println("Status: " + authenticator.authenticate("bob",    "xxx"));
        System.out.println("Status: " + authenticator.authenticate("dave",   "ddd"));
        System.out.println("Status: " + authenticator.authenticate("erin",   "xxx"));
        System.out.println("Status: " + authenticator.authenticate("hacker", "xxx"));
    }
}

应用程序的输出如下:

Status: true
Status: false
Status: true
Status: false
Status: false

身份验证记录器方面:

我想添加一个法律方面,其中包含有关身份验证方法的around() 建议,就像稍后的黑客方面一样。

package de.scrum_master.aspect;

import de.scrum_master.app.Authenticator;

public aspect AuthenticationLogger {
    pointcut authentication(String user) :
        execution(boolean Authenticator.authenticate(String, String)) && args(user, *);

    boolean around(String user): authentication(user) {
        boolean result = proceed(user);
        System.out.println("[INFO] Authentication result for '" + user + "' = " + result);
        return result;
    }
}

输出变成:

[INFO] Authentication result for 'alice' = true
Status: true
[INFO] Authentication result for 'bob' = false
Status: false
[INFO] Authentication result for 'dave' = true
Status: true
[INFO] Authentication result for 'erin' = false
Status: false
[INFO] Authentication result for 'hacker' = false
Status: false

如您所见,只要系统没有被黑客入侵,“状态”和“认证结果”是相同的。这并不奇怪。

黑客方面:

现在让我们破解系统。我们可以始终返回 true(肯定的身份验证结果)或始终为某个用户返回 true - 无论我们喜欢什么。如果我们想要它的副作用,我们甚至可以 proceed() 到原来的调用,但我们仍然可以总是返回 true,这就是我们在这个例子中所做的:

package de.scrum_master.hack;

import de.scrum_master.app.Authenticator;

public aspect Hack {
    declare precedence : *, Hack;
    pointcut authentication() :
        execution(boolean Authenticator.authenticate(String, String));

    boolean around(): authentication() {
        System.out.println("Hack is active!");
        proceed();
        return true;
    }
}

输出变为:

Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = true
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = true
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = true
Status: true

因为黑客方面声明自己是通知优先级中的最后一个(即在同一连接点上嵌套的一系列proceed() 调用中的最内层外壳,其返回值将沿调用链向上传播到记录器方面,这就是为什么记录器从内部接收到它之后打印已经操纵的身份验证结果。

如果我们将声明更改为declare precedence : Hack, *;,输出如下:

Hack is active!
[INFO] Authentication result for 'alice' = true
Status: true
Hack is active!
[INFO] Authentication result for 'bob' = false
Status: true
Hack is active!
[INFO] Authentication result for 'dave' = true
Status: true
Hack is active!
[INFO] Authentication result for 'erin' = false
Status: true
Hack is active!
[INFO] Authentication result for 'hacker' = false
Status: true

即记录器现在记录原始结果并将其沿调用链传播到黑客方面,黑客方面可以在最后操纵它,因为它是优先的,因此可以控制整个调用链。拥有最终决定权是黑客通常想要的,但在这种情况下,它会显示记录的内容(一些身份验证为真,一些为假)与应用程序的实际行为方式(始终为真,因为它被黑客入侵)之间的不匹配。

反黑客方面:

现在,最后但并非最不重要的一点是,我们要拦截建议执行并确定它们是否来自可能的黑客方面。好消息是:AspectJ 有一个切入点,称为adviceexecution() - nomen est omen。 :-)

建议执行连接点具有可通过thisJoinPoint.getArgs() 确定的参数。不幸的是,AspectJ 无法通过args() 将它们绑定到参数。如果截获的通知是around() 类型,则第一个adviceexecution() 参数将是AroundClosure 对象。如果在这个闭包对象上调用run() 方法并指定正确的参数(可以通过getState() 确定),效果是不会执行实际的通知主体,而只会隐式调用proceed()。这有效地禁用了拦截的建议!

package de.scrum_master.aspect;

import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;

public aspect AntiHack {
    pointcut catchHack() :
        adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);

    Object around() : catchHack() {
        Object[] adviceArgs = thisJoinPoint.getArgs();
        if (adviceArgs[0] instanceof AroundClosure) {
            AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
            Object[] closureState = aroundClosure.getState();
            System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
            try {
                return aroundClosure.run(closureState);
            } catch (Throwable t) {
                throw new SoftException(t);
            }
        }
        return proceed();
    }
}

结果输出是:

[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'alice' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'bob' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'dave' = true
Status: true
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'erin' = false
Status: false
[WARN] Disabling probable authentication hack: adviceexecution(boolean de.scrum_master.hack.Hack.around(AroundClosure))
[INFO] Authentication result for 'hacker' = false
Status: false

如你所见,

  • 现在的结果与没有黑客方面的结果相同,即我们有效地禁用了它,
  • 没有必要知道黑客方面的类或包名,但是在我们的catchHack() 切入点中,我们指定了一个已知方面的白名单不应禁用,即保持不变,
  • 我们只针对around() 建议,因为before()after() 建议的签名没有AroundClosures。

目标方法启发式反黑客建议:

不幸的是,我发现无法确定周围闭包所针对的方法,因此没有确切的方法将反黑客建议的范围限制为专门针对我们想要防止黑客攻击的方法的建议。在这个例子中,我们可以通过启发式检查AroundClosure.getState()返回的数组的内容来缩小范围,该数组包含

  • advice的目标对象作为第一个参数(我们需要检查它是否是Authenticator实例),
  • 目标方法调用的参数(对于Authenticator.authenticate(),必须有两个Strings)。

这些知识是无证的(就像建议执行的参数的内容一样),我是通过反复试验发现的。无论如何,此修改启用了启发式:

package de.scrum_master.aspect;

import org.aspectj.lang.SoftException;
import org.aspectj.runtime.internal.AroundClosure;

import de.scrum_master.app.Authenticator;

public aspect AntiHack {
    pointcut catchHack() :
        adviceexecution() && ! within(AntiHack) && !within(AuthenticationLogger);

    Object around() : catchHack() {
        Object[] adviceArgs = thisJoinPoint.getArgs();
        if (adviceArgs[0] instanceof AroundClosure) {
            AroundClosure aroundClosure = (AroundClosure) adviceArgs[0];
            Object[] closureState = aroundClosure.getState();
            if (closureState.length == 3
                    && closureState[0] instanceof Authenticator
                    && closureState[1] instanceof String
                    && closureState[2] instanceof String
            ) {
                System.out.println("[WARN] Disabling probable authentication hack: " + thisJoinPointStaticPart);
                try {
                    return aroundClosure.run(closureState);
                } catch (Throwable t) {
                    throw new SoftException(t);
                }
            }
        }
        return proceed();
    }
}

输出与上面保持相同,但如果在黑客方面有多个建议,甚至是多个黑客方面,您会看到差异。这个版本正在缩小范围。如果你想要这个取决于你。我建议你使用更简单的版本。在这种情况下,您只需小心更新切入点以始终拥有最新的白名单。

很抱歉回答冗长,但我发现这个问题很有趣,并试图尽可能好地解释我的解决方案。

【讨论】:

  • 优秀的答案。那些在接受答案后停止阅读的人错过了一种享受!
【解决方案3】:

我认为您错过了proceed() 调用。你可能想要的是这样的:

public aspect Hack {

    pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);

    boolean around(String user, String pass): authHack(user,pass) {
        out("$$$ " + user + ":" + pass + " $$$");
        boolean result = proceed(user,pass);
        return result;
    }

}

【讨论】:

  • 编写第二个方面,取消/禁用 authHack 建议不修复 Hack
  • @Queequeg 我不明白你拒绝了我的回答,因为:1. 你不是那个问的人,2. 这解决了能够进行身份验证的问题,3. 你没有提供更好的替代
  • @anjosc,请不要受到侮辱,请注意,正如 Queequeg 所写,您的回答错过了问题的全部要点,正如她所说,“编写取消/禁用的第二个方面authHack 建议不要修复 Hack”。顺便说一句,从个人经验来看,可能是另一个人对你的答案投了反对票,而不是那个很好地评论为什么它是坏的人。祝你好运
  • @anjosc 否决票是将答案向下推,因为它与问题无关。无意冒犯:)
猜你喜欢
  • 1970-01-01
  • 2014-10-01
  • 2020-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多