ProGuard加密混淆SpringBoot应用代码
背景
我们的项目是基于SpringCloud架构的微服务应用,采用Docker离线部署方式交付客户,通过授权证书来控制应用的许可功能模块和使用时间。我们已经在代码层已经实现:
- 基于多维度硬件指纹的绑定验证,cpu id、mac地址、磁盘序列、系统时钟、应用初始时间等
- 双重时间验证机制(系统时间+硬件时钟)
- 安全续期机制支持离线更新
- 防调试/防篡改保护
来解决离线容器化部署Java应用程序授权问题。
整体流程如下:
该解决方案已基本能解决离线容器化部署Java应用程序授权问题,为了进一步加强安全防止通过反编译代码破解授权证书,我们决定对代码进行加密混淆。
Proguard
ProGuard 是一款开源 Java 类文件压缩器、优化器、混淆器和预验证器。因此,ProGuard 处理的应用程序和库更小、速度更快。
- 缩减步骤检测并删除未使用的类、字段、方法和属性。
- 优化器步骤优化字节码并删除未使用的指令。
- 名称混淆步骤使用简短而无意义的名称重命名剩余的类、字段和方法。
Maven插件
我们的项目是SpringBoot 2.2.9 + jdk1.8,基于Maven构建,因此我们使用Proguard的Maven插件:proguard-maven-plugin来进行自动化代码混淆。下面是SpringBoot项目下基本的proguard-maven-plugin插件配置:
app org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 1.8 org.projectlombok lombok 1.18.24 org.mapstruct mapstruct-processor ${org.mapstruct.version} -Aprojectlombok.classpath=${project.build.outputDirectory} >com/hka/business/uaaserver/license/crypto/LicenseGenerator.java com.github.wvengen proguard-maven-plugin 2.6.0 package proguard 6.2.2 ${project.build.finalName}.jar ${project.build.finalName}.jar true ${project.basedir}/proguard.cfg true ${java.home}/lib/rt.jar ${java.home}/lib/jce.jar ${java.home}/lib/jsse.jar !META-INF/**,!META-INF/versions/9/**.class ${project.basedir}/target net.sf.proguard proguard-base 6.2.2 org.springframework.boot spring-boot-maven-plugin ${spring-boot-dependencies.version} repackage
这里需要重点注意的是proguard-maven-plugin插件配置必须在Maven插件之后(先编译后混淆)。
proguard.cfg配置如下:
#指定Java的版本 -target 1.8 # 保留Spring Boot启动类 -keep class com.hka.business.uaaserver.UaaCenterApplication { *;} -keepclassmembers class com.hka.business.uaaserver.UaaCenterApplication { @* *; } # 保留Spring相关注解 -keep @org.springframework.stereotype.Service class * -keep @org.springframework.stereotype.Component class * -keep @org.springframework.stereotype.Repository class * -keep @org.springframework.stereotype.Controller class * -keep @javax.annotation.PostConstruct class * -keep @lombok.RequiredArgsConstructor class * -keep @lombok.extern.slf4j.Slf4j class * -keep @lombok.Data class * -keep @lombok.AllArgsConstructor class * # 保留MyBatis Mapper接口 -keep @org.apache.ibatis.annotations.Mapper class * -keepclassmembers class * { @org.apache.ibatis.annotations.* *; } # 保留Nacos相关配置 -keep class com.alibaba.nacos.** { *; } # 保留JAXB注解(Spring Boot可能需要) -keepclassmembers class * { @javax.xml.bind.annotation.XmlElement *; @javax.xml.bind.annotation.XmlRootElement *; } # 保留包及其类上的注解 -keep class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { *; } -keepclassmembers class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { @* *; } -keep class com.hka.business.uaaserver.application.** { *; } -keepclassmembers class com.hka.business.uaaserver.application.** { @* *; } -keep class com.hka.business.uaaserver.config.** { *; } -keepclassmembers class com.hka.business.uaaserver.config.** { @* *; } -keep class com.hka.business.uaaserver.infrastructure.** { *; } -keepclassmembers class com.hka.business.uaaserver.infrastructure.** { @* *; } -keep class com.hka.business.uaaserver.interfaces.** { *; } -keepclassmembers class com.hka.business.uaaserver.interfaces.** { @* *; } # 强制混淆的License包 -keep class !com.hka.business.uaaserver.license.** { *; } # 处理Lambda表达式 -keepclassmembers class * { private static synthetic java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda); } # 保留枚举类 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # 保留序列化相关 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # 忽略 javax.activation 包中的类 -dontwarn javax.activation.** # 忽略 javax.xml.bind 包中的类 -dontwarn javax.xml.bind.** # 忽略 module-info 类 -dontwarn module-info -ignorewarnings -dontnote # 配置保留注解 -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
踩过的坑
需要完整配置需要保留的类
使用ProGuard最大的挑战应该是ProGuard默认会处理所有代码,因此需要精确配置哪些类需要保留,哪些需要混淆。特别是对于SpringBoot项目中存在大量注解、序列化、第三方框架、动态注入等场景。最简单的例子就是Spring Boot启动类的配置,不光要配置保留启动类同时还需要配置保留相关注解,否则混淆后的启动类class文件会没有注解。正常proguard.cfg配置片断如下:
# 保留Spring Boot启动类 -keep class com.hka.business.uaaserver.UaaCenterApplication { *;} # 保留Spring Boot启动类注解 -keepclassmembers class com.hka.business.uaaserver.UaaCenterApplication { @* *; }
对于其他普通的类也是一样,比如我们需要保留工程的bi模块不受代码混淆影响,也是需要同时配置相关类和注解保留配置,比如Lambda、Slf4j、mybatis以及spring注解等。proguard.cfg配置片断如下:
# 保留包及其类上的注解 -keep class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { *; } -keepclassmembers class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { @* *; }
Spring Bean 注入问题
在SpringBoot框架中,存在大量基于接口+依赖注入以及动态刷新机制来扩展第三方框架,例如集成SpringBoot Security Oauth2框架时,我们常会通过接口+依赖注入以及动态刷新机制来扩展ClientDetailsService,通过继承JdbcClientDetailsService ,扩展客户端加载机制,在使用数据库数据源基础增加redis缓存。但是我们在注入ClientDetailsService依赖时,无需显示指定注入RedisClientDetailsServiceImpl Bean。
@Slf4j @Service public class RedisClientDetailsServiceImpl extends JdbcClientDetailsService { // 省略 public SecurityBrowserConfig(AuthenticationEntryPoint authenticationEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler, TokenStore tokenStore, UserDetailsService userDetailsService, RedisClientDetailsServiceImpl clientDetailsService) { this.authenticationEntryPoint = authenticationEntryPoint; this.customAccessDeniedHandler = customAccessDeniedHandler; this.tokenStore = tokenStore; this.userDetailsService = userDetailsService; // 这里仅需要通过接口方式动态注入Bean依赖 this.clientDetailsService = clientDetailsService; }
但是通过代码混淆后,无法正常启动服务,出现异常提示如下:
2025-02-20 10:58:03.930 WARN [main]org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext.refresh:559 -Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userApiController' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/interfaces/web/api/UserApiController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userApplicationImpl' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/application/impl/UserApplicationImpl.class]: Unsatisfied dependency expressed through constructor parameter 2; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityBrowserConfig' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/config/SecurityBrowserConfig.class]: Unsatisfied dependency expressed through constructor parameter 4; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.security.oauth2.provider.ClientDetailsService' available: expected single matching bean but found 2: redisClientDetailsServiceImpl,clientDetailsService
这里问题暂时没通过直接修改项目的proguard.cfg配置解决(目前网上暂时没有相关直接的解决方案),而是通过曲线救国方式解决。主要是将项目的授权逻辑剥离封装成独立的纯Java依赖项目,在构建依赖项目时进行代码混淆,避免代码混淆影响Spring Bean依赖关系注入。实际的项目通过私有仓库引入混淆后的依赖包达到代码混淆的目的。
具体实施步骤:
- 剥离授权逻辑,抽象成纯Java项目,没有任何Bean依赖和注解。
- 依赖项目集成proguard-maven-plugin插件,支持在推送依赖到私有仓库时进行代码混淆,具体就是在执行mvn:deploy命令触发代码混淆。
- 推送依赖到私有仓库
- 应用项目引入授权依赖
-
下面是依赖项目的proguard-maven-plugin插件配置:
基本和上文的配置一致,只是多了deploy触发proguard的配置。
org.apache.maven.plugins maven-compiler-plugin 3.3 1.8 1.8 org.apache.maven.plugins maven-deploy-plugin 2.8.2 false ${project.build.directory}/${project.build.finalName}.jar com.github.wvengen proguard-maven-plugin 2.6.0 package-proguard package proguard deploy-proguard deploy proguard 6.2.2 ${project.build.finalName}.jar ${project.build.finalName}.jar true ${project.basedir}/proguard.cfg true ${java.home}/lib/rt.jar ${java.home}/lib/jce.jar ${java.home}/lib/jsse.jar !META-INF/**,!META-INF/versions/9/**.class ${project.basedir}/target net.sf.proguard proguard-base 6.2.2
proguard.cfg配置如下:
纯Java依赖项目的proguard.cfg配置就非常简单,仅需要保留保留所有公共类和方法配置,其他基本可以全部使用插件的默认配置。
# 指定Java的版本 -target 1.8 # 保留所有公共类和方法 -keep public class * { public *; } # 忽略 javax.activation 包中的类 -dontwarn javax.activation.** # 忽略 javax.xml.bind 包中的类 -dontwarn javax.xml.bind.** # 忽略 module-info 类 -dontwarn module-info -ignorewarnings -dontnote
以上就是我们使用proguard代码混淆的分享。也希望有大佬看到我的帖子可以帮忙分享Spring Bean 注入问题的解决方案。
参考
A慧眼如炬-ProGuard加密混淆Java代码
proguard