Frage

I have some compiled library which contains a method like this:

public boolean foo(String userID) {
    Class<?> ntSystemClass = Thread.currentThread().getContextClassLoader()
        .loadClass("com.sun.security.auth.module.NTSystem");
    Method getNameMethod = ntSystemClass.getMethod("getName", null);
    Object ntSystem = ntSystemClass.newInstance();
    String name = (String)getNameMethod.invoke(ntSystem, null);
    boolean same=userID.equalsIgnoreCase(name);
    if (same) {
       // more work done here
    } else {
       // more work done here
    }
}

For some rather special use case I need to ensure that the boolean same is always true.

My first approach was extending the class and overriding the method foo() but that wasn't realizable because within the method many references on other library private stuff is required.

So the next approach is using AOP. I tried a few things with AspectJ but didn't find a solution. Can anyone help we with this? As far as I understand I can't alter the boolean same directly. Is there a possibility to address the String name = (String)getNameMethod.invoke(ntSystem, null); in any way just within the library?

War es hilfreich?

Lösung

Let us talk straight here: Henceforth I am assuming that your "special use case" is that you want to tweak/hack user authentication, hopefully in a legal way for testing or whatever.

The main problems to be solved are:

  • It would be easy to just manipulate the result of your foo method (I have renamed it to isCurrentUser(String userID) to clarify its intent). But if I understand correctly, the method has side effects, i.e. it calls other methods, and you want to keep those side effects. So we have to be more careful and use a scalpel, not an axe.
  • There is no pointcut for local variable changes so you have to intercept the method execution or call which changes the local variable's value.
  • AspectJ cannot normally intercept JDK method execution() (unless you want to weave the JDK first, which is possible but out of scope here). Thus, you have to intercept the call() from your own code. I am assuming it is possible to weave into that code even if it is contained in a JAR and you do not have the sources. You can either use LTW for the target class or binary weaving for the JAR, creating a new, woven version of it.
  • Your method call to NTSystem.getName() is not done in a normal way but via Reflection API. Thus, you cannot just use a pointcut like call(NTSystem.getName()) because it will never be triggered. You have to intercept call(public Object Method.invoke(Object, Object...)).
  • Theoretically it could be that other reflective method calls are made from within isCurrentUser(..), so we have to refine our pointcut in order to only match if really NTSystem.getName() is called, not any other method.
  • As a convenience function, we want to be able to dynamically switch the hackingMode on and off.

Now here is a complete, compileable code sample (obviously only working on Windows just like your own code snippet):

Java class with method to be manipulated and main method for demonstration purposes:

package de.scrum_master.app;

import java.lang.reflect.Method;

import de.scrum_master.aspect.TweakAuthenticationAspect;

public class UserAuthentication {
    private static final String USER_NAME_GOOD = "alexander"; // Add your own user name here
    private static final String USER_NAME_BAD = "hacker";

    public static boolean isCurrentUser(String userID) throws Exception {
        Class<?> ntSystemClass = Thread.currentThread().getContextClassLoader()
                .loadClass("com.sun.security.auth.module.NTSystem");
        Method getNameMethod = ntSystemClass.getMethod("getName");
        Object ntSystem = ntSystemClass.newInstance();
        String currentUserID = (String) getNameMethod.invoke(ntSystem);
        boolean same = userID.equalsIgnoreCase(currentUserID);
        if (same) {
            System.out.println("Do something (same == true)");
        } else {
            System.out.println("Do something (same == false)");
        }
        return same;
    }

    public static void main(String[] args) throws Exception {
        testAuthentication(false);
        testAuthentication(true);
    }

    private static void testAuthentication(boolean hackingMode) throws Exception {
        TweakAuthenticationAspect.hackingMode = hackingMode;
        System.out.println("Testing authentication for hackingMode == " + hackingMode);
        System.out.println("Authentication result for " + USER_NAME_GOOD + ": "
                + isCurrentUser(USER_NAME_GOOD));
        System.out.println("Authentication result for " + USER_NAME_BAD + ": "
                + isCurrentUser(USER_NAME_BAD));
        System.out.println();
    }
}

As you can see, testAuthentication(boolean hackingMode) is called twice, once with the hacking code disabled and then enabled. In both cases it tests a good/correct user name (please edit!) first and then a bad one ("hacker").

Aspect manipulating the authentication method:

package de.scrum_master.aspect;

import com.sun.security.auth.module.NTSystem;
import de.scrum_master.app.UserAuthentication;
import java.lang.reflect.Method;

public aspect TweakAuthenticationAspect {
    public static boolean hackingMode = false;

    pointcut reflectiveCall_NTSystem_getName(NTSystem ntSystem, Method method) :
        call(public Object Method.invoke(Object, Object...)) &&
        args(ntSystem, *) &&
        target(method) &&
        if(method.getName().equals("getName"));

    pointcut cflow_isCurrentUser(String userID) :
        cflow(
            execution(* UserAuthentication.isCurrentUser(String)) &&
            args(userID)
        );

    Object around(NTSystem ntSystem, Method method, String userID) :
        reflectiveCall_NTSystem_getName(ntSystem, method) &&
        cflow_isCurrentUser(userID) &&
        if(hackingMode)
    {
        System.out.println("Join point: " + thisJoinPoint);
        System.out.println("Given user ID: " + userID);
        System.out.println("Reflectively called method: " + method);
        return userID;
    }
}

A few words of explanation here:

  • Pointcut reflectiveCall_NTSystem_getName intercepts calls to Method.invoke(..), restricting the first parameter to NTSystem type, which eliminates reflective calls to other classes. It also checks if the target method is actually getName. I.e. the pointcut checks if really NTSystem.getName()` is to be invoked.
  • Pointcut cflow_isCurrentUser catches joinpoints in the control flow of method UserAuthentication.isCurrentUser(..), exposing its parameter userID for later use.
  • The around(NTSystem ntSystem, Method method, String userID) advice combines both pointcuts with && and has access to the three named objects in its signature. In its method body we can do whatever we please with those objects, e.g. print them to the console. We could also change their state, which is not necessary in this case. The advice is activated dynamically via if(hackingMode). If you do not need this, you can remove it, it is just for convenience. Because we use an around() advice here, we can return anything instead of the original method result. In this case, we always return userID, as if the given user was the one currently logged into Windows. This effectively results in the local same variable to become always true because the call to equalsIgnoreCase(..) always returns true as well.
  • I could also have manipulated the result of equalsIgnoreCase(..) directly, but then local variable currentUserID would be non-equal to userID. Depending on which kinds of side effects you want, you can change this behaviour according to your preferences.

Sample output:

Testing authentication for hackingMode == false
Do something (same == true)
Authentication result for alexander: true
Do something (same == false)
Authentication result for hacker: false

Testing authentication for hackingMode == true
Join point: call(Object java.lang.reflect.Method.invoke(Object, Object[]))
Given user ID: alexander
Reflectively called method: public java.lang.String com.sun.security.auth.module.NTSystem.getName()
Do something (same == true)
Authentication result for alexander: true
Join point: call(Object java.lang.reflect.Method.invoke(Object, Object[]))
Given user ID: hacker
Reflectively called method: public java.lang.String com.sun.security.auth.module.NTSystem.getName()
Do something (same == true)
Authentication result for hacker: true

You can see in the upper part that authentication works as usual if hackingMode == false, but will always positively authenticate any given user name if hackingMode == true.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top