禁用/避免在AspectJ中执行建议

时间:2012-04-17 16:11:26

标签: java aop aspectj

假设我有一个方面

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,这会创建一个无限循环..

3 个答案:

答案 0 :(得分:9)

为了模拟您的情况,我编写了以下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方面的原始切入点是“调用”时才有效。而不是执行&#39;因为执行实际上会反映出来。

说明:

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

declare precedence: "HackTheHack", "Hack";

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

  1. authHack切入点:pointcut authHack(String user, String pass): call(* Authenticator.authenticate(String,String)) && args(user,pass);使用(在两个方面)call()代替execution()
  2. 我没有在HackTheHack中调用proceed()
  3. 我想推荐给Manning's AspectJ in Action, Second Edition,它让我走上正轨:

      

    6.3.1建议的排序

         

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

         

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

         

    2具有更高优先级的方面在之后的连接点上执行其后建议   优先级较低的方面。

         

    3高优先级方面的周围建议包含了周围的建议   优先级较低的方面。这种安排允许更高的 -   优先级方面来控制低优先级的建议是否会   通过控制调用proceed()来运行。如果优先级更高   方面不会在其建议体中调用proceed(),而不仅仅是   较低优先级方面不执行,但也建议连接点   不会执行。

答案 1 :(得分:3)

实际上,@ 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

身份验证记录器方面:

我想在身份验证方法上添加一个 legal 方面的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

正如您所见,&#34; status&#34;和&#34;认证结果&#34;只要系统没有遭到黑客攻击,它们就是一样的。这里不足为奇。

黑客方面:

现在让我们破解系统。无论我们喜欢什么,我们都可以始终返回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()中最内层的shell调用同一个连接点,其返回值将沿调用链向上传播到记录器方面,这就是记录器在内部方面接收到之后打印已经被操纵的认证结果的原因。

如果我们将声明更改为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()确定),则效果是实际的建议体不会被执行但只是隐式{{1将被调用。这有效地禁用了截获的建议!

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()

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

不幸的是,我发现无法确定周围闭包所针对的方法,因此没有确切的方法来限制反黑客建议的范围,以专门针对我们想要防止黑客入侵的方法提供建议。在这个例子中,我们可以通过试探性地检查由AroundClosure返回的数组的内容来缩小范围,其中包含

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

这些知识没有记录(就像建议执行的内容一样),我通过反复试验找到了。无论如何,这种修改启用了启发式:

String

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

对于长篇答案感到抱歉,但我发现这个问题引人入胜并试图尽可能好地解释我的解决方案。

答案 2 :(得分:-1)

我认为你错过了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;
    }

}