草庐IT

springboot+proguard+maven 实现代码混淆 看这一篇就够了

郎涯技术 2023-04-05 原文

使用 proguard 混淆代码只能增加阅读和理解的难度, 并不能百分百保证代码安全。常用的应用场景是项目需要部署到客户机器上,一定程度上防止代码泄露。

proguard 简介

ProGuard 是一个混淆代码的开源项目,它的主要作用是混淆代码,ProGuard 包括以下 4 个功能:

  • 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)

  • 优化(Optimize):对字节码进行优化,移除无用的指令

  • 混淆(Obfuscate):使用 a,b,c,d 这样简短而无意义的名称,对类、字段和方法进行重命名

  • 预检(Preveirfy):在 Java 平台上对处理后的代码进行预检,确保加载的 class 文件是可执行的

实战示例

proguard 是广为使用的工具之一,可是用他的客户端方式来混淆 springboot 项目的时候最后总得不到可执行的 jar。后来发现了 proguard-maven-plugin 这个插件,所有 proguard 的指令都可以在 pom 中进行定义实现,本文基于 springboot2.x + maven + proguard 架构进行代码混淆。

源码:

https://github.com/hacfins/spring-boot-2-api

修改 pom 文件

定义 proguard-maven-plugin 插件且插件位于 spring-boot-maven-plugin 插件的前面

  • proguardInclude

    表示使用外部扩展的配置文件 proguard.cfg,和 pom.xml 同目录

  • keepparameternames

    此选项将保留所有原始方法参数,controller 如果函数的参数也混淆(如混淆为a、b、c等)会导致传参映射不上

详细配置如下(由于有详细注释,不再一一说明):

<build>
    <plugins>
        <!--proguard混淆插件-->
        <plugin>
            <groupId>com.github.wvengen</groupId>
            <artifactId>proguard-maven-plugin</artifactId>
            <version>${proguard-maven-plugin.version}</version>
            <executions>
                <execution>
                    <!--打包的时候开始混淆-->
                    <phase>package</phase>
                    <goals>
                        <goal>proguard</goal>
                    </goals>
                </execution>
            </executions>

            <configuration>
                <proguardVersion>${proguard.version}</proguardVersion>
                <injar>${project.build.finalName}.jar</injar>
                <!--输出的jar-->
                <outjar>${project.build.finalName}.jar</outjar>
                <!--是否混淆-->
                <obfuscate>true</obfuscate>
                <proguardInclude>${basedir}/proguard.cfg</proguardInclude>

                <options>
                    <!--默认开启,不做收缩(删除注释、未被引用代码)-->
                    <option>-dontshrink</option>
                    <!--默认是开启的,这里关闭字节码级别的优化-->
                    <option>-dontoptimize</option>
                    <!--对于类成员的命名的混淆采取唯一策略-->
                    <option>-useuniqueclassmembernames</option>
                    <!--混淆时不生成大小写混合的类名,默认是可以大小写混合-->
                    <option>-dontusemixedcaseclassnames </option>
                    <!--混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
                    <option>-adaptclassstrings</option>

                    <!--对异常、注解信息在runtime予以保留,不然影响springboot启动-->
                    <option>-keepattributes
                        Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
                    </option>

                    <!--此选项将保存接口中的所有原始名称(不混淆)-->
                    <option>-keepnames interface ** { *; }</option>
                    <!--此选项将保存所有软件包中的所有原始接口文件(不进行混淆)-->
                    <!--<option>-keep interface * extends * { *; }</option>-->
                    
                    <!--此选项将保留所有原始方法参数,controller如果参数也混淆会导致传参映射不上  -->
                    <option>-keepparameternames</option>

                    <!--保留枚举成员及方法-->
                    <option>-keepclassmembers enum * { *; }</option>

                    <!--不混淆所有类,保存原始定义的注释-->
                    <!--<option>-keepclassmembers class * {
                        @org.springframework.context.annotation.Bean *;
                        @org.springframework.beans.factory.annotation.Autowired *;
                        @org.springframework.beans.factory.annotation.Value *;
                        @org.springframework.stereotype.Service *;
                        @org.springframework.stereotype.Component *;
                        }
                    </option>-->

                    <!--忽略warn消息-->
                    <option>-ignorewarnings</option>
                    <!--忽略note消息-->
                    <option>-dontnote</option>
                </options>
                <!--java 11-->
                <libs>
                    <lib>${java.home}/jmods/</lib>
                </libs>
                <!--java 8-->
                <!-- <libs>
                     <lib>${java.home}/lib/rt.jar</lib>
                     <lib>${java.home}/lib/jsse.jar</lib>
                 </libs>-->
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>com.guardsquare</groupId>
                    <artifactId>proguard-base</artifactId>
                    <version>${proguard.version}</version>
                </dependency>
            </dependencies>
        </plugin>

        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!--jar可直接运行-->
                <executable>true</executable>
            </configuration>
        </plugin>       
    </plugins>
</build>

proguard.cfg

作为 pom.xml 中的扩展配置,详细配置如下:

#所有类(包括接口)的方法参数不混淆(包括没被keep的),如果参数混淆了,mybatis的mapper参数绑定会出错(如#{id})
-keepattributes MethodParameters

#入口程序类不能混淆,混淆会导致springboot启动不了
-keep class com.langyastudio.edu.admin.Application {
        public static void main(java.lang.String[]);
     }

