有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java在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;
}

}

{}方法很重要。黑客拦截对该方法的调用

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

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


共 (3) 个答案

  1. # 1 楼答案

    实际上,user@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()序列中最内层的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()确定),其效果是不会执行实际的通知体,而只会隐式地调用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的签名

    使用目标冰毒的反黑客建议od启发法:

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

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

    我通过反复试验发现,这些知识是没有记录的(就像advice execution的论点内容一样)。无论如何,此修改启用了启发式:

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

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

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

  2. # 2 楼答案

    我想你错过了电话。你可能想要的是这样的:

    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;
        }
    
    }
    
  3. # 3 楼答案

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

    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'
    

    我添加了你的黑客方面:

    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之前调用Hack thehack:

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

    我想向你们介绍一下Manning's AspectJ in Action, Second Edition,它使我在以下方面走上了正确的道路:

    6.3.1 Ordering of advice

    As you’ve just seen, with multiple aspects present in a system, pieces of advice in the different aspects can often apply to a single join point. When this happens, AspectJ uses the following precedence rules to determine the order in which the advice is applied. Later, you’ll see how to control precedence:

    1 The aspect with higher precedence executes its before advice on a join point before the aspect with lower precedence.

    2 The aspect with higher precedence executes its after advice on a join point after the aspect with lower precedence.

    3 The around advice in the higher-precedence aspect encloses the around advice in the lower-precedence aspect. This kind of arrangement allows the higher- precedence aspect to control whether the lower-precedence advice will run by controlling the call to proceed(). If the higher-precedence aspect doesn’t call proceed() in its advice body, not only will the lower-precedence aspects not execute, but the advised join point also won’t execute.