9

假设我有一个方面

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 拦截对此方法的调用。

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

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

4

3 回答 3

9

为了模拟您的情况,我编写了以下身份验证器代码:

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)) && args(user,pass);使用(在两个方面)call()而不是execution()
  2. 我没有打电话给proceed()HackTheHack

我想向您推荐 Manning 的 AspectJ in Action,第二版,它使我走上了正轨:

6.3.1 建议的排序

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

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

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

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

于 2012-04-19T09:25:31.613 回答
4

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

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

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

身份验证器:

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

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()因为建议的签名没有s。before()after()AroundClosure

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

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

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

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

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

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

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

于 2014-06-05T10:51:22.860 回答
-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;
    }

}
于 2012-04-18T00:05:01.877 回答