#mybatis的mapper/实体类不混淆,否则会导致xml配置的mapper找不到
-keep class com.langyastudio.edu.admin.dao.*
-keeppackagenames com.langyastudio.edu.admin.dao

#考虑到scanBasePackages,需要包名不被修改
-keeppackagenames com.langyastudio.edu
-keeppackagenames com.langyastudio.edu.admin.common

#一些配置类比如datasource,aopconfig如果混淆会导致各种启动报错
# 比如用@Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))")
# 指定webLog方法对应的@Pointcut作为切入点,所以包的名字不能修改
-keeppackagenames com.langyastudio.edu.*.controller.**
-keep class com.langyastudio.edu.admin.config.*

#保留Serializable序列化的类不被混淆
#例如传入/输出的Bean属性
-keepclassmembers class * implements java.io.Serializable {*;}

#保留空的构造函数
#-keepclassmembers class com.hacfin.* {
# public <init>(...);
#}

混淆配置要点

  • 建议逐个 java 包定义混淆规则,这样思路更清晰

  • repository(dao)层需要保存包名和类名,因为 Mybatis 的 xml 文件中引用了dao 层的接口

  • controller 层注意在使用 @PathVariable、@RequestParam 时需要显式声明参数名

  • dao 层用于映射数据库表的类和 controller 层映射前台参数的类,都需要保留类成员

  • 修改 spring 的 bean 命名策略,改成按类的全限定名来命名

  • 等等

入口程序类

  • 入口程序类不能混淆,混淆会导致 springboot 启动不了,增加如下配置:
-keep class com.langyastudio.edu.admin.Application {
        public static void main(java.lang.String[]);
     }

bean 名称冲突

  • 默认混淆后的类名为 xx.a.b、xx.c.a,直接使用混淆后的类名作为 bean 会引发重名异常,所以需要修改 BeanName 生成策略。

不能重写 generateBeanName 方法,因为有些 Bean 会自定义 BeanName,所以这些情况还需要走原来的逻辑。

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

    /**
     * 由于需要混淆代码,混淆后类都是A B C,spring 默认是把A B C当成BeanName,BeanName又不能重复导致报错
     * 所以需要重新定义BeanName生成策略
     */
    @Component("UniqueNameGenerator")
    public static class UniqueNameGenerator extends AnnotationBeanNameGenerator
    {
        /**
         * 重写buildDefaultBeanName
         * 其他情况(如自定义BeanName)还是按原来的生成策略,只修改默认(非其他情况)生成的BeanName带上包名
         */
        @Override
        public @NotNull String buildDefaultBeanName(BeanDefinition definition)
        {
            //全限定类名
            return Objects.requireNonNull(definition.getBeanClassName());
        }
    }
}

包名保留

  • 考虑到 scanBasePackages 等特殊的注解配置,需要包名不被修改,配置如下:
-keeppackagenames com.langyastudio.edu
  • scanBasePackages 样例:

如果 com.langyastudio.edu 名称被混淆,将导致 scanBasePackages 失效

@SpringBootApplication(scanBasePackages = {"com.langyastudio.edu.*"})
public class Application
{
    public static void main(String[] args)
    {
		xxxxx
    }
}

配置类

  • 一些配置类比如 datasource、aop、config 等如果混淆会导致各种启动报错

    比如用 @Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))") 指定 webLog 方法对应的 @Pointcut 作为切入点。所以包的名字与函数名称不能修改

    -keeppackagenames com.langyastudio.edu.*.controller.**
    -keep class com.langyastudio.edu.admin.config.*
    
  • 配置类样例:

    public class WebLogAspect
    {
        /**
         * 包及其子包下所有类中的所有方法都应用切面里的通知
         */
        @Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))")
        public void webLog()
        {
        }
    
        @Before("webLog()")
        public void doBefore(JoinPoint joinPoint) throws Throwable
        {
        }
    }
    

dao 层

  • mybatis 的 mapper/实体类 不能混淆,否则会导致 xml 配置的 mapper 找不到

    -keep class com.langyastudio.edu.admin.dao.*
    -keeppackagenames com.langyastudio.edu.admin.dao
    #接口类保留
    xxxx
    

bean 属性保留

  • controller 层映射前台参数的类、后端返回的 bean 属性类等,不能混淆类的成员属性(如变成 string a;

  • 修改方案为保留 Serializable 序列化的类成员不被混淆
    -keepclassmembers class * implements java.io.Serializable {*;}

  • bean 样例:

    需要将原有的属性类增加 Serializable 的继承

@Data
@NoArgsConstructor
public class TokenVO implements Serializable
{
    /**
     * token
     */
    private String token;

    /**
     * token 前缀
     */
    private String tokenHead;
}

编译打包

打包成功一定要运行 编译后的 jar ,测试是否能否正常运行

mvnw clean package  -Dmaven.test.skip=true

混淆效果

用于进一步查看打包后的 jar 文件是否符合要求,同时还可以辅助查找 jar 文件运行失败的原因

常见的反编译工具使用 jd-gui。下载地址:http://java-decompiler.github.io/

直接通过 jd-gui 窗口打开编译打包后的 jar 文件即可。

参考文档

ProGuard manual

Code obfuscation for Spring Boot applications using the ProGuard plugin proguard-maven-plugin

springboot proguard 代码混淆

有关springboot+proguard+maven 实现代码混淆 看这一篇就够了的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  5. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  6. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  7. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  8. 程序员如何提高代码能力? - 2

    前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源

  9. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  10. 7个大一C语言必学的程序 / C语言经典代码大全 - 2

    嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来

随机推荐