I am developing a Spring Boot Application that serves REST HTTP(S) requests. (pretty common).
It works as it is supposed, but after the final (and working) jar is signed (by a valid certificate) all URL mappings stop working, returning only 404 to any request. (Note that the embedded Tomcat server starts without problems and I don't receive any exception)
After some debugging I found that the Java's default ClassLoader (Laucher$AppClassLoader) just doesn't return the classes within the packages I configurated (@ComponentScan) when the jar is signed.
//org.springframework.core.io.support.PathMatchingResourcePatternResolver
//Param 'path' has my valid, existing and desired package with @Controller or @Component inside
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<Resource>(16);
ClassLoader cl = getClassLoader(); //sun.misc.Laucher$AppClassLoader
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
//Empty enumeration when jar is signed
...
}
I tried to use a custom Class Loader without success; same problem.
Since it works when I sign the jar with a self signed certificate, I think that there may be a problem with the signing process that was done by another person. But I can't find any proof of that.
It appears that once signed, I can't list the package content...
I'll try some more tests and add here if I consider useful...
UPDATE
After debugging with the help of a custom Class Loader, I found that:
((java.net.JarURLConnection)new java.net.URL("jar:file:/home/user/my-app-with-dependencies_signed.jar!/META-INF/MANIFEST.MF").openConnection()).getJarEntry();
Ok. Works.
((java.net.JarURLConnection)new java.net.URL("jar:file:/home/user/my-app-with-dependencies_signed.jar!/META-INF/").openConnection()).getJarEntry();
Doesn't work! >.< It throws
Exception occurred in target VM: JAR entry META-INF/ not found in /home/user/my-app-with-dependencies_signed.jar
java.io.FileNotFoundException: JAR entry META-INF/ not found in /home/user/my-app-with-dependencies_signed.jar
at sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:142)
at sun.net.www.protocol.jar.JarURLConnection.getJarEntry(JarURLConnection.java:94)
...
This same second example works when trying to access a unsigned or self-signed jar.
This operation of opening the jar is performed by Spring when reading @Controller and @Component from the given packages in @ComponentScan.
In the same way, Java's Class Loader doesn't read directories content, only specified files.
this.getClass().getClassLoader(); //sun.misc.Launcher$AppClassLoader@18b4aac2
this.getClass().getClassLoader().getResources("META-INF/MANIFEST.MF").hasMoreElements(); //always true
this.getClass().getClassLoader().getResources("META-INF/").hasMoreElements(); //false when signed
UPDATE 2
I got the information about the signature. The people responsible for signatures and certificates actually uses a Windows applications that signs the jar with certificates from Windows-MY keystore and private key from a USB token.
Not that this is certainly the cause, but I think it is important to note that jarsigner
is not used.
UPDATE 3
I created a github repository with a simple test case: https://github.com/jesjobom/signed-jar-class-loader-test
I have reached a solution but the problem still exists.
When loading the classes I pointed via @ComponentScan
Spring asks the ClassLoader (Laucher$AppClassLoader
) for the java.net.URL
for each package I informed. Since, for some unknown reason, I can't load packages/folders, I created a custom ClassLoader that always return the expected URL if the package is mine.
public class CustomClassLoader extends ClassLoader {
...
@Override
public Enumeration<URL> getResources(String name) throws IOException {
if(name.startsWith("com/my/package/")) {
readBasePath(); //obtains path to jar (e.g. "jar:file:/home/app.jar!/")
List<URL> resources = new ArrayList<>();
resources.add(new URL(basePath + name));
return Collections.enumeration(resources);
}
return fallback.getResources(name); //default classloader
}
...
}
Even so, later, Spring tries to load the ".class" from the packages and fails for the same reasons... So, I created a custom implementation of PathMatchingResourcePatternResolver
that will list all the content of the jar (this I can do!) and selects only those within the given package.
public class CustomPathMatchingResourceLoader extends PathMatchingResourcePatternResolver {
@Override
protected Set<Resource> doFindPathMatchingJarResources(final Resource rootDirResource, URL rootDirURL, String subPattern) throws IOException {
try {
String searchBase = ...; //package within jar
String pathBase = ...; //path to jar
URLConnection conn = new URL(pathBase).openConnection();
Set<Resource> resources = new HashSet();
JarFile file = ((JarURLConnection) conn).getJarFile();
Enumeration<JarEntry> entries = file.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith(searchBase) && !entry.getName().endsWith("/")) {
resources.add(new UrlResource(pathBase + entry.getName()));
}
}
return resources;
} catch (Exception e) {
e.printStackTrace();
}
return super.doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern);
}
...
}
So it worked without any interference in the signing process... I am pretty confident that signing with jarsigner
would solve the problem, but I think that it'd be difficult...
Anyway, although it worked, it's not a solution. Therefore, I will not accept this answer as the correct one...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With