一 背景

        Java web项目部署到服务器上以后,尤其针对是在客户的服务器上部署,很容易被“友商”捞到相关的包,通过反编译的手段,我们的代码几乎等同于裸奔在不可管控的服务器上,产品的设计和代码细节都被一览无余,所以针对给厂商做的服务,我们做一些代码的混淆是很有必要的。

   

二 步骤

2.1 导入maven插件

        通过maven插件的办法的好处就是,在编译过程中混淆代码,尽量避免了给业务上带来的影响。这里混淆代码如下,实际混淆过程中要根据实际业务调整


<plugin>
                <groupId>com.github.wvengen</groupId>
                <artifactId>proguard-maven-plugin</artifactId>
                <version>2.0.11</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>proguard</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <proguardVersion>2.0.11</proguardVersion>
                    <injar>${project.build.finalName}.jar</injar>
                    <outjar>${project.build.finalName}.jar</outjar>
                    <obfuscate>true</obfuscate>
                    <proguardInclude>${project.basedir}/proguard.cfg</proguardInclude>
                    <libs>
                        <!-- Include main JAVA library required.-->
                        <lib>${java.home}/lib</lib>
                        <!-- Include crypto JAVA library if necessary.-->
                        <!--<lib>${java.home}/lib/jce.jar</lib>-->
                    </libs>

                    <options>
                        <!-- JDK目标版本1.8-->
                        <option>-target 1.8</option>
                        <!-- 不做收缩(删除注释、未被引用代码)-->
                        <option>-dontshrink</option>
                        <!-- 不做优化(变更代码实现逻辑)-->
                        <option>-dontoptimize</option>
                        <!-- 不路过非公用类文件及成员-->
                        <option>-dontskipnonpubliclibraryclasses</option>
                        <option>-dontskipnonpubliclibraryclassmembers</option>
                        <!--不用大小写混合类名机制-->
                        <option>-dontusemixedcaseclassnames</option>

                        <!-- 优化时允许访问并修改有修饰符的类和类的成员 -->
                        <option>-allowaccessmodification</option>
                        <!-- 确定统一的混淆类的成员名称来增加混淆-->
                        <option>-useuniqueclassmembernames</option>
                        <!-- 不混淆所有包名-->
                        <option>-keeppackagenames</option>
                        <!-- 混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代 -->
                        <option>-adaptclassstrings</option>
                        <!--忽略警告信息-->
                        <option>-ignorewarnings</option>

                        <option>-printseeds</option>

                        <option>-keepattributes
                            Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LocalVariable*Table,*Annotation*,Synthetic,EnclosingMethod
                        </option>

                        <!-- 不混淆所有的set/get方法-->
                        <option>-keepclassmembers public class * {void set*(***);*** get*();}</option>

                        <!-- 不混淆main的信息-->
                        <option>-keepclasseswithmembers public class * { public static void main(java.lang.String[]);}</option>
                        <!-- 不混淆枚举方法的信息-->
                        <option>-keepclassmembers enum * { *; }</option>

                    </options>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>net.sf.proguard</groupId>
                        <artifactId>proguard-base</artifactId>
                        <version>6.2.2</version>
                    </dependency>
                </dependencies>
            </plugin>


2.2 调整spring-boot-maven-plugin

        在使用proguard完成代码混淆的工作后,我们需要构建相关的jar包为可运行的目录结构,这里需要对spring-boot-maven-plugin插件做调整,设置打包策略为repackage,并且将spring-boot-maven-plugin插件位置置于proguard插件之后,这里的原因主要有两个:

  1. proguard混淆代码是在compile和package之后,我们希望在这个过程中,将编译好的class文件能进行混淆,也能够保证最终输出的jar是我们构建的可运行的jar包
  2. proguard混淆插件如果在springboot后使用,那么我们会先将编译好的文件生成jar包,然后repackage,通过springboot-plugin插件构建出最终可运行的jar的目录结构,然后对之前编译好的内容进行混淆,这样会导致覆盖掉之前springboot构建的结果。
<plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <executions>
            <execution>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
</plugin>

2.3 项目构建

        mvn package -Dmaven.test.skip=true

通过maven完成项目的构建过程,可以通过在target下生成的proguard_map.txt文件,查看混淆前后的映射关系

yguard maven混淆 maven代码混淆_spring boot


2.4 项目运行中出现的问题

2.4.1 non-compatible bean definition of same name and class [a]

ConflictingBeanDefinitionException: Annotation-specified bean name 'a' for bean class [a] conflicts with existing, non-compatible bean definition of same name and class [a]

        在proguard混淆代码后,在同一个包下会根据类名生成混淆后的类名,比如a.class ,b.class , c.class等,而通过反编译查看类可知,当我们在其他地方引用时(非同包下),会使用全类名的方式进行引用,这样就避免了使用时出现的问题,而不同的包下的同类名使用时也不会出现任何问题。

        springboot容器启动失败的原因是,在使用到@Controller @Service @Component @Repository等注解时,如果不特别指定value属性,那么默认使用的是类名首字母小写的方式注入到容器中作为beanName的,我们默认的混淆规则在同一个包下根据类名长度会生成出诸如 a.class,b.class等的类名 ,这样在作为beanName时就会产生冲突。因为在容器中beanName要求是唯一的,在不考虑业务的情况下我们可以配置@Primary决定优先使用具体哪个,或者是配置spring参数可以为覆盖的方式。但是都不是混淆后的代码的解决办法。

        解决办法:

        这里采用了自定义BeanNameGenerator的方式强制将beanName重写为全类名,这样加入包名后,在容器就是唯一的beanName。下面的方式是一种模板,这里"xxx"主要针对于某些引入的jar包,可能会由于beanName使用全类名的方式,而在beanFactory中的一级缓存中获取bean实例时,还是采用旧的方式生成的beanName来获取,就会产生异常。

public class ProGuardBeanNameGenerator extends AnnotationBeanNameGenerator {

    /**
     * 重写buildDefaultBeanName
     * 其他情况(如自定义BeanName)还是按原来的生成策略,只修改默认(非其他情况)生成的BeanName带上 包名
     *
     */
    @Override
    public String buildDefaultBeanName(BeanDefinition definition) {
        if("xxx".equals(definition.getBeanClassName())){
            String beanClassName = definition.getBeanClassName();
            Assert.state(beanClassName != null, "No bean class name set");
            String shortClassName = ClassUtils.getShortName(beanClassName);
            return Introspector.decapitalize(shortClassName);
        }
        return definition.getBeanClassName();
    }

}

 在启动类Application,将自定义beanNameGenerator引入。

@EnableCaching
@EnableScheduling
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class)
                .beanNameGenerator(new ProGuardBeanNameGenerator())
                .run(args);
    }

}

2.4.2 @Bean注解生成的bean问题

        springboot同样存在问题的还有Configuration注解下自定义生成的Bean,默认情况下如果我们不增加value属性,那么生成的Bean的beanName为方法名,在代码混淆后,会出现混淆名称重复的问题,和之前的类的名称冲突类似,我们的类中的名称也会出现a,b,c等method。为了避免这样的问题。提供两种方式去改进。

2.4.2.1 通过proguard插件的选项根据包名排除@Configuration的类

<option>-keep class com.xxx.** {
    *;
    }
</option>

2.4.2.2 通过proguard插件直接排除@Configuration下的bean注解

<option>-keep class * {
    @org.springframework.context.annotation.Bean  *;
    }
</option>

2.4.2.3 通过proguard插件排除包含@Configuration包名下的方法名

<option>-keepclassmembers class com.xxx.** {** *;} </option>

2.4.3 @Async 可能会出现循环依赖的问题

Error creating bean with name 'xxx': Bean with name 'xxx' has been injected into other beans [xxx] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

        由于别的bean在引用时没有使用到最终版本的类,所以抛出异常

这里没有最终版本的类指的是,就类似于如果只存在二级缓存,那么就无法在生成代理类和属性注入时,保持使用的是统一的代理类 的道理是一样的。

  这里最简单的办法就是使用懒加载,如果两个Bean真的有依赖的关系 ,就通过懒加载@Lazy去解决。保证了在注入属性时,避免互相依赖的问题。

2.4.4 数据库实体类&& 参数

        这里主要针对的是常用的Mybatis和 Mybatis-plus,在使用到实体类时,由于我们是通过ORM,也就是对象关系映射,来完成数据库字段和属性的映射的,那么我们对于entity的排除就是非常有必要的。

        我们在写mapper的接口时,偶尔会出现入参不加@Param注解的问题,在以往编译中,我们的mapper中的入参的参数名是保留的(这里需要提及一点,这里仅针对mapper的入参,其他的参数名称默认情况下我们是不能通过反射的方式获取到的,也就是在编译后,参数名称丢失了),但是实际混淆后,我们的参数名称也会丢失,这里就会出现一种问题,那就是在写mapper.xml时的sql时是无法注入参数的,因为名称已经丢失,所以这里希望可以按照标准的方式@Param标识Mybatis接口的参数

2.4.5 no converter for type  序列化的问题

        出现这类型的问题,建议是通过混淆选项options的方式将其排除在外,我们混淆的目的是让人无法通过反编译轻松读懂我们的代码,虽然各种问题可以通过微调处理,但实际就我的理解而言,是不必追求混淆的极限的。

2.5 运行

        我们的项目运行起来以后,对项目的影响可以说是微乎其微,既尽量保证了没有将混淆的内容耦合到我们的业务代码中,也保证我们通过springboot插件最终输出的可运行jar结构是稳定不变的。不管最终是通过容器运行,还是通过java的命令运行,都可以顺利的执行起来。

对于上述提到的标签,我附一个官方文档,用于方便查询

ProGuard官方文档

3 结束

        在解决完混淆中引入的问题后,这样的项目通过反编译,会产生出很难理解的一些名称,帮助到我们保护代码。这里要补充一点,如果是我们自己的测试环境还是建议大家关闭混淆,否则你在查看日志时就是一堆看不懂的内容,我们的代码连行号都不显示了。这样做完以后,就大功告成了!

❤❤❤