背景
我们的项目是基于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插件配置:
<build>
<finalName>app</finalName>
<plugins>
<!-- 先编译 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <!-- 使用与你的 Maven 版本兼容的版本 -->
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version> <!-- 使用与你的 Maven 版本兼容的版本 -->
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aprojectlombok.classpath=${project.build.outputDirectory}</arg>
</compilerArgs>
<excludes>
<exclude>>com/hka/business/uaaserver/license/crypto/LicenseGenerator.java</exclude>
</excludes>
</configuration>
</plugin>
<!-- 代码混淆编译配置: -->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<!-- 以下配置说明执行mvn的package命令时候,会执行proguard-->
<execution>
<phase>package</phase>
<goals>
<goal>proguard</goal>
</goals>
</execution>
</executions>
<configuration>
<proguardVersion>6.2.2</proguardVersion>
<!-- 就是输入Jar的名称,路径必须包含.jar-->
<injar>${project.build.finalName}.jar</injar>
<!-- 输出jar名称,路径必须包含.jar -->
<outjar>${project.build.finalName}.jar</outjar>
<!-- 是否混淆 默认是true -->
<obfuscate>true</obfuscate>
<!-- 引入外部配置proguard.cfg代替options标签 -->
<proguardInclude>${project.basedir}/proguard.cfg</proguardInclude>
<!-- 额外的jar包,通常是项目编译所需要的jar -->
<putLibraryJarsInTempDir>true</putLibraryJarsInTempDir>
<libs>
<lib>${java.home}/lib/rt.jar</lib>
<lib>${java.home}/lib/jce.jar</lib>
<lib>${java.home}/lib/jsse.jar</lib>
</libs>
<!-- 对输入jar进行过滤比如,如下配置就是对META-INFO文件不处理。 -->
<inLibsFilter>!META-INF/**,!META-INF/versions/9/**.class</inLibsFilter>
<!-- 输出路径配置,但是要注意这个路径必须要包括injar标签填写的jar,把jar包放到临时目录以便缩短命令行,解决windows的cmd有长度限制,出现CreateProcess error=206, 文件名或扩展名太长异常 -->
<outputDirectory>${project.basedir}/target</outputDirectory>
<!--options标签配置混淆的一些细节选项,比如哪些类不需要混淆,哪些需要混淆-->
<options>
<!-- proguard-maven-plugin支持通过option标签配置以及通过proguardInclude引入外部proguard.cfg配置来指定混淆规则,我们采用proguard.cfg方式,避免pom文件过长 -->
</options>
</configuration>
<dependencies>
<dependency>
<groupId>net.sf.proguard</groupId>
<artifactId>proguard-base</artifactId>
<version>6.2.2</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-dependencies.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
这里需要重点注意的是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。
java">@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的配置。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- 配置 maven-deploy-plugin 排除 sources 文件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<configuration>
<skip>false</skip>
<file>${project.build.directory}/${project.build.finalName}.jar</file> <!-- 禁用附加文件 -->
</configuration>
</plugin>
<!-- 代码混淆编译配置: -->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<!-- 以下配置说明执行mvn的package命令时候,会执行proguard-->
<execution>
<id>package-proguard</id>
<phase>package</phase>
<goals>
<goal>proguard</goal>
</goals>
</execution>
<!-- 以下配置说明执行mvn的deploy命令时候,会执行proguard-->
<execution>
<id>deploy-proguard</id>
<phase>deploy</phase>
<goals>
<goal>proguard</goal>
</goals>
</execution>
</executions>
<configuration>
<proguardVersion>6.2.2</proguardVersion>
<injar>${project.build.finalName}.jar</injar>
<outjar>${project.build.finalName}.jar</outjar>
<obfuscate>true</obfuscate>
<proguardInclude>${project.basedir}/proguard.cfg</proguardInclude>
<putLibraryJarsInTempDir>true</putLibraryJarsInTempDir>
<libs>
<lib>${java.home}/lib/rt.jar</lib>
<lib>${java.home}/lib/jce.jar</lib>
<lib>${java.home}/lib/jsse.jar</lib>
</libs>
<inLibsFilter>!META-INF/**,!META-INF/versions/9/**.class</inLibsFilter>
<outputDirectory>${project.basedir}/target</outputDirectory>
<options>
</options>
</configuration>
<dependencies>
<dependency>
<groupId>net.sf.proguard</groupId>
<artifactId>proguard-base</artifactId>
<version>6.2.2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
